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
25pub const COMPILE_SCHEMA_VERSION: u32 = 1;
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30pub struct CompilerIdentity {
31 pub name: &'static str,
32 pub version: &'static str,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum CompileStatus {
39 Success,
40 Failure,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "kebab-case")]
46pub enum CompileFailureClass {
47 Frontend,
48 Integration,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct CompileDiagnostic {
55 pub severity: crate::tooling::DiagnosticSeverity,
56 pub code: String,
57 pub message: String,
58 pub span: Option<crate::tooling::SourceLocation>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub script: Option<ScriptDiagnostic>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct CompileResult {
67 pub status: CompileStatus,
68 pub exit_code: u8,
69 pub failure_class: Option<CompileFailureClass>,
70 pub diagnostics: Vec<CompileDiagnostic>,
71 pub stdout: String,
72 pub workshop_exact: String,
73 pub workshop: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct CompileReport {
80 pub schema_version: u32,
81 pub compiler: CompilerIdentity,
82 pub catalog: CatalogIdentity,
83 pub compile: CompileResult,
84}
85
86impl CompilerIdentity {
87 fn current() -> Self {
88 Self {
89 name: "opy-rs",
90 version: env!("CARGO_PKG_VERSION"),
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct IntegrationDiagnostic {
98 pub code: String,
99 pub message: String,
100 pub span: Option<HirSpan>,
101 pub script: Option<Box<ScriptDiagnostic>>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106#[serde(rename_all = "camelCase")]
107pub struct ScriptDiagnostic {
108 pub source_name: Option<String>,
109 pub line: Option<u32>,
110 pub column: Option<u32>,
111 pub stack: Option<String>,
112}
113
114impl IntegrationDiagnostic {
115 fn new(code: impl Into<String>, message: impl Into<String>, span: Option<HirSpan>) -> Self {
116 Self {
117 code: code.into(),
118 message: message.into(),
119 span,
120 script: None,
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct IntegrationError {
128 pub diagnostic: IntegrationDiagnostic,
129}
130
131impl IntegrationError {
132 fn new(code: impl Into<String>, message: impl Into<String>, span: Option<HirSpan>) -> Self {
133 Self {
134 diagnostic: IntegrationDiagnostic::new(code, message, span),
135 }
136 }
137
138 fn post_compile_hook(error: crate::macro_js::MacroError, span: Option<HirSpan>) -> Self {
139 let message = error.to_string();
140 let script = match error {
141 crate::macro_js::MacroError::Script(error) => Some(Box::new(ScriptDiagnostic {
142 source_name: error.source_name,
143 line: error.line,
144 column: error.column,
145 stack: error.stack,
146 })),
147 crate::macro_js::MacroError::InvalidResult { .. }
148 | crate::macro_js::MacroError::Internal(_) => None,
149 };
150 Self {
151 diagnostic: IntegrationDiagnostic {
152 code: "post-compile-hook".to_string(),
153 message,
154 span,
155 script,
156 },
157 }
158 }
159}
160
161impl std::fmt::Display for IntegrationError {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 write!(f, "{}: {}", self.diagnostic.code, self.diagnostic.message)
164 }
165}
166
167impl std::error::Error for IntegrationError {}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct LinkReport {
172 pub catalog_ids_checked: usize,
173 pub domains_checked: usize,
174}
175
176pub(crate) fn cross_check_manifest(
180 manifest: &Manifest,
181 catalog: &Catalog,
182) -> Result<LinkReport, IntegrationError> {
183 let mut catalog_ids_checked = 0;
184 let mut domains_checked = 0;
185
186 for function in &manifest.functions {
187 if let Some(catalog_id) = &function.catalog_id {
188 let kind = match function.kind {
189 FunctionKind::Action | FunctionKind::MemberAction => Kind::Action,
190 FunctionKind::Value | FunctionKind::MemberValue => Kind::Value,
191 };
192 catalog_ids_checked += 1;
193 if catalog.entry(kind, catalog_id).is_none() {
194 return Err(IntegrationError::new(
195 "catalog-link-missing",
196 format!(
197 "manifest function '{}' links to missing {:?} catalog id '{}'",
198 function.id, kind, catalog_id
199 ),
200 None,
201 ));
202 }
203 }
204
205 for parameter in &function.params {
206 let Some(domain) = ¶meter.domain else {
207 continue;
208 };
209 let contextual = function
210 .contextual_domain
211 .as_ref()
212 .is_some_and(|context| context.domain == *domain);
213 if contextual {
214 continue;
215 }
216 domains_checked += 1;
217 if catalog.enum_domain(domain).is_none() {
218 return Err(IntegrationError::new(
219 "domain-link-missing",
220 format!(
221 "manifest function '{}' parameter '{}' links to missing enum domain '{}',",
222 function.id, parameter.name, domain
223 ),
224 None,
225 ));
226 }
227 }
228
229 if let Some(contextual) = &function.contextual_domain {
230 for option in contextual.options.values() {
231 domains_checked += 1;
232 if catalog.enum_domain(&option.domain).is_none() {
233 return Err(IntegrationError::new(
234 "domain-link-missing",
235 format!(
236 "manifest function '{}' contextual option links to missing enum domain '{}'",
237 function.id, option.domain
238 ),
239 None,
240 ));
241 }
242 }
243 }
244 }
245
246 Ok(LinkReport {
247 catalog_ids_checked,
248 domains_checked,
249 })
250}
251
252pub struct Compiler {
255 catalog: Catalog,
256 manifest: &'static Manifest,
257 links: LinkReport,
258}
259
260impl Compiler {
261 pub fn new() -> Result<Self, IntegrationError> {
262 let catalog = Catalog::builtin()
263 .map_err(|error| IntegrationError::new("catalog-load", error.to_string(), None))?;
264 let manifest = Manifest::builtin()
265 .map_err(|error| IntegrationError::new("manifest-load", error.to_string(), None))?;
266 let links = cross_check_manifest(manifest, &catalog)?;
267 let identity = catalog.identity();
268 if identity.implementation_version != WORKSHOP_RS_VERSION {
269 return Err(IntegrationError::new(
270 "workshop-contract-version",
271 format!(
272 "expected workshop-rs {}, loaded {}",
273 WORKSHOP_RS_VERSION, identity.implementation_version
274 ),
275 None,
276 ));
277 }
278 Ok(Self {
279 catalog,
280 manifest,
281 links,
282 })
283 }
284
285 pub fn catalog_identity(&self) -> CatalogIdentity {
286 self.catalog.identity()
287 }
288
289 pub fn link_report(&self) -> LinkReport {
290 self.links
291 }
292
293 pub fn compile_hir(&self, hir: &hir::Program) -> Result<CompilationArtifact, IntegrationError> {
296 self.compile_hir_with_locale(hir, &Locale::new("en-US"))
297 }
298
299 pub fn compile_hir_with_locale(
301 &self,
302 hir: &hir::Program,
303 locale: &Locale,
304 ) -> Result<CompilationArtifact, IntegrationError> {
305 if !self.catalog.supports(locale) {
306 return Err(IntegrationError::new(
307 "locale-unsupported",
308 format!("workshop catalog does not declare locale '{locale}'"),
309 None,
310 ));
311 }
312 reject_unlowered_directives(hir)?;
313 let expanded_hir = expand_macros(hir)?;
314 let mut lowering = Lowering::new(self, &expanded_hir)?;
315 lowering.copy_files()?;
316 lowering.lower_declarations()?;
317 lowering.lower_rules()?;
318
319 lowering.wir.validate().map_err(|error| {
320 let span = error
321 .span()
322 .and_then(|span| lowering.hir_span_from_workshop(span));
323 IntegrationError::new(error.code(), error.message(), span)
324 })?;
325 workshop_rs::validate::validate_canonical_ids(&lowering.wir, &self.catalog).map_err(
326 |error| {
327 let span = workshop_error_span(&error)
328 .and_then(|span| lowering.hir_span_from_workshop(span));
329 IntegrationError::new("catalog-validation", error.to_string(), span)
330 },
331 )?;
332 let emitted =
333 workshop_rs::emitter::emit(&lowering.wir, &self.catalog, locale).map_err(|error| {
334 let span = workshop_error_span(&error)
335 .and_then(|span| lowering.hir_span_from_workshop(span));
336 IntegrationError::new("workshop-emission", error.to_string(), span)
337 })?;
338
339 Ok(CompilationArtifact {
340 wir: lowering.wir,
341 final_output: emitted.clone(),
342 emitted,
343 catalog_identity: self.catalog.identity(),
344 hook_console_output: Vec::new(),
345 })
346 }
347
348 pub fn compile_source(
354 &self,
355 source: &str,
356 main_path: &str,
357 root: &std::path::Path,
358 ) -> Result<CompileOutput, IntegrationError> {
359 self.compile_source_with_language(source, main_path, root, "en-US")
360 }
361
362 pub fn compile_source_with_language(
365 &self,
366 source: &str,
367 main_path: &str,
368 root: &std::path::Path,
369 language: &str,
370 ) -> Result<CompileOutput, IntegrationError> {
371 self.compile_source_with_locale(source, main_path, root, &Locale::new(language))
372 .map(CompilationArtifact::into_output)
373 }
374
375 pub fn compile_source_with_locale(
380 &self,
381 source: &str,
382 main_path: &str,
383 root: &std::path::Path,
384 locale: &Locale,
385 ) -> Result<CompilationArtifact, IntegrationError> {
386 self.compile_source_internal(source, main_path, root, locale)
387 }
388
389 pub fn compile_source_artifact(
392 &self,
393 source: &str,
394 main_path: &str,
395 root: &std::path::Path,
396 ) -> Result<CompilationArtifact, IntegrationError> {
397 self.compile_source_with_locale(source, main_path, root, &Locale::new("en-US"))
398 }
399
400 pub fn compile_source_report(
403 &self,
404 source: &str,
405 main_path: &str,
406 root: &std::path::Path,
407 ) -> CompileReport {
408 self.compile_source_report_with_language(source, main_path, root, "en-US")
409 }
410
411 pub fn compile_source_report_with_language(
414 &self,
415 source: &str,
416 main_path: &str,
417 root: &std::path::Path,
418 language: &str,
419 ) -> CompileReport {
420 self.compile_source_report_with_locale(source, main_path, root, &Locale::new(language))
421 }
422
423 pub fn compile_source_report_with_locale(
426 &self,
427 source: &str,
428 main_path: &str,
429 root: &std::path::Path,
430 locale: &Locale,
431 ) -> CompileReport {
432 let outcome = crate::compile_with_overlay_outcome(
433 source,
434 main_path,
435 root,
436 &std::collections::BTreeMap::new(),
437 );
438 let catalog = self.catalog.identity();
439 let compiler = CompilerIdentity::current();
440 let Some(hir) = outcome.hir else {
441 let error = outcome
442 .error
443 .expect("failed frontend compile has diagnostic");
444 let diagnostic = CompileDiagnostic {
445 severity: crate::tooling::DiagnosticSeverity::Error,
446 code: error.code,
447 message: error.message,
448 span: error
449 .span
450 .and_then(|span| source_location_from_file_records(span, &outcome.files)),
451 script: None,
452 };
453 return CompileReport::failure(
454 compiler,
455 catalog,
456 CompileFailureClass::Frontend,
457 vec![diagnostic],
458 );
459 };
460
461 match self.compile_hir_with_locale_and_hook(&hir, outcome.post_compile_hook, locale) {
462 Ok(artifact) => CompileReport::success(compiler, catalog, artifact),
463 Err(error) => CompileReport::failure(
464 compiler,
465 catalog,
466 CompileFailureClass::Integration,
467 vec![compile_diagnostic(error, &hir.files)],
468 ),
469 }
470 }
471
472 fn compile_source_internal(
473 &self,
474 source: &str,
475 main_path: &str,
476 root: &std::path::Path,
477 locale: &Locale,
478 ) -> Result<CompilationArtifact, IntegrationError> {
479 let outcome = crate::compile_with_overlay_outcome(
480 source,
481 main_path,
482 root,
483 &std::collections::BTreeMap::new(),
484 );
485 let hir = outcome.hir.ok_or_else(|| {
486 let error = outcome
487 .error
488 .expect("failed frontend compile has diagnostic");
489 IntegrationError::new(
490 error.code,
491 error.message,
492 error.span.map(hir_span_from_diag),
493 )
494 })?;
495 self.compile_hir_with_locale_and_hook(&hir, outcome.post_compile_hook, locale)
496 }
497
498 fn compile_hir_with_locale_and_hook(
499 &self,
500 hir: &hir::Program,
501 hook: Option<crate::PostCompileHookRecord>,
502 locale: &Locale,
503 ) -> Result<CompilationArtifact, IntegrationError> {
504 let mut artifact = self.compile_hir_with_locale(hir, locale)?;
505 if let Some(hook) = hook {
506 let runtime = crate::macro_js::MacroRuntime::new(crate::macro_js::Limits::default());
507 let result = runtime
508 .run_hook(&hook.source, &artifact.emitted, &hook.script)
509 .map_err(|error| {
510 IntegrationError::post_compile_hook(error, hook.span.map(hir_span_from_diag))
511 })?;
512 artifact.final_output = result.text;
513 artifact.hook_console_output = result.console_output;
514 }
515 Ok(artifact)
516 }
517}
518
519impl CompileReport {
520 fn success(
521 compiler: CompilerIdentity,
522 catalog: CatalogIdentity,
523 artifact: CompilationArtifact,
524 ) -> Self {
525 Self {
526 schema_version: COMPILE_SCHEMA_VERSION,
527 compiler,
528 catalog,
529 compile: CompileResult {
530 status: CompileStatus::Success,
531 exit_code: 0,
532 failure_class: None,
533 diagnostics: Vec::new(),
534 stdout: String::new(),
535 workshop_exact: artifact.final_output.clone(),
536 workshop: normalize_workshop(&artifact.final_output),
537 },
538 }
539 }
540
541 fn failure(
542 compiler: CompilerIdentity,
543 catalog: CatalogIdentity,
544 failure_class: CompileFailureClass,
545 diagnostics: Vec<CompileDiagnostic>,
546 ) -> Self {
547 Self {
548 schema_version: COMPILE_SCHEMA_VERSION,
549 compiler,
550 catalog,
551 compile: CompileResult {
552 status: CompileStatus::Failure,
553 exit_code: 1,
554 failure_class: Some(failure_class),
555 diagnostics,
556 stdout: String::new(),
557 workshop_exact: String::new(),
558 workshop: String::new(),
559 },
560 }
561 }
562}
563
564fn compile_diagnostic(error: IntegrationError, files: &[hir::SourceFile]) -> CompileDiagnostic {
565 let diagnostic = error.diagnostic;
566 CompileDiagnostic {
567 severity: crate::tooling::DiagnosticSeverity::Error,
568 code: diagnostic.code,
569 message: diagnostic.message,
570 span: diagnostic
571 .span
572 .and_then(|span| source_location_from_hir(span, files)),
573 script: diagnostic.script.map(|script| *script),
574 }
575}
576
577fn source_location_from_file_records(
578 span: crate::diag::Span,
579 files: &[crate::preprocess::FileRecord],
580) -> Option<crate::tooling::SourceLocation> {
581 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
582 Some(crate::tooling::SourceLocation {
583 file_id: span.file,
584 path,
585 start: span.start,
586 end: span.end,
587 })
588}
589
590fn source_location_from_hir(
591 span: HirSpan,
592 files: &[hir::SourceFile],
593) -> Option<crate::tooling::SourceLocation> {
594 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
595 Some(crate::tooling::SourceLocation {
596 file_id: span.file,
597 path,
598 start: crate::diag::Position::new(span.start.line, span.start.col),
599 end: crate::diag::Position::new(span.end.line, span.end.col),
600 })
601}
602
603fn normalize_workshop(text: &str) -> String {
604 if text.is_empty() {
605 return String::new();
606 }
607 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
608 let mut lines = normalized
609 .split('\n')
610 .map(|line| line.trim_end_matches([' ', '\t']).to_owned())
611 .collect::<Vec<_>>();
612 while lines.last().is_some_and(|line| line.is_empty()) {
613 lines.pop();
614 }
615 if lines.is_empty() {
616 String::new()
617 } else {
618 lines.join("\n") + "\n"
619 }
620}
621
622fn reject_unlowered_directives(hir: &hir::Program) -> Result<(), IntegrationError> {
623 if let Some(replacement) = hir.preprocessing.replacements.first() {
624 let span = hir
625 .preprocessing
626 .directives
627 .iter()
628 .find(|directive| directive.name.starts_with("replace"))
629 .and_then(|directive| directive.span)
630 .or(replacement.span);
631 return Err(IntegrationError::new(
632 "backend-directive-unsupported",
633 format!(
634 "replacement directive '{}' has no canonical workshop-rs lowering",
635 replacement.value
636 ),
637 span,
638 ));
639 }
640 if let Some(replacement) = hir
641 .preprocessing
642 .directives
643 .iter()
644 .find(|directive| directive.name.starts_with("replace"))
645 {
646 return Err(IntegrationError::new(
647 "backend-directive-unsupported",
648 format!(
649 "replacement directive '{}' has no canonical workshop-rs lowering",
650 replacement.name
651 ),
652 replacement.span,
653 ));
654 }
655 Ok(())
656}
657
658type MacroBindings = HashMap<String, Expr>;
659
660struct MacroExpander {
661 macros: HashMap<String, (Vec<String>, Vec<Stmt>)>,
662 stack: Vec<String>,
663}
664
665fn expand_macros(program: &hir::Program) -> Result<hir::Program, IntegrationError> {
666 let macros = program
667 .declarations
668 .iter()
669 .filter_map(|declaration| match declaration {
670 hir::Declaration::Macro {
671 name, args, body, ..
672 } => Some((name.clone(), (args.clone(), body.clone()))),
673 _ => None,
674 })
675 .collect();
676 let mut expander = MacroExpander {
677 macros,
678 stack: Vec::new(),
679 };
680 let mut expanded = program.clone();
681 let bindings = MacroBindings::new();
682
683 for declaration in &mut expanded.declarations {
684 match declaration {
685 hir::Declaration::GlobalVariable { initializer, .. }
686 | hir::Declaration::PlayerVariable { initializer, .. } => {
687 if let Some(initializer) = initializer {
688 **initializer = expander.expand_expr(initializer, &bindings)?;
689 }
690 }
691 _ => {}
692 }
693 }
694 for entry in &mut expanded.rules {
695 match entry {
696 RuleEntry::Rule(rule) => {
697 for argument in &mut rule.event.args {
698 *argument = expander.expand_expr(argument, &bindings)?;
699 }
700 for condition in &mut rule.conditions {
701 *condition = expander.expand_expr(condition, &bindings)?;
702 }
703 rule.actions = expander.expand_stmts(&rule.actions, &bindings)?;
704 }
705 RuleEntry::SubroutineDef { body, .. } => {
706 *body = expander.expand_stmts(body, &bindings)?;
707 }
708 }
709 }
710 Ok(expanded)
711}
712
713impl MacroExpander {
714 fn expand_stmts(
715 &mut self,
716 statements: &[Stmt],
717 bindings: &MacroBindings,
718 ) -> Result<Vec<Stmt>, IntegrationError> {
719 let mut expanded = Vec::new();
720 for statement in statements {
721 if let Stmt::Expr { expr, .. } = statement {
722 if let Expr::MacroCall { name, args, span } = expr.as_ref() {
723 let args = args
724 .iter()
725 .map(|arg| self.expand_expr(arg, bindings))
726 .collect::<Result<Vec<_>, _>>()?;
727 expanded.extend(self.expand_macro_body(name, &args, *span)?);
728 continue;
729 }
730 }
731 expanded.push(self.expand_stmt(statement, bindings)?);
732 }
733 Ok(expanded)
734 }
735
736 fn expand_stmt(
737 &mut self,
738 statement: &Stmt,
739 bindings: &MacroBindings,
740 ) -> Result<Stmt, IntegrationError> {
741 Ok(match statement {
742 Stmt::Expr { expr, span } => Stmt::Expr {
743 expr: Box::new(self.expand_expr(expr, bindings)?),
744 span: *span,
745 },
746 Stmt::Assign {
747 target,
748 value,
749 span,
750 } => Stmt::Assign {
751 target: Box::new(self.expand_expr(target, bindings)?),
752 value: Box::new(self.expand_expr(value, bindings)?),
753 span: *span,
754 },
755 Stmt::If {
756 branches,
757 r#else,
758 span,
759 } => Stmt::If {
760 branches: branches
761 .iter()
762 .map(|branch| {
763 Ok(hir::types::IfBranch {
764 condition: Box::new(self.expand_expr(&branch.condition, bindings)?),
765 body: self.expand_stmts(&branch.body, bindings)?,
766 })
767 })
768 .collect::<Result<Vec<_>, IntegrationError>>()?,
769 r#else: r#else
770 .as_ref()
771 .map(|body| self.expand_stmts(body, bindings))
772 .transpose()?,
773 span: *span,
774 },
775 Stmt::For {
776 variable,
777 iterable,
778 body,
779 span,
780 } => Stmt::For {
781 variable: Box::new(self.expand_expr(variable, bindings)?),
782 iterable: Box::new(self.expand_expr(iterable, bindings)?),
783 body: self.expand_stmts(body, bindings)?,
784 span: *span,
785 },
786 Stmt::While {
787 condition,
788 body,
789 span,
790 } => Stmt::While {
791 condition: Box::new(self.expand_expr(condition, bindings)?),
792 body: self.expand_stmts(body, bindings)?,
793 span: *span,
794 },
795 Stmt::DoWhile {
796 condition,
797 body,
798 span,
799 } => Stmt::DoWhile {
800 condition: Box::new(self.expand_expr(condition, bindings)?),
801 body: self.expand_stmts(body, bindings)?,
802 span: *span,
803 },
804 Stmt::Switch { value, arms, span } => Stmt::Switch {
805 value: Box::new(self.expand_expr(value, bindings)?),
806 arms: arms
807 .iter()
808 .map(|arm| match arm {
809 SwitchArm::Case { value, body, span } => Ok(SwitchArm::Case {
810 value: Box::new(self.expand_expr(value, bindings)?),
811 body: self.expand_stmts(body, bindings)?,
812 span: *span,
813 }),
814 SwitchArm::Default { body, span } => Ok(SwitchArm::Default {
815 body: self.expand_stmts(body, bindings)?,
816 span: *span,
817 }),
818 })
819 .collect::<Result<Vec<_>, IntegrationError>>()?,
820 span: *span,
821 },
822 Stmt::Break { .. } | Stmt::CallSubroutine { .. } | Stmt::Pass { .. } => {
823 statement.clone()
824 }
825 })
826 }
827
828 fn expand_expr(
829 &mut self,
830 expression: &Expr,
831 bindings: &MacroBindings,
832 ) -> Result<Expr, IntegrationError> {
833 match expression {
834 Expr::MacroParam { name, span } => bindings.get(name).cloned().ok_or_else(|| {
835 IntegrationError::new(
836 "unsupported-integration-surface",
837 format!("macro parameter '{name}' has no expansion binding"),
838 *span,
839 )
840 }),
841 Expr::MacroCall { name, args, span } => {
842 let args = args
843 .iter()
844 .map(|arg| self.expand_expr(arg, bindings))
845 .collect::<Result<Vec<_>, _>>()?;
846 let body = self.expand_macro_body(name, &args, *span)?;
847 if body.len() != 1 {
848 return Err(IntegrationError::new(
849 "macro-invalid",
850 format!("macro '{name}' must produce one expression in value position"),
851 *span,
852 ));
853 }
854 match body.into_iter().next().expect("one macro body statement") {
855 Stmt::Expr { expr, .. } => Ok(*expr),
856 _ => Err(IntegrationError::new(
857 "macro-invalid",
858 format!("macro '{name}' must produce an expression in value position"),
859 *span,
860 )),
861 }
862 }
863 Expr::Array { elements, span } => Ok(Expr::Array {
864 elements: elements
865 .iter()
866 .map(|element| self.expand_expr(element, bindings))
867 .collect::<Result<Vec<_>, _>>()?,
868 span: *span,
869 }),
870 Expr::Dict { entries, span } => Ok(Expr::Dict {
871 entries: entries
872 .iter()
873 .map(|entry| {
874 Ok(hir::DictEntry {
875 key: Box::new(self.expand_expr(&entry.key, bindings)?),
876 value: Box::new(self.expand_expr(&entry.value, bindings)?),
877 span: entry.span,
878 })
879 })
880 .collect::<Result<Vec<_>, IntegrationError>>()?,
881 span: *span,
882 }),
883 Expr::Comprehension {
884 element,
885 variable,
886 variable_span,
887 index,
888 index_span,
889 iterable,
890 condition,
891 span,
892 } => Ok(Expr::Comprehension {
893 element: Box::new(self.expand_expr(element, bindings)?),
894 variable: variable.clone(),
895 variable_span: *variable_span,
896 index: index.clone(),
897 index_span: *index_span,
898 iterable: Box::new(self.expand_expr(iterable, bindings)?),
899 condition: condition
900 .as_ref()
901 .map(|condition| self.expand_expr(condition, bindings).map(Box::new))
902 .transpose()?,
903 span: *span,
904 }),
905 Expr::Lambda {
906 params,
907 param_spans,
908 body,
909 span,
910 } => Ok(Expr::Lambda {
911 params: params.clone(),
912 param_spans: param_spans.clone(),
913 body: Box::new(self.expand_expr(body, bindings)?),
914 span: *span,
915 }),
916 Expr::Type { name, args, span } => Ok(Expr::Type {
917 name: name.clone(),
918 args: args
919 .iter()
920 .map(|arg| self.expand_expr(arg, bindings))
921 .collect::<Result<Vec<_>, _>>()?,
922 span: *span,
923 }),
924 Expr::Vector { x, y, z, span } => Ok(Expr::Vector {
925 x: Box::new(self.expand_expr(x, bindings)?),
926 y: Box::new(self.expand_expr(y, bindings)?),
927 z: Box::new(self.expand_expr(z, bindings)?),
928 span: *span,
929 }),
930 Expr::PlayerVar {
931 player,
932 name,
933 member_span,
934 span,
935 } => Ok(Expr::PlayerVar {
936 player: Box::new(self.expand_expr(player, bindings)?),
937 name: name.clone(),
938 member_span: *member_span,
939 span: *span,
940 }),
941 Expr::Member {
942 receiver,
943 member,
944 member_span,
945 span,
946 } => Ok(Expr::Member {
947 receiver: Box::new(self.expand_expr(receiver, bindings)?),
948 member: member.clone(),
949 member_span: *member_span,
950 span: *span,
951 }),
952 Expr::Call { name, args, span } => Ok(Expr::Call {
953 name: name.clone(),
954 args: args
955 .iter()
956 .map(|arg| self.expand_expr(arg, bindings))
957 .collect::<Result<Vec<_>, _>>()?,
958 span: *span,
959 }),
960 Expr::ReceiverCall {
961 receiver,
962 name,
963 args,
964 span,
965 } => Ok(Expr::ReceiverCall {
966 receiver: Box::new(self.expand_expr(receiver, bindings)?),
967 name: name.clone(),
968 args: args
969 .iter()
970 .map(|arg| self.expand_expr(arg, bindings))
971 .collect::<Result<Vec<_>, _>>()?,
972 span: *span,
973 }),
974 Expr::Binary {
975 op,
976 left,
977 right,
978 span,
979 } => Ok(Expr::Binary {
980 op: op.clone(),
981 left: Box::new(self.expand_expr(left, bindings)?),
982 right: Box::new(self.expand_expr(right, bindings)?),
983 span: *span,
984 }),
985 Expr::Conditional {
986 then_value,
987 condition,
988 else_value,
989 span,
990 } => Ok(Expr::Conditional {
991 then_value: Box::new(self.expand_expr(then_value, bindings)?),
992 condition: Box::new(self.expand_expr(condition, bindings)?),
993 else_value: Box::new(self.expand_expr(else_value, bindings)?),
994 span: *span,
995 }),
996 Expr::Unary { op, operand, span } => Ok(Expr::Unary {
997 op: op.clone(),
998 operand: Box::new(self.expand_expr(operand, bindings)?),
999 span: *span,
1000 }),
1001 Expr::Index { array, index, span } => Ok(Expr::Index {
1002 array: Box::new(self.expand_expr(array, bindings)?),
1003 index: Box::new(self.expand_expr(index, bindings)?),
1004 span: *span,
1005 }),
1006 Expr::Format { text, args, span } => Ok(Expr::Format {
1007 text: text.clone(),
1008 args: args
1009 .iter()
1010 .map(|arg| self.expand_expr(arg, bindings))
1011 .collect::<Result<Vec<_>, _>>()?,
1012 span: *span,
1013 }),
1014 _ => Ok(expression.clone()),
1015 }
1016 }
1017
1018 fn expand_macro_body(
1019 &mut self,
1020 name: &str,
1021 args: &[Expr],
1022 span: Option<HirSpan>,
1023 ) -> Result<Vec<Stmt>, IntegrationError> {
1024 let Some((params, body)) = self.macros.get(name).cloned() else {
1025 return Err(IntegrationError::new(
1026 "unsupported-integration-surface",
1027 format!("macro '{name}' has no declaration"),
1028 span,
1029 ));
1030 };
1031 if params.len() != args.len() {
1032 return Err(IntegrationError::new(
1033 "macro-arity",
1034 format!(
1035 "macro '{name}' expects {} argument(s) but got {}",
1036 params.len(),
1037 args.len()
1038 ),
1039 span,
1040 ));
1041 }
1042 if self.stack.iter().any(|active| active == name) {
1043 return Err(IntegrationError::new(
1044 "macro-recursion",
1045 format!("recursive macro expansion detected for '{name}'"),
1046 span,
1047 ));
1048 }
1049 let mut bindings = MacroBindings::new();
1050 for (param, arg) in params.into_iter().zip(args.iter()) {
1051 bindings.insert(param, arg.clone());
1052 }
1053 self.stack.push(name.to_string());
1054 let result = self.expand_stmts(&body, &bindings);
1055 self.stack.pop();
1056 result
1057 }
1058}
1059
1060#[derive(Debug, Clone, PartialEq, Eq)]
1065pub struct CompileOutput {
1066 pub workshop: String,
1068 pub emitted_workshop: String,
1070 pub hook_console_output: Vec<String>,
1072}
1073
1074pub struct CompilationArtifact {
1077 pub wir: Program,
1078 pub emitted: String,
1079 pub catalog_identity: CatalogIdentity,
1080 pub final_output: String,
1081 pub hook_console_output: Vec<String>,
1082}
1083
1084impl CompilationArtifact {
1085 fn into_output(self) -> CompileOutput {
1086 CompileOutput {
1087 workshop: self.final_output,
1088 emitted_workshop: self.emitted,
1089 hook_console_output: self.hook_console_output,
1090 }
1091 }
1092}
1093
1094fn convert_settings(settings: crate::hir::Settings) -> workshop_rs::settings::Settings {
1095 workshop_rs::settings::Settings {
1096 span: settings.span.map(convert_settings_span),
1097 children: settings
1098 .children
1099 .into_iter()
1100 .map(convert_settings_node)
1101 .collect(),
1102 }
1103}
1104
1105fn convert_settings_node(node: crate::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
1106 use crate::hir::SettingsNode as SourceNode;
1107 use workshop_rs::settings::{SettingsListElement, SettingsNode as TargetNode};
1108
1109 match node {
1110 SourceNode::Group {
1111 name,
1112 children,
1113 span,
1114 } => TargetNode::Group {
1115 name,
1116 children: children.into_iter().map(convert_settings_node).collect(),
1117 span: span.map(convert_settings_span),
1118 },
1119 SourceNode::Number { name, value, span } => TargetNode::Number {
1120 name,
1121 value,
1122 span: span.map(convert_settings_span),
1123 },
1124 SourceNode::Bool { name, value, span } => TargetNode::Bool {
1125 name,
1126 value,
1127 span: span.map(convert_settings_span),
1128 },
1129 SourceNode::String { name, value, span } => TargetNode::String {
1130 name,
1131 value,
1132 span: span.map(convert_settings_span),
1133 },
1134 SourceNode::List {
1135 name,
1136 elements,
1137 span,
1138 } => TargetNode::List {
1139 name,
1140 elements: elements
1141 .into_iter()
1142 .map(|element| SettingsListElement {
1143 value: element.value,
1144 span: element.span.map(convert_settings_span),
1145 })
1146 .collect(),
1147 span: span.map(convert_settings_span),
1148 },
1149 }
1150}
1151
1152fn convert_settings_span(span: HirSpan) -> WorkshopSpan {
1153 WorkshopSpan::new(
1154 workshop_rs::source::FileId::from_index(span.file as usize),
1155 WorkshopPosition::new(span.start.line, span.start.col),
1156 WorkshopPosition::new(span.end.line, span.end.col),
1157 )
1158}
1159
1160struct Lowering<'a> {
1161 compiler: &'a Compiler,
1162 hir: &'a hir::Program,
1163 wir: Program,
1164 files: HashMap<u32, workshop_rs::source::FileId>,
1165 wir_to_hir_files: Vec<u32>,
1166 globals: HashMap<String, wir::GlobalVarId>,
1167 players: HashMap<String, wir::PlayerVarId>,
1168 subroutines: HashMap<String, wir::SubroutineId>,
1169 constants: HashMap<String, &'a Expr>,
1170 defined_subroutines: HashSet<wir::SubroutineId>,
1171 array_bindings: Vec<ArrayBinding>,
1172}
1173
1174#[derive(Debug, Clone)]
1175struct ArrayBinding {
1176 element: String,
1177 index: Option<String>,
1178}
1179
1180#[derive(Debug, Clone, Copy)]
1181enum BreakTarget {
1182 Loop,
1183 DoWhile,
1184 Switch,
1185}
1186
1187type SwitchBreak = (usize, HirSpan);
1188type LoweredSwitchBody = (Vec<wir::ActionId>, Option<SwitchBreak>);
1189
1190impl<'a> Lowering<'a> {
1191 fn new(compiler: &'a Compiler, hir: &'a hir::Program) -> Result<Self, IntegrationError> {
1192 Ok(Self {
1193 compiler,
1194 hir,
1195 wir: Program::default(),
1196 files: HashMap::new(),
1197 wir_to_hir_files: Vec::new(),
1198 globals: HashMap::new(),
1199 players: HashMap::new(),
1200 subroutines: HashMap::new(),
1201 constants: HashMap::new(),
1202 defined_subroutines: HashSet::new(),
1203 array_bindings: Vec::new(),
1204 })
1205 }
1206
1207 fn copy_files(&mut self) -> Result<(), IntegrationError> {
1208 self.wir.settings = self.hir.settings.clone().map(convert_settings);
1209 for file in &self.hir.files {
1210 if self.files.contains_key(&file.id) {
1211 return Err(IntegrationError::new(
1212 "source-file",
1213 format!("duplicate HIR source file id {}", file.id),
1214 None,
1215 ));
1216 }
1217 let id = self.wir.files.push(SourceFile::new(file.path.clone()));
1218 self.files.insert(file.id, id);
1219 self.wir_to_hir_files.push(file.id);
1220 }
1221 Ok(())
1222 }
1223
1224 fn lower_declarations(&mut self) -> Result<(), IntegrationError> {
1225 let (implicit_globals, implicit_players) = implicit_default_variables(self.hir);
1226 for declaration in &self.hir.declarations {
1227 if let hir::Declaration::GlobalVariable {
1228 name,
1229 index: Some(index),
1230 span,
1231 ..
1232 } = declaration
1233 {
1234 for (implicit_name, implicit_span) in &implicit_globals {
1235 if default_var_index(implicit_name) == Some(*index) {
1236 return Err(IntegrationError::new(
1237 "index-collision",
1238 format!(
1239 "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'"
1240 ),
1241 implicit_span.or(*span),
1242 ));
1243 }
1244 }
1245 }
1246 if let hir::Declaration::PlayerVariable {
1247 name,
1248 index: Some(index),
1249 span,
1250 ..
1251 } = declaration
1252 {
1253 for (implicit_name, implicit_span) in &implicit_players {
1254 if default_var_index(implicit_name) == Some(*index) {
1255 return Err(IntegrationError::new(
1256 "index-collision",
1257 format!(
1258 "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'"
1259 ),
1260 implicit_span.or(*span),
1261 ));
1262 }
1263 }
1264 }
1265 }
1266
1267 let globals = self
1268 .hir
1269 .declarations
1270 .iter()
1271 .filter_map(|declaration| match declaration {
1272 hir::Declaration::GlobalVariable { index, span, .. } => Some((*index, *span)),
1273 _ => None,
1274 })
1275 .collect::<Vec<_>>();
1276 let players = self
1277 .hir
1278 .declarations
1279 .iter()
1280 .filter_map(|declaration| match declaration {
1281 hir::Declaration::PlayerVariable { index, span, .. } => Some((*index, *span)),
1282 _ => None,
1283 })
1284 .collect::<Vec<_>>();
1285 let subroutines = self
1286 .hir
1287 .declarations
1288 .iter()
1289 .filter_map(|declaration| match declaration {
1290 hir::Declaration::Subroutine { index, span, .. } => Some((*index, *span)),
1291 _ => None,
1292 })
1293 .collect::<Vec<_>>();
1294 let implicit_reserved = implicit_globals
1295 .keys()
1296 .map(|name| default_var_index(name).expect("implicit default variable names resolve"))
1297 .collect::<HashSet<_>>();
1298 let implicit_player_reserved = implicit_players
1299 .keys()
1300 .map(|name| default_var_index(name).expect("implicit default player names resolve"))
1301 .collect::<HashSet<_>>();
1302 let empty = HashSet::new();
1303 let global_indices = allocate_indices(&globals, &implicit_reserved, "global variable")?;
1304 let player_indices =
1305 allocate_indices(&players, &implicit_player_reserved, "player variable")?;
1306 let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?;
1307 let mut global_index = 0;
1308 let mut player_index = 0;
1309 let mut subroutine_index = 0;
1310
1311 let mut declared_globals: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> = Vec::new();
1316 let mut global_initializers = Vec::new();
1317 let mut declared_players: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> = Vec::new();
1318 let mut player_initializers = Vec::new();
1319 let mut declared_subroutines: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> =
1320 Vec::new();
1321
1322 for declaration in &self.hir.declarations {
1323 match declaration {
1324 hir::Declaration::GlobalVariable {
1325 name,
1326 index: _,
1327 span,
1328 name_span,
1329 initializer,
1330 } => {
1331 let assigned = global_indices[global_index];
1332 global_index += 1;
1333 if declared_globals
1334 .iter()
1335 .any(|(existing, ..)| *existing == name)
1336 {
1337 return Err(IntegrationError::new(
1338 "symbol-collision",
1339 format!("duplicate global variable '{name}'"),
1340 *span,
1341 ));
1342 }
1343 declared_globals.push((name, assigned, *span, *name_span));
1344 if let Some(init) = initializer {
1345 if !is_zero_initializer(init) {
1346 global_initializers.push((name, init, *span, *name_span));
1347 }
1348 }
1349 }
1350 hir::Declaration::PlayerVariable {
1351 name,
1352 index: _,
1353 span,
1354 name_span,
1355 initializer,
1356 } => {
1357 let assigned = player_indices[player_index];
1358 player_index += 1;
1359 if declared_players
1360 .iter()
1361 .any(|(existing, ..)| *existing == name)
1362 {
1363 return Err(IntegrationError::new(
1364 "symbol-collision",
1365 format!("duplicate player variable '{name}'"),
1366 *span,
1367 ));
1368 }
1369 declared_players.push((name, assigned, *span, *name_span));
1370 if let Some(init) = initializer {
1371 if !is_zero_initializer(init) {
1372 player_initializers.push((name, init, *span, *name_span));
1373 }
1374 }
1375 }
1376 hir::Declaration::Subroutine {
1377 name,
1378 span,
1379 name_span,
1380 ..
1381 } => {
1382 let assigned = subroutine_indices[subroutine_index];
1383 subroutine_index += 1;
1384 if declared_subroutines
1385 .iter()
1386 .any(|(existing, ..)| *existing == name)
1387 {
1388 return Err(IntegrationError::new(
1389 "symbol-collision",
1390 format!("duplicate subroutine '{name}'"),
1391 *span,
1392 ));
1393 }
1394 declared_subroutines.push((name, assigned, *span, *name_span));
1395 }
1396 hir::Declaration::Constant { name, value, span } => {
1397 if self.constants.insert(name.clone(), value).is_some() {
1398 return Err(IntegrationError::new(
1399 "symbol-collision",
1400 format!("duplicate constant '{name}'"),
1401 *span,
1402 ));
1403 }
1404 }
1405 hir::Declaration::Macro { .. } => {
1406 }
1409 }
1410 }
1411
1412 let mut planned_globals: Vec<(String, u32, Option<HirSpan>, Option<HirSpan>)> =
1413 declared_globals
1414 .into_iter()
1415 .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span))
1416 .collect();
1417 planned_globals.extend(implicit_globals.iter().map(|(name, span)| {
1418 (
1419 name.clone(),
1420 default_var_index(name).expect("implicit default variable names resolve"),
1421 *span,
1422 None,
1423 )
1424 }));
1425 planned_globals.sort_by_key(|(_, index, ..)| *index);
1426 for (name, assigned, span, name_span) in planned_globals {
1427 let id = self.wir.global_variables.push(wir::WorkshopVariable {
1428 name: name.clone(),
1429 index: assigned,
1430 span: self.wir_span(span)?,
1431 name_span: self.wir_span(name_span)?,
1432 });
1433 self.globals.insert(name.clone(), id);
1434 }
1435
1436 let mut planned_players: Vec<(String, u32, Option<HirSpan>, Option<HirSpan>)> =
1437 declared_players
1438 .into_iter()
1439 .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span))
1440 .collect();
1441 planned_players.extend(implicit_players.iter().map(|(name, span)| {
1442 (
1443 name.clone(),
1444 default_var_index(name).expect("implicit default player names resolve"),
1445 *span,
1446 None,
1447 )
1448 }));
1449 planned_players.sort_by_key(|(_, index, ..)| *index);
1450 for (name, assigned, span, name_span) in planned_players {
1451 let id = self.wir.player_variables.push(wir::WorkshopVariable {
1452 name: name.clone(),
1453 index: assigned,
1454 span: self.wir_span(span)?,
1455 name_span: self.wir_span(name_span)?,
1456 });
1457 self.players.insert(name, id);
1458 }
1459
1460 declared_subroutines.sort_by_key(|(_, index, ..)| *index);
1461 for (name, assigned, span, name_span) in declared_subroutines {
1462 let id = self.wir.subroutines.push(wir::WorkshopSubroutine {
1463 name: name.to_string(),
1464 index: assigned,
1465 span: self.wir_span(span)?,
1466 name_span: self.wir_span(name_span)?,
1467 });
1468 self.subroutines.insert(name.to_string(), id);
1469 }
1470
1471 if !global_initializers.is_empty() {
1472 let mut actions = Vec::with_capacity(global_initializers.len());
1473 for (name, init_expr, span, target_span) in global_initializers {
1474 let variable = *self.globals.get(name).expect("declared global is created");
1475 let value = self.lower_value(init_expr)?;
1476 actions.push(self.wir.actions.push(Action::SetGlobalVariable {
1477 variable,
1478 value,
1479 span: self.wir_span(span)?,
1480 target_span: self.wir_span(target_span)?,
1481 }));
1482 }
1483 self.wir.rules.push(wir::Rule {
1484 name: "Initialize global variables".to_string(),
1485 span: None,
1486 name_span: None,
1487 disabled: false,
1488 event: Event::Global,
1489 conditions: Vec::new(),
1490 actions,
1491 });
1492 }
1493
1494 if !player_initializers.is_empty() {
1495 let mut actions = Vec::with_capacity(player_initializers.len());
1496 for (name, init_expr, span, target_span) in player_initializers {
1497 let variable = *self
1498 .players
1499 .get(name)
1500 .expect("declared player variable is created");
1501 let player = self
1502 .wir
1503 .values
1504 .push(ValueNode::new(Value::EventPlayer, None));
1505 let value = self.lower_value(init_expr)?;
1506 actions.push(self.wir.actions.push(Action::SetPlayerVariable {
1507 player,
1508 variable,
1509 value,
1510 span: self.wir_span(span)?,
1511 target_span: self.wir_span(target_span)?,
1512 }));
1513 }
1514 self.wir.rules.push(wir::Rule {
1515 name: "Initialize player variables".to_string(),
1516 span: None,
1517 name_span: None,
1518 disabled: false,
1519 event: Event::EachPlayer,
1520 conditions: Vec::new(),
1521 actions,
1522 });
1523 }
1524
1525 Ok(())
1526 }
1527
1528 fn lower_rules(&mut self) -> Result<(), IntegrationError> {
1529 for entry in &self.hir.rules {
1530 match entry {
1531 RuleEntry::Rule(rule) => self.lower_rule(rule)?,
1532 RuleEntry::SubroutineDef {
1533 name,
1534 source_name,
1535 span,
1536 name_span,
1537 body,
1538 annotations,
1539 ..
1540 } => {
1541 self.lower_subroutine(name, source_name, *span, *name_span, body, annotations)?
1542 }
1543 }
1544 }
1545 Ok(())
1546 }
1547
1548 fn lower_rule(&mut self, rule: &hir::Rule) -> Result<(), IntegrationError> {
1549 self.reject_rule_metadata(rule)?;
1550 let event = self.lower_event(&rule.event, &rule.annotations)?;
1551 let conditions = rule
1552 .conditions
1553 .iter()
1554 .map(|expr| self.lower_condition(expr))
1555 .collect::<Result<Vec<_>, _>>()?;
1556 let mut actions = Vec::new();
1557 actions.extend(self.lower_actions(&rule.actions, None)?);
1558 self.wir.rules.push(wir::Rule {
1559 name: rule.name.clone(),
1560 span: self.wir_span(rule.span)?,
1561 name_span: self.wir_span(rule.name_span)?,
1562 disabled: rule.disabled,
1563 event,
1564 conditions,
1565 actions,
1566 });
1567 Ok(())
1568 }
1569
1570 fn lower_subroutine(
1571 &mut self,
1572 name: &str,
1573 source_name: &str,
1574 span: Option<HirSpan>,
1575 name_span: Option<HirSpan>,
1576 body: &[Stmt],
1577 annotations: &[hir::Annotation],
1578 ) -> Result<(), IntegrationError> {
1579 self.reject_subroutine_metadata(annotations)?;
1580 let source_name = if source_name.is_empty() {
1581 name
1582 } else {
1583 source_name
1584 };
1585 let subroutine = *self.subroutines.get(source_name).ok_or_else(|| {
1586 self.unsupported(
1587 format!("subroutine definition '{source_name}' has no declaration"),
1588 name_span.or(span),
1589 )
1590 })?;
1591 if !self.defined_subroutines.insert(subroutine) {
1592 return Err(self.unsupported(
1593 format!("subroutine '{source_name}' has multiple definitions"),
1594 name_span.or(span),
1595 ));
1596 }
1597 let mut actions = Vec::new();
1598 actions.extend(self.lower_actions(body, None)?);
1599 self.wir.rules.push(wir::Rule {
1600 name: self.subroutine_rule_name(name),
1601 span: self.wir_span(span)?,
1602 name_span: self.wir_span(name_span)?,
1603 disabled: false,
1604 event: Event::Subroutine(subroutine),
1605 conditions: Vec::new(),
1606 actions,
1607 });
1608 Ok(())
1609 }
1610
1611 fn reject_rule_metadata(&self, rule: &hir::Rule) -> Result<(), IntegrationError> {
1612 if rule.delimiter {
1613 let span = rule
1614 .annotations
1615 .iter()
1616 .find(|annotation| annotation.name == "Delimiter")
1617 .and_then(|annotation| annotation.span)
1618 .or(rule.span);
1619 return Err(self.unsupported(
1620 "rule delimiter metadata is not representable in canonical WIR",
1621 span,
1622 ));
1623 }
1624 if rule.new_page.is_some() {
1625 let span = rule
1626 .annotations
1627 .iter()
1628 .find(|annotation| annotation.name == "NewPage")
1629 .and_then(|annotation| annotation.span)
1630 .or(rule.span);
1631 return Err(self.unsupported(
1632 "rule new-page metadata is not representable in canonical WIR",
1633 span,
1634 ));
1635 }
1636 for annotation in &rule.annotations {
1637 match annotation.name.as_str() {
1638 "Event" | "Condition" | "Team" | "Slot" | "Hero" | "Disabled"
1639 | "SuppressWarnings" => {}
1640 _ => {
1641 return Err(self.unsupported(
1642 format!(
1643 "rule annotation '{}' is not representable in canonical WIR",
1644 annotation.name
1645 ),
1646 annotation.span.or(rule.span),
1647 ));
1648 }
1649 }
1650 }
1651 Ok(())
1652 }
1653
1654 fn reject_subroutine_metadata(
1655 &self,
1656 annotations: &[hir::Annotation],
1657 ) -> Result<(), IntegrationError> {
1658 for annotation in annotations {
1659 match annotation.name.as_str() {
1660 "Name" | "SuppressWarnings" => {}
1661 _ => {
1662 return Err(self.unsupported(
1663 format!(
1664 "subroutine annotation '{}' is not representable in canonical WIR",
1665 annotation.name
1666 ),
1667 annotation.span,
1668 ));
1669 }
1670 }
1671 }
1672 Ok(())
1673 }
1674
1675 fn subroutine_rule_name(&self, generated_name: &str) -> String {
1676 if self.hir.preprocessing.rule_prefix_template.is_some() {
1677 generated_name.to_string()
1678 } else {
1679 format!("Subroutine {generated_name}")
1680 }
1681 }
1682
1683 fn lower_event(
1684 &self,
1685 event: &hir::Event,
1686 annotations: &[hir::Annotation],
1687 ) -> Result<Event, IntegrationError> {
1688 if !event.args.is_empty() {
1689 return Err(self.unsupported(
1690 "event arguments are not representable in canonical WIR; use structural event filters",
1691 event.span,
1692 ));
1693 }
1694 let team = self.lower_event_team(annotations)?;
1695 let target = self.lower_event_target(annotations)?;
1696 let has_filters =
1697 !matches!(team, wir::EventTeam::All) || !matches!(target, wir::EventTarget::All);
1698 match event.name.as_str() {
1699 "global" => {
1700 if has_filters {
1701 return Err(
1702 self.unsupported("global events cannot have player filters", event.span)
1703 );
1704 }
1705 Ok(Event::Global)
1706 }
1707 "eachPlayer" => {
1708 if has_filters {
1709 Ok(Event::EachPlayerWithFilters { team, target })
1710 } else {
1711 Ok(Event::EachPlayer)
1712 }
1713 }
1714 name => player_event_kind(name).map_or_else(
1715 || {
1716 Err(self.unsupported(
1717 format!("event '{name}' is not supported by canonical WIR"),
1718 event.span,
1719 ))
1720 },
1721 |kind| Ok(Event::Player { kind, team, target }),
1722 ),
1723 }
1724 }
1725
1726 fn lower_event_team(
1727 &self,
1728 annotations: &[hir::Annotation],
1729 ) -> Result<wir::EventTeam, IntegrationError> {
1730 let team_annotations = annotations
1731 .iter()
1732 .filter(|annotation| annotation.name == "Team")
1733 .collect::<Vec<_>>();
1734 if team_annotations.len() > 1 {
1735 return Err(self.unsupported(
1736 "an event cannot have multiple @Team filters",
1737 team_annotations[1].span.or(team_annotations[0].span),
1738 ));
1739 }
1740 let Some(annotation) = team_annotations.first() else {
1741 return Ok(wir::EventTeam::All);
1742 };
1743 let argument = annotation
1744 .args
1745 .first()
1746 .ok_or_else(|| self.unsupported("@Team requires one filter value", annotation.span))?;
1747 if annotation.args.len() != 1 {
1748 return Err(
1749 self.unsupported("@Team requires exactly one filter value", annotation.span)
1750 );
1751 }
1752 let spelling = match argument.text.as_str() {
1753 "1" => "Team 1",
1754 "2" => "Team 2",
1755 value => value,
1756 };
1757 let (_, member) = self
1758 .compiler
1759 .catalog
1760 .resolve_enum_member("EventTeam", &Locale::new("en-US"), spelling)
1761 .ok_or_else(|| {
1762 self.unsupported(
1763 format!("unknown EventTeam filter '{spelling}'"),
1764 argument.span.or(annotation.span),
1765 )
1766 })?;
1767 match member.as_str() {
1768 "ALL" => Ok(wir::EventTeam::All),
1769 "TEAM_1" => Ok(wir::EventTeam::Team1),
1770 "TEAM_2" => Ok(wir::EventTeam::Team2),
1771 _ => Err(self.unsupported(
1772 format!("catalog EventTeam member '{member}' is not supported by canonical WIR"),
1773 argument.span.or(annotation.span),
1774 )),
1775 }
1776 }
1777
1778 fn lower_event_target(
1779 &self,
1780 annotations: &[hir::Annotation],
1781 ) -> Result<wir::EventTarget, IntegrationError> {
1782 let mut filters = Vec::new();
1783 for name in ["Slot", "Hero"] {
1784 let matches = annotations
1785 .iter()
1786 .filter(|annotation| annotation.name == name)
1787 .collect::<Vec<_>>();
1788 if matches.len() > 1 {
1789 return Err(self.unsupported(
1790 format!("an event cannot have multiple @{name} filters"),
1791 matches[1].span.or(matches[0].span),
1792 ));
1793 }
1794 filters.extend(matches);
1795 }
1796 if filters.len() > 1 {
1797 return Err(self.unsupported(
1798 "an event cannot combine @Slot and @Hero filters",
1799 filters[1].span.or(filters[0].span),
1800 ));
1801 }
1802 let Some(annotation) = filters.first() else {
1803 return Ok(wir::EventTarget::All);
1804 };
1805 let argument = annotation.args.first().ok_or_else(|| {
1806 self.unsupported(
1807 format!("@{} requires one filter value", annotation.name),
1808 annotation.span,
1809 )
1810 })?;
1811 if annotation.args.len() != 1 {
1812 return Err(self.unsupported(
1813 format!("@{} requires exactly one filter value", annotation.name),
1814 annotation.span,
1815 ));
1816 }
1817 let spelling = if annotation.name == "Slot" {
1818 match argument.text.as_str() {
1819 value if value.parse::<u8>().is_ok() => {
1820 format!("Slot {}", value.parse::<u8>().unwrap_or_default())
1821 }
1822 value => value.to_string(),
1823 }
1824 } else {
1825 argument.text.clone()
1826 };
1827 let domain = if annotation.name == "Slot" {
1828 "EventPlayer"
1829 } else {
1830 "Hero"
1831 };
1832 let (_, member) = self
1833 .compiler
1834 .catalog
1835 .resolve_enum_member(domain, &Locale::new("en-US"), &spelling)
1836 .ok_or_else(|| {
1837 self.unsupported(
1838 format!("unknown {domain} filter '{spelling}'"),
1839 argument.span.or(annotation.span),
1840 )
1841 })?;
1842 if domain == "EventPlayer" {
1843 if member == "ALL" {
1844 Ok(wir::EventTarget::All)
1845 } else if let Some(slot) = member.strip_prefix("SLOT_") {
1846 let slot = slot.parse::<u8>().map_err(|_| {
1847 self.unsupported(
1848 format!("catalog EventPlayer member '{member}' is not a slot"),
1849 argument.span.or(annotation.span),
1850 )
1851 })?;
1852 Ok(wir::EventTarget::Slot(slot))
1853 } else {
1854 Err(self.unsupported(
1855 format!(
1856 "catalog EventPlayer member '{member}' is not supported by canonical WIR"
1857 ),
1858 argument.span.or(annotation.span),
1859 ))
1860 }
1861 } else {
1862 Ok(wir::EventTarget::Hero(member))
1863 }
1864 }
1865
1866 fn lower_actions(
1867 &mut self,
1868 statements: &[Stmt],
1869 break_target: Option<BreakTarget>,
1870 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
1871 let mut actions = Vec::new();
1872 for statement in statements {
1873 actions.extend(self.lower_action(statement, break_target)?);
1874 }
1875 Ok(actions)
1876 }
1877
1878 fn lower_action(
1879 &mut self,
1880 stmt: &Stmt,
1881 break_target: Option<BreakTarget>,
1882 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
1883 match stmt {
1884 Stmt::Pass { .. } => Ok(Vec::new()),
1885 Stmt::Assign {
1886 target,
1887 value,
1888 span,
1889 } => self.lower_assign(target, value, *span).map(|action| vec![action]),
1890 Stmt::If {
1891 branches,
1892 r#else,
1893 span,
1894 } => {
1895 let branches = branches
1896 .iter()
1897 .map(|branch| {
1898 Ok(wir::IfBranch {
1899 condition: self.lower_value(&branch.condition)?,
1900 body: self.lower_actions(&branch.body, break_target)?,
1901 })
1902 })
1903 .collect::<Result<Vec<_>, IntegrationError>>()?;
1904 let else_body = r#else
1905 .as_ref()
1906 .map(|body| self.lower_actions(body, break_target))
1907 .transpose()?;
1908 Ok(vec![self.wir.actions.push(Action::If {
1909 branches,
1910 else_body,
1911 span: self.wir_span(*span)?,
1912 })])
1913 }
1914 Stmt::For {
1915 variable,
1916 iterable,
1917 body,
1918 span,
1919 } => {
1920 let (start, stop, step) = self.lower_range(iterable)?;
1921 let body = self.lower_actions(body, Some(BreakTarget::Loop))?;
1922 match variable.as_ref() {
1923 Expr::GlobalVar {
1924 name,
1925 span: target_span,
1926 } => {
1927 let variable_id = *self.globals.get(name).ok_or_else(|| {
1928 self.unsupported(
1929 format!("unknown global variable '{name}'"),
1930 *target_span,
1931 )
1932 })?;
1933 Ok(vec![self.wir.actions.push(Action::ForGlobalVariable {
1934 variable: variable_id,
1935 start,
1936 stop,
1937 step,
1938 body,
1939 span: self.wir_span(*span)?,
1940 target_span: self.wir_span(*target_span)?,
1941 })])
1942 }
1943 Expr::PlayerVar {
1944 player,
1945 name,
1946 span: target_span,
1947 ..
1948 } => {
1949 let variable_id = *self.players.get(name).ok_or_else(|| {
1950 self.unsupported(
1951 format!("unknown player variable '{name}'"),
1952 *target_span,
1953 )
1954 })?;
1955 let player = self.lower_value(player)?;
1956 Ok(vec![self.wir.actions.push(Action::ForPlayerVariable {
1957 player,
1958 variable: variable_id,
1959 start,
1960 stop,
1961 step,
1962 body,
1963 span: self.wir_span(*span)?,
1964 })])
1965 }
1966 _ => Err(self.unsupported(
1967 "range loops require a global- or player-variable binder in canonical WIR",
1968 variable.span().copied(),
1969 )),
1970 }
1971 }
1972 Stmt::While {
1973 condition,
1974 body,
1975 span,
1976 } => {
1977 let condition = self.lower_value(condition)?;
1978 let body = self.lower_actions(body, Some(BreakTarget::Loop))?;
1979 Ok(vec![self.wir.actions.push(Action::While {
1980 condition,
1981 body,
1982 span: self.wir_span(*span)?,
1983 })])
1984 }
1985 Stmt::DoWhile {
1986 condition,
1987 body,
1988 span,
1989 } => {
1990 let body = self.lower_do_while_body(body)?;
1991 let condition = self.lower_value(condition)?;
1992 let loop_if = self.wir.actions.push(Action::Call {
1993 name: "loopIf".to_string(),
1994 args: vec![condition],
1995 span: self.wir_span(*span)?,
1996 });
1997 let mut actions = body;
2000 actions.push(loop_if);
2001 Ok(actions)
2002 }
2003 Stmt::Switch {
2004 value,
2005 arms,
2006 span,
2007 } => self.lower_switch(value, arms, *span).map(|action| vec![action]),
2008 Stmt::Break { span } => match break_target {
2009 Some(BreakTarget::Loop) => Ok(vec![self.wir.actions.push(Action::Call {
2010 name: "break".to_string(),
2011 args: Vec::new(),
2012 span: self.wir_span(*span)?,
2013 })]),
2014 Some(BreakTarget::DoWhile) => Err(self.unsupported(
2015 "break inside a do-while must be a direct statement or a single conditional break",
2016 *span,
2017 )),
2018 Some(BreakTarget::Switch) => Err(self.unsupported(
2019 "break inside a nested conditional cannot be normalized into canonical switch control flow",
2020 *span,
2021 )),
2022 None => Err(self.unsupported(
2023 "break has no enclosing canonical loop or switch",
2024 *span,
2025 )),
2026 },
2027 Stmt::Expr { expr, span } => match expr.as_ref() {
2028 Expr::Call { name, args, .. } => {
2029 if name == "disableInspector" && args.is_empty() {
2030 Ok(vec![self.wir.actions.push(Action::Call {
2031 name: "disableInspector".to_string(),
2032 args: Vec::new(),
2033 span: self.wir_span(*span)?,
2034 })])
2035 } else if name == "debug" && args.len() == 1 {
2036 Ok(vec![self.lower_debug(&args[0], *span)?])
2037 } else if name == "print" && args.len() == 1 {
2038 Ok(vec![self.lower_print(&args[0], *span)?])
2039 } else {
2040 self.lower_action_call(name, args, *span).map(|action| vec![action])
2041 }
2042 }
2043 Expr::ReceiverCall {
2044 receiver,
2045 name,
2046 args,
2047 span: call_span,
2048 } => self
2049 .lower_receiver_action_call(receiver, name, args, *call_span)
2050 .map(|action| vec![action]),
2051 _ => Err(self.unsupported(
2052 "only action calls are currently representable as expression statements in canonical WIR",
2053 *span,
2054 )),
2055 },
2056 Stmt::CallSubroutine { name, span } => {
2057 let subroutine = *self.subroutines.get(name).ok_or_else(|| {
2058 self.unsupported(format!("unknown subroutine '{name}'"), *span)
2059 })?;
2060 let span = self.wir_span(*span)?;
2061 Ok(vec![self.wir.actions.push(Action::CallSubroutine {
2062 subroutine,
2063 span,
2064 callee_span: span,
2065 })])
2066 }
2067 }
2068 }
2069
2070 fn lower_do_while_body(
2071 &mut self,
2072 statements: &[Stmt],
2073 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2074 let mut actions = Vec::new();
2075 for (index, statement) in statements.iter().enumerate() {
2076 let direct_break = matches!(statement, Stmt::Break { .. });
2077 let conditional_break = match statement {
2078 Stmt::If {
2079 branches,
2080 r#else: None,
2081 ..
2082 } if branches.len() == 1 => {
2083 matches!(branches[0].body.as_slice(), [Stmt::Break { .. }])
2084 }
2085 _ => false,
2086 };
2087
2088 if direct_break || conditional_break {
2089 let tail = self.lower_do_while_body(&statements[index + 1..])?;
2090 let distance = self.canonical_action_width(&tail, statement.span().copied())? + 1;
2091 let (name, args, span) = if let Stmt::Break { span } = statement {
2092 ("skip", Vec::new(), *span)
2093 } else if let Stmt::If { branches, span, .. } = statement {
2094 (
2095 "skipIf",
2096 vec![self.lower_value(&branches[0].condition)?],
2097 *span,
2098 )
2099 } else {
2100 unreachable!("break shape was checked above")
2101 };
2102 let distance = self.wir.values.push(ValueNode::new(
2103 Value::Number {
2104 value: distance as f64,
2105 text: distance.to_string(),
2106 },
2107 self.wir_span(span)?,
2108 ));
2109 let mut args = args;
2110 args.push(distance);
2111 actions.push(self.wir.actions.push(Action::Call {
2112 name: name.to_string(),
2113 args,
2114 span: self.wir_span(span)?,
2115 }));
2116 actions.extend(tail);
2117 return Ok(actions);
2118 }
2119
2120 actions.extend(self.lower_action(statement, Some(BreakTarget::DoWhile))?);
2121 }
2122 Ok(actions)
2123 }
2124
2125 fn lower_range(
2126 &mut self,
2127 iterable: &Expr,
2128 ) -> Result<(wir::ValueId, wir::ValueId, wir::ValueId), IntegrationError> {
2129 let Expr::Call { name, args, .. } = iterable else {
2130 return Err(self.unsupported(
2131 "range loop iterable must be a range(...) call",
2132 iterable.span().copied(),
2133 ));
2134 };
2135 if name != "range" || !(1..=3).contains(&args.len()) {
2136 return Err(self.unsupported(
2137 "range loop requires one to three arguments",
2138 iterable.span().copied(),
2139 ));
2140 }
2141 let span = iterable.span().copied();
2142 let number = |this: &mut Self, value: f64| -> Result<wir::ValueId, IntegrationError> {
2143 Ok(this.wir.values.push(ValueNode::new(
2144 Value::Number {
2145 value,
2146 text: value.to_string(),
2147 },
2148 this.wir_span(span)?,
2149 )))
2150 };
2151 match args.as_slice() {
2152 [stop] => Ok((
2153 number(self, 0.0)?,
2154 self.lower_value(stop)?,
2155 number(self, 1.0)?,
2156 )),
2157 [start, stop] => Ok((
2158 self.lower_value(start)?,
2159 self.lower_value(stop)?,
2160 number(self, 1.0)?,
2161 )),
2162 [start, stop, step] => Ok((
2163 self.lower_value(start)?,
2164 self.lower_value(stop)?,
2165 self.lower_value(step)?,
2166 )),
2167 _ => unreachable!("range arity checked above"),
2168 }
2169 }
2170
2171 fn lower_switch(
2172 &mut self,
2173 value: &Expr,
2174 arms: &[SwitchArm],
2175 span: Option<HirSpan>,
2176 ) -> Result<wir::ActionId, IntegrationError> {
2177 let selector = self.lower_value(value)?;
2178 let mut case_values = Vec::new();
2179 let mut lowered_arms = Vec::with_capacity(arms.len());
2180 let mut case_offsets = Vec::new();
2181 let mut offset = 0usize;
2182 let mut default_offset = None;
2183
2184 for arm in arms {
2185 let (value, (body, break_at)) = match arm {
2186 SwitchArm::Case { value, body, .. } => {
2187 case_values.push(self.lower_value(value)?);
2188 (Some(value), self.lower_switch_body(body)?)
2189 }
2190 SwitchArm::Default { body, span } => {
2191 if default_offset.is_some() {
2192 return Err(
2193 self.unsupported("a switch may contain at most one default arm", *span)
2194 );
2195 }
2196 default_offset = Some(offset);
2197 (None, self.lower_switch_body(body)?)
2198 }
2199 };
2200 if value.is_some() {
2201 case_offsets.push(offset);
2202 }
2203 offset += self.canonical_action_width(&body, span)? + usize::from(break_at.is_some());
2204 lowered_arms.push((value, body, break_at));
2205 }
2206 let default_offset = default_offset.unwrap_or(offset);
2207
2208 let break_arms: Vec<_> = lowered_arms
2209 .iter()
2210 .enumerate()
2211 .filter_map(|(index, (_, _, break_at))| break_at.map(|break_at| (index, break_at)))
2212 .collect();
2213 if break_arms.len() > 1 {
2214 let (first_index, first_break) = break_arms[0];
2215 let has_actions_after_first = lowered_arms[first_index].1.len() > first_break.0
2216 || lowered_arms
2217 .iter()
2218 .skip(first_index + 1)
2219 .any(|(_, body, _)| !body.is_empty());
2220 if has_actions_after_first {
2221 return Err(self.unsupported(
2222 "multiple switch breaks with later reachable actions require canonical switch targets",
2223 Some(break_arms[1].1.1),
2224 ));
2225 }
2226 }
2227
2228 let case_values = self.lower_array(case_values, span)?;
2229 let value_span = self.wir_span(span)?;
2230 let offset_values = std::iter::once(default_offset)
2231 .chain(case_offsets)
2232 .map(|value| {
2233 self.wir.values.push(ValueNode::new(
2234 Value::Number {
2235 value: value as f64,
2236 text: value.to_string(),
2237 },
2238 value_span,
2239 ))
2240 })
2241 .collect();
2242 let offsets = self.lower_array(offset_values, span)?;
2243 let one = self.wir.values.push(ValueNode::new(
2244 Value::Number {
2245 value: 1.0,
2246 text: "1".to_string(),
2247 },
2248 self.wir_span(span)?,
2249 ));
2250 let index = self.wir.values.push(ValueNode::new(
2251 Value::Call {
2252 name: "indexOfArrayValue".to_string(),
2253 args: vec![case_values, selector],
2254 },
2255 self.wir_span(span)?,
2256 ));
2257 let case_offset = self.wir.values.push(ValueNode::new(
2258 Value::Call {
2259 name: "add".to_string(),
2260 args: vec![one, index],
2261 },
2262 self.wir_span(span)?,
2263 ));
2264 let skip_condition = self.wir.values.push(ValueNode::new(
2265 Value::Call {
2266 name: "valueInArray".to_string(),
2267 args: vec![offsets, case_offset],
2268 },
2269 self.wir_span(span)?,
2270 ));
2271 let skip = self.wir.actions.push(Action::Call {
2272 name: "skip".to_string(),
2273 args: vec![skip_condition],
2274 span: self.wir_span(span)?,
2275 });
2276 let true_value = self
2277 .wir
2278 .values
2279 .push(ValueNode::new(Value::Bool(true), self.wir_span(span)?));
2280
2281 let first_break = break_arms.first().copied();
2282 let mut branch_body = vec![skip];
2283 let else_body = if let Some((break_index, (break_at, _))) = first_break {
2284 for (index, (_, body, _)) in lowered_arms.iter().enumerate() {
2285 if index < break_index {
2286 branch_body.extend(body.iter().copied());
2287 } else if index == break_index {
2288 branch_body.extend(body[..break_at].iter().copied());
2289 }
2290 }
2291 let mut tail = Vec::new();
2292 tail.extend(lowered_arms[break_index].1[break_at..].iter().copied());
2293 for (_, body, _) in lowered_arms.iter().skip(break_index + 1) {
2294 tail.extend(body.iter().copied());
2295 }
2296 Some(tail)
2297 } else {
2298 for (_, body, _) in &lowered_arms {
2299 branch_body.extend(body.iter().copied());
2300 }
2301 None
2302 };
2303
2304 Ok(self.wir.actions.push(Action::If {
2305 branches: vec![wir::IfBranch {
2306 condition: true_value,
2307 body: branch_body,
2308 }],
2309 else_body,
2310 span: self.wir_span(span)?,
2311 }))
2312 }
2313
2314 fn lower_switch_body(
2315 &mut self,
2316 statements: &[Stmt],
2317 ) -> Result<LoweredSwitchBody, IntegrationError> {
2318 let mut actions = Vec::new();
2319 let mut break_at = None;
2320 for statement in statements {
2321 if let Stmt::Break { span } = statement {
2322 if break_at.is_some() {
2323 return Err(self.unsupported(
2324 "multiple switch breaks in one arm require canonical switch targets",
2325 *span,
2326 ));
2327 }
2328 break_at = Some((
2329 actions.len(),
2330 span.ok_or_else(|| {
2331 self.unsupported("switch break is missing source provenance", None)
2332 })?,
2333 ));
2334 continue;
2335 }
2336 actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?);
2337 }
2338 Ok((actions, break_at))
2339 }
2340
2341 fn canonical_action_width(
2343 &self,
2344 actions: &[wir::ActionId],
2345 fallback_span: Option<HirSpan>,
2346 ) -> Result<usize, IntegrationError> {
2347 workshop_rs::emitter::action_width(
2348 &self.wir,
2349 &self.compiler.catalog,
2350 &Locale::new("en-US"),
2351 actions,
2352 )
2353 .map(|layout| layout.width)
2354 .map_err(|error| {
2355 let workshop_span = match &error {
2356 workshop_rs::emitter::ActionLayoutError::InvalidWIR(error) => error.span(),
2357 workshop_rs::emitter::ActionLayoutError::Emission(error) => {
2358 workshop_error_span(error)
2359 }
2360 };
2361 let span = workshop_span
2362 .and_then(|span| self.hir_span_from_workshop(span))
2363 .or(fallback_span);
2364 IntegrationError::new("workshop-action-layout", error.to_string(), span)
2365 })
2366 }
2367
2368 fn lower_array(
2369 &mut self,
2370 elements: Vec<wir::ValueId>,
2371 span: Option<HirSpan>,
2372 ) -> Result<wir::ValueId, IntegrationError> {
2373 let name = if elements.is_empty() {
2374 "emptyArray"
2375 } else {
2376 "array"
2377 };
2378 Ok(self.wir.values.push(ValueNode::new(
2379 Value::Call {
2380 name: name.to_string(),
2381 args: elements,
2382 },
2383 self.wir_span(span)?,
2384 )))
2385 }
2386
2387 fn lower_debug(
2388 &mut self,
2389 expr: &Expr,
2390 span: Option<HirSpan>,
2391 ) -> Result<wir::ActionId, IntegrationError> {
2392 macro_rules! call {
2393 ($name:literal $(, $arg:expr)* $(,)?) => {{
2394 let args = vec![$($arg),*];
2395 self.push_call($name, args)
2396 }};
2397 }
2398
2399 let value = self.lower_value(expr)?;
2400 let array_text = if self.debug_value_is_array(value) {
2401 self.lower_debug_array_text(value)
2402 } else {
2403 value
2404 };
2405 let debug_label = canonical_debug_text(&debug_expr_text(expr));
2406 let debug_prefix = format!("{debug_label}\u{2028}= {{0}}");
2407 let inline_padding = 128 - debug_prefix.chars().count() - "{1}".chars().count();
2408 let padding_text = self.push_value(Value::String(" ".repeat(170 - inline_padding)));
2409 let padding = self.push_call("customString", vec![padding_text]);
2410 let debug_label = self.push_value(Value::String(format!(
2411 "{debug_prefix}{}{{1}}",
2412 " ".repeat(inline_padding)
2413 )));
2414 let text = self.push_call("customString", vec![debug_label, array_text, padding]);
2415 let all_teams = self.push_value(Value::Enum {
2416 value_type: "Team".to_string(),
2417 value: "ALL".to_string(),
2418 });
2419 let all_players = call!("allPlayers", all_teams);
2420 let null_value = self.push_value(Value::Null);
2421 let null_value_2 = self.push_value(Value::Null);
2422 let null_value_3 = self.push_value(Value::Null);
2423 let null_value_4 = self.push_value(Value::Null);
2424 let hud_position = self.push_value(Value::Enum {
2425 value_type: "HudPosition".to_string(),
2426 value: "LEFT".to_string(),
2427 });
2428 let sort_order = self.push_value(Value::Number {
2429 value: -9999.0,
2430 text: "-9999".to_string(),
2431 });
2432 let color = self.push_value(Value::Enum {
2433 value_type: "Color".to_string(),
2434 value: "WHITE".to_string(),
2435 });
2436 let reevaluation = self.push_value(Value::Enum {
2437 value_type: "HudReeval".to_string(),
2438 value: "VISIBILITY_SORT_ORDER_STRING_AND_COLOR".to_string(),
2439 });
2440 let visibility = self.push_value(Value::Enum {
2441 value_type: "SpecVisibility".to_string(),
2442 value: "DEFAULT".to_string(),
2443 });
2444 Ok(self.wir.actions.push(Action::Call {
2445 name: "createHudText".to_string(),
2446 args: vec![
2447 all_players,
2448 null_value,
2449 text,
2450 null_value_2,
2451 hud_position,
2452 sort_order,
2453 null_value_3,
2454 color,
2455 null_value_4,
2456 reevaluation,
2457 visibility,
2458 ],
2459 span: self.wir_span(span)?,
2460 }))
2461 }
2462
2463 fn lower_print(
2464 &mut self,
2465 expr: &Expr,
2466 span: Option<HirSpan>,
2467 ) -> Result<wir::ActionId, IntegrationError> {
2468 macro_rules! call {
2469 ($name:literal $(, $arg:expr)* $(,)?) => {{
2470 let args = vec![$($arg),*];
2471 self.push_call($name, args)
2472 }};
2473 }
2474
2475 let message = self.lower_value(expr)?;
2476 let padding_text = self.push_value(Value::String(" ".repeat(45)));
2477 let padding = self.push_call("customString", vec![padding_text]);
2478 let body_text = self.push_value(Value::String(format!("{}{{0}}", " ".repeat(125))));
2479 let body = self.push_call("customString", vec![body_text, padding]);
2480 let all_teams = self.push_value(Value::Enum {
2481 value_type: "Team".to_string(),
2482 value: "ALL".to_string(),
2483 });
2484 let all_players = call!("allPlayers", all_teams);
2485 let null_value = self.push_value(Value::Null);
2486 let null_value_2 = self.push_value(Value::Null);
2487 let null_value_3 = self.push_value(Value::Null);
2488 let hud_position = self.push_value(Value::Enum {
2489 value_type: "HudPosition".to_string(),
2490 value: "LEFT".to_string(),
2491 });
2492 let sort_order = self.push_value(Value::Number {
2493 value: -9999.0,
2494 text: "-9999".to_string(),
2495 });
2496 let color = self.push_value(Value::Enum {
2497 value_type: "Color".to_string(),
2498 value: "ORANGE".to_string(),
2499 });
2500 let reevaluation = self.push_value(Value::Enum {
2501 value_type: "HudReeval".to_string(),
2502 value: "VISIBILITY_AND_STRING".to_string(),
2503 });
2504 let visibility = self.push_value(Value::Enum {
2505 value_type: "SpecVisibility".to_string(),
2506 value: "DEFAULT".to_string(),
2507 });
2508 Ok(self.wir.actions.push(Action::Call {
2509 name: "createHudText".to_string(),
2510 args: vec![
2511 all_players,
2512 message,
2513 body,
2514 null_value,
2515 hud_position,
2516 sort_order,
2517 color,
2518 null_value_2,
2519 null_value_3,
2520 reevaluation,
2521 visibility,
2522 ],
2523 span: self.wir_span(span)?,
2524 }))
2525 }
2526
2527 fn lower_debug_array_text(&mut self, value: wir::ValueId) -> wir::ValueId {
2528 macro_rules! call {
2529 ($name:literal $(, $arg:expr)* $(,)?) => {{
2530 let args = vec![$($arg),*];
2531 self.push_call($name, args)
2532 }};
2533 }
2534
2535 let current_count = call!("countOf", call!("currentArrayElement"));
2536 let is_single = call!(
2537 "==",
2538 call!("countOf", call!("currentArrayElement")),
2539 self.push_number(1.0, "1")
2540 );
2541 let is_empty = call!("==", call!("currentArrayElement"), call!("emptyArray"));
2542 let not_null = call!(
2543 "!=",
2544 call!("currentArrayElement"),
2545 self.push_value(Value::Null)
2546 );
2547 let has_empty_array = call!("and", is_empty, not_null);
2548 let brackets = call!("or", is_single, has_empty_array);
2549 let first_element = call!(
2550 "customString",
2551 self.push_value(Value::String("[{0}]".to_string())),
2552 call!("currentArrayElement"),
2553 );
2554 let many_elements = call!(
2555 "customString",
2556 self.push_value(Value::String("[{0}, …+{1}]".to_string())),
2557 call!("currentArrayElement"),
2558 call!(
2559 "subtract",
2560 call!("countOf", call!("currentArrayElement")),
2561 self.push_number(1.0, "1"),
2562 ),
2563 );
2564 let element_text = call!(
2565 "ifThenElse",
2566 brackets,
2567 first_element,
2568 call!(
2569 "ifThenElse",
2570 current_count,
2571 many_elements,
2572 call!("currentArrayElement"),
2573 ),
2574 );
2575 let mapped_elements = call!("mappedArray", value, element_text,);
2576 let mapped_input = call!("array", mapped_elements);
2577 let current_array = call!("currentArrayElement");
2578 let actual_array = call!(
2579 "or",
2580 call!("countOf", current_array),
2581 call!(
2582 "and",
2583 call!("==", call!("currentArrayElement"), call!("emptyArray")),
2584 call!(
2585 "!=",
2586 call!("currentArrayElement"),
2587 self.push_value(Value::Null)
2588 ),
2589 ),
2590 );
2591 let empty_length = call!(
2592 "ifThenElse",
2593 call!(
2594 "and",
2595 call!("not", call!("countOf", call!("currentArrayElement"))),
2596 call!("!=", call!("currentArrayElement"), call!("emptyArray"),),
2597 ),
2598 self.push_number(3.0, "3"),
2599 call!(
2600 "multiply",
2601 call!("countOf", call!("currentArrayElement")),
2602 self.push_number(3.0, "3"),
2603 ),
2604 );
2605 let x = call!(
2606 "appendToArray",
2607 call!("appendToArray", actual_array, empty_length),
2608 current_array,
2609 );
2610 let x_input = call!("mappedArray", mapped_input, x);
2611 let x_length = |this: &mut Self| {
2612 let current = this.push_call("currentArrayElement", Vec::new());
2613 let index = this.push_number(1.0, "1");
2614 this.push_call("valueInArray", vec![current, index])
2615 };
2616 let x_value = |this: &mut Self, index: f64| {
2617 let current = this.push_call("currentArrayElement", Vec::new());
2618 let index_value = this.push_number(index, &index.to_string());
2619 this.push_call("valueInArray", vec![current, index_value])
2620 };
2621 let first = call!("firstOf", call!("currentArrayElement"));
2622 let array_tail = call!(
2623 "customString",
2624 self.push_value(Value::String("{0}, {1}, {2}".to_string())),
2625 x_value(self, 4.0),
2626 x_value(self, 5.0),
2627 call!(
2628 "customString",
2629 self.push_value(Value::String("{0}, {1}, …\u{0001}".to_string())),
2630 x_value(self, 6.0),
2631 x_value(self, 7.0),
2632 ),
2633 );
2634 let array_head = call!(
2635 "customString",
2636 self.push_value(Value::String("{0}, {1}, {2}".to_string())),
2637 x_value(self, 2.0),
2638 x_value(self, 3.0),
2639 array_tail,
2640 );
2641 let placeholder = call!(
2642 "customString",
2643 self.push_value(Value::String("0, 0, 0, 0, 0, 0, …\u{0001}".to_string())),
2644 );
2645 let length_for_slice = x_length(self);
2646 let end_length_for_slice = x_length(self);
2647 let slice = call!(
2648 "stringSlice",
2649 placeholder,
2650 call!("add", self.push_number(-2.0, "-2"), length_for_slice),
2651 call!(
2652 "subtract",
2653 self.push_number(22.0, "22"),
2654 end_length_for_slice,
2655 ),
2656 );
2657 let replaced = call!("stringReplace", array_head, slice, call!("emptyArray"),);
2658 let length_for_compare = x_length(self);
2659 let length_for_divide = x_length(self);
2660 let plus = call!(
2661 "ifThenElse",
2662 call!(">", length_for_compare, self.push_number(18.0, "18")),
2663 call!(
2664 "customString",
2665 self.push_value(Value::String("+{0}".to_string())),
2666 call!(
2667 "subtract",
2668 call!("divide", length_for_divide, self.push_number(3.0, "3")),
2669 self.push_number(6.0, "6"),
2670 ),
2671 ),
2672 call!("emptyArray"),
2673 );
2674 let formatted_array = call!(
2675 "customString",
2676 self.push_value(Value::String("[{0}{1}]".to_string())),
2677 replaced,
2678 plus,
2679 );
2680 let current_for_split = call!("currentArrayElement");
2681 let rendered = call!(
2682 "ifThenElse",
2683 first,
2684 formatted_array,
2685 call!(
2686 "stringSplit",
2687 call!(
2688 "valueInArray",
2689 current_for_split,
2690 self.push_number(2.0, "2")
2691 ),
2692 call!("emptyArray"),
2693 ),
2694 );
2695 call!("mappedArray", x_input, rendered)
2696 }
2697
2698 fn debug_value_is_array(&self, value: wir::ValueId) -> bool {
2699 match &self
2700 .wir
2701 .values
2702 .get(value)
2703 .expect("lowered value must exist")
2704 .value
2705 {
2706 Value::GlobalVariable(_) | Value::Array(_) => true,
2707 Value::Call { name, .. } if matches!(name.as_str(), "array" | "emptyArray") => true,
2708 Value::Call { name, .. } => self
2709 .compiler
2710 .catalog
2711 .entry(Kind::Value, name)
2712 .and_then(|entry| entry.return_type())
2713 .is_some_and(|return_type| {
2714 return_type.split('|').any(|part| part.trim() == "Array")
2715 }),
2716 _ => false,
2717 }
2718 }
2719
2720 fn push_value(&mut self, value: Value) -> wir::ValueId {
2721 self.wir.values.push(ValueNode::new(value, None))
2722 }
2723
2724 fn push_call(&mut self, name: &str, args: Vec<wir::ValueId>) -> wir::ValueId {
2725 self.push_value(Value::Call {
2726 name: name.to_string(),
2727 args,
2728 })
2729 }
2730
2731 fn push_number(&mut self, value: f64, text: &str) -> wir::ValueId {
2732 self.push_value(Value::Number {
2733 value,
2734 text: text.to_string(),
2735 })
2736 }
2737
2738 fn fold_numeric_binary(
2739 &self,
2740 op: &str,
2741 left: wir::ValueId,
2742 right: wir::ValueId,
2743 ) -> Option<f64> {
2744 let number = |id| match self.wir.values.get(id)?.value {
2745 Value::Number { value, .. } => Some(value),
2746 _ => None,
2747 };
2748 let left = number(left)?;
2749 let right = number(right)?;
2750 let value = match op {
2751 "+" => left + right,
2752 "-" => left - right,
2753 "*" => left * right,
2754 "/" if right != 0.0 => left / right,
2755 "%" if right != 0.0 => left % right,
2756 "**" => left.powf(right),
2757 _ => return None,
2758 };
2759 value.is_finite().then_some(value)
2760 }
2761
2762 fn lower_condition(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
2763 let value = self.lower_value(expr)?;
2764 let is_comparison = |expr: &Expr| matches!(expr, Expr::Binary { op, .. } if matches!(op.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">="));
2765 if is_comparison(expr)
2766 || matches!(expr, Expr::Unary { op, operand, .. } if op == "not" && is_comparison(operand))
2767 {
2768 return Ok(value);
2769 }
2770 let true_value = self.wir.values.push(ValueNode::new(
2771 Value::Bool(true),
2772 self.wir_span(expr.span().copied())?,
2773 ));
2774 Ok(self.wir.values.push(ValueNode::new(
2775 Value::Call {
2776 name: "==".to_string(),
2777 args: vec![value, true_value],
2778 },
2779 self.wir_span(expr.span().copied())?,
2780 )))
2781 }
2782
2783 fn lower_assign(
2784 &mut self,
2785 target: &Expr,
2786 value: &Expr,
2787 span: Option<HirSpan>,
2788 ) -> Result<wir::ActionId, IntegrationError> {
2789 match target {
2790 Expr::GlobalVar {
2791 name,
2792 span: target_span,
2793 } => {
2794 let variable = *self.globals.get(name).ok_or_else(|| {
2795 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
2796 })?;
2797 if let Expr::Binary {
2798 op, left, right, ..
2799 } = value
2800 {
2801 if let Expr::GlobalVar {
2802 name: left_name, ..
2803 } = left.as_ref()
2804 {
2805 if left_name == name {
2806 if let Some(modify_op) = modify_op_from_str(op) {
2807 let val = self.lower_value(right)?;
2808 return Ok(self.wir.actions.push(Action::ModifyGlobalVariable {
2809 variable,
2810 op: modify_op,
2811 value: val,
2812 span: self.wir_span(span)?,
2813 target_span: self.wir_span(*target_span)?,
2814 }));
2815 }
2816 }
2817 }
2818 }
2819 let val = self.lower_value(value)?;
2820 Ok(self.wir.actions.push(Action::SetGlobalVariable {
2821 variable,
2822 value: val,
2823 span: self.wir_span(span)?,
2824 target_span: self.wir_span(*target_span)?,
2825 }))
2826 }
2827 Expr::PlayerVar {
2828 player,
2829 name,
2830 span: target_span,
2831 ..
2832 } => {
2833 let variable = *self.players.get(name).ok_or_else(|| {
2834 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
2835 })?;
2836 let player_val = self.lower_value(player)?;
2837 if let Expr::Binary {
2838 op, left, right, ..
2839 } = value
2840 {
2841 if let Expr::PlayerVar {
2842 player: left_player,
2843 name: left_name,
2844 ..
2845 } = left.as_ref()
2846 {
2847 if left_name == name && left_player.as_ref() == player.as_ref() {
2848 if let Some(modify_op) = modify_op_from_str(op) {
2849 let val = self.lower_value(right)?;
2850 return Ok(self.wir.actions.push(Action::ModifyPlayerVariable {
2851 player: player_val,
2852 variable,
2853 op: modify_op,
2854 value: val,
2855 span: self.wir_span(span)?,
2856 target_span: self.wir_span(*target_span)?,
2857 }));
2858 }
2859 }
2860 }
2861 }
2862 let val = self.lower_value(value)?;
2863 Ok(self.wir.actions.push(Action::SetPlayerVariable {
2864 player: player_val,
2865 variable,
2866 value: val,
2867 span: self.wir_span(span)?,
2868 target_span: self.wir_span(*target_span)?,
2869 }))
2870 }
2871 Expr::Index {
2872 array,
2873 index,
2874 span: target_span,
2875 } => match array.as_ref() {
2876 Expr::GlobalVar {
2877 name,
2878 span: arr_span,
2879 } => {
2880 let variable = *self.globals.get(name).ok_or_else(|| {
2881 self.unsupported(format!("unknown global variable '{name}'"), *arr_span)
2882 })?;
2883 let var_node = self.wir.values.push(ValueNode::new(
2884 Value::GlobalVariable(variable),
2885 self.wir_span(*arr_span)?,
2886 ));
2887 let index_val = self.lower_value(index)?;
2888 if let Expr::Binary {
2889 op, left, right, ..
2890 } = value
2891 {
2892 if let Expr::Index {
2893 array: left_arr,
2894 index: left_idx,
2895 ..
2896 } = left.as_ref()
2897 {
2898 if left_arr.as_ref() == array.as_ref()
2899 && left_idx.as_ref() == index.as_ref()
2900 {
2901 if let Some(op_id) = modify_catalog_name_from_str(op) {
2902 let op_node = self.wir.values.push(ValueNode::new(
2903 Value::Call {
2904 name: op_id.to_string(),
2905 args: Vec::new(),
2906 },
2907 None,
2908 ));
2909 let right_val = self.lower_value(right)?;
2910 return Ok(self.wir.actions.push(Action::Call {
2911 name: "modifyGlobalVariableAtIndex".to_string(),
2912 args: vec![var_node, index_val, op_node, right_val],
2913 span: self.wir_span(span)?,
2914 }));
2915 }
2916 }
2917 }
2918 }
2919 let val = self.lower_value(value)?;
2920 Ok(self.wir.actions.push(Action::Call {
2921 name: "setGlobalVariableAtIndex".to_string(),
2922 args: vec![var_node, index_val, val],
2923 span: self.wir_span(span)?,
2924 }))
2925 }
2926 Expr::PlayerVar {
2927 player,
2928 name,
2929 span: arr_span,
2930 ..
2931 } => {
2932 let player_val = self.lower_value(player)?;
2933 let variable = *self.players.get(name).ok_or_else(|| {
2934 self.unsupported(format!("unknown player variable '{name}'"), *arr_span)
2935 })?;
2936 let var_node = self.wir.values.push(ValueNode::new(
2937 Value::PlayerVariable {
2938 player: player_val,
2939 variable,
2940 },
2941 self.wir_span(*arr_span)?,
2942 ));
2943 let index_val = self.lower_value(index)?;
2944 if let Expr::Binary {
2945 op, left, right, ..
2946 } = value
2947 {
2948 if let Expr::Index {
2949 array: left_arr,
2950 index: left_idx,
2951 ..
2952 } = left.as_ref()
2953 {
2954 if left_arr.as_ref() == array.as_ref()
2955 && left_idx.as_ref() == index.as_ref()
2956 {
2957 if let Some(op_id) = modify_catalog_name_from_str(op) {
2958 let op_node = self.wir.values.push(ValueNode::new(
2959 Value::Call {
2960 name: op_id.to_string(),
2961 args: Vec::new(),
2962 },
2963 None,
2964 ));
2965 let right_val = self.lower_value(right)?;
2966 return Ok(self.wir.actions.push(Action::Call {
2971 name: "modifyPlayerVariableAtIndex".to_string(),
2972 args: vec![var_node, index_val, op_node, right_val],
2973 span: self.wir_span(span)?,
2974 }));
2975 }
2976 }
2977 }
2978 }
2979 let val = self.lower_value(value)?;
2980 Ok(self.wir.actions.push(Action::Call {
2984 name: "setPlayerVariableAtIndex".to_string(),
2985 args: vec![var_node, index_val, val],
2986 span: self.wir_span(span)?,
2987 }))
2988 }
2989 _ => Err(self.unsupported(
2990 "indexing assignment is only representable for global or player variables",
2991 *target_span,
2992 )),
2993 },
2994 _ => Err(self.unsupported(
2995 "only global-variable, player-variable, or index assignment is currently representable in canonical WIR",
2996 span,
2997 )),
2998 }
2999 }
3000
3001 fn lower_action_call(
3002 &mut self,
3003 name: &str,
3004 args: &[Expr],
3005 span: Option<HirSpan>,
3006 ) -> Result<wir::ActionId, IntegrationError> {
3007 let function = self
3008 .compiler
3009 .manifest
3010 .resolve_function(name)
3011 .ok_or_else(|| self.unsupported(format!("unknown action '{name}'"), span))?;
3012 if !matches!(function.kind, FunctionKind::Action) {
3013 return Err(self.unsupported(format!("'{name}' is not a generic OPY action"), span));
3014 }
3015 if function.id == "hudSubheader" {
3016 return self.lower_hud_subheader(args, span);
3017 }
3018 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
3019 self.unsupported(
3020 format!(
3021 "action '{}' requires a special lowering not in #46",
3022 function.id
3023 ),
3024 span,
3025 )
3026 })?;
3027 let args = args
3028 .iter()
3029 .map(|expr| self.lower_value(expr))
3030 .collect::<Result<Vec<_>, _>>()?;
3031 Ok(self.wir.actions.push(Action::Call {
3032 name: catalog_id.clone(),
3033 args,
3034 span: self.wir_span(span)?,
3035 }))
3036 }
3037
3038 fn lower_hud_subheader(
3039 &mut self,
3040 args: &[Expr],
3041 span: Option<HirSpan>,
3042 ) -> Result<wir::ActionId, IntegrationError> {
3043 let [
3044 visible_to,
3045 text,
3046 position,
3047 sort_order,
3048 color,
3049 reevaluation,
3050 spectators,
3051 ] = args
3052 else {
3053 return Err(
3054 self.unsupported("hudSubheader requires exactly seven bound arguments", span)
3055 );
3056 };
3057 let visible_to = self.lower_hud_visible_to(visible_to)?;
3058 let null_header = self.push_value(Value::Null);
3059 let null_text = self.push_value(Value::Null);
3060 let text_value = self.lower_value(text)?;
3061 let text = self.push_call("customString", vec![text_value]);
3062 let args = vec![
3063 visible_to,
3064 null_header,
3065 text,
3066 null_text,
3067 self.lower_value(position)?,
3068 self.lower_value(sort_order)?,
3069 self.push_value(Value::Null),
3070 self.lower_value(color)?,
3071 self.push_value(Value::Null),
3072 self.lower_value(reevaluation)?,
3073 self.lower_value(spectators)?,
3074 ];
3075 Ok(self.wir.actions.push(Action::Call {
3076 name: "createHudText".to_string(),
3077 args,
3078 span: self.wir_span(span)?,
3079 }))
3080 }
3081
3082 fn lower_hud_visible_to(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3083 if let Expr::Call { name, args, .. } = expr {
3084 if name == "getAllPlayers" && args.is_empty() {
3085 let all_teams = self.push_value(Value::Enum {
3086 value_type: "Team".to_string(),
3087 value: "ALL".to_string(),
3088 });
3089 return Ok(self.push_call("allPlayers", vec![all_teams]));
3090 }
3091 }
3092 self.lower_value(expr)
3093 }
3094
3095 fn lower_receiver_action_call(
3096 &mut self,
3097 receiver: &Expr,
3098 name: &str,
3099 args: &[Expr],
3100 span: Option<HirSpan>,
3101 ) -> Result<wir::ActionId, IntegrationError> {
3102 let function = self
3103 .compiler
3104 .manifest
3105 .resolve_member(name)
3106 .ok_or_else(|| self.unsupported(format!("unknown member action '{name}'"), span))?;
3107 if !matches!(function.kind, FunctionKind::MemberAction) {
3108 return Err(self.unsupported(format!("'{name}' is not a member action"), span));
3109 }
3110
3111 if function.id == "append" {
3114 let [value] = args else {
3115 return Err(self.unsupported("append requires exactly one argument", span));
3116 };
3117 let value = self.lower_value(value)?;
3118 return match receiver {
3119 Expr::GlobalVar {
3120 name,
3121 span: target_span,
3122 } => {
3123 let variable = *self.globals.get(name).ok_or_else(|| {
3124 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
3125 })?;
3126 Ok(self.wir.actions.push(Action::ModifyGlobalVariable {
3127 variable,
3128 op: wir::ModifyOp::AppendToArray,
3129 value,
3130 span: self.wir_span(span)?,
3131 target_span: self.wir_span(*target_span)?,
3132 }))
3133 }
3134 Expr::PlayerVar {
3135 player,
3136 name,
3137 span: target_span,
3138 ..
3139 } => {
3140 let variable = *self.players.get(name).ok_or_else(|| {
3141 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
3142 })?;
3143 let player = self.lower_value(player)?;
3144 Ok(self.wir.actions.push(Action::ModifyPlayerVariable {
3145 player,
3146 variable,
3147 op: wir::ModifyOp::AppendToArray,
3148 value,
3149 span: self.wir_span(span)?,
3150 target_span: self.wir_span(*target_span)?,
3151 }))
3152 }
3153 _ => Err(self.unsupported(
3154 "append requires a global or player variable receiver",
3155 receiver.span().copied().or(span),
3156 )),
3157 };
3158 }
3159
3160 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
3161 self.unsupported(
3162 format!(
3163 "member action '{}' has no canonical catalog identity",
3164 function.id
3165 ),
3166 span,
3167 )
3168 })?;
3169 let mut lowered = Vec::with_capacity(args.len() + 1);
3170 lowered.push(self.lower_value(receiver)?);
3171 lowered.extend(
3172 args.iter()
3173 .map(|arg| self.lower_value(arg))
3174 .collect::<Result<Vec<_>, _>>()?,
3175 );
3176 Ok(self.wir.actions.push(Action::Call {
3177 name: catalog_id.clone(),
3178 args: lowered,
3179 span: self.wir_span(span)?,
3180 }))
3181 }
3182
3183 fn lower_value(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3184 let span = expr.span().copied();
3185 let value = match expr {
3186 Expr::Number { value, text, .. } => Value::Number {
3187 value: *value,
3188 text: canonical_number_text(*value, text),
3189 },
3190 Expr::String { value, .. } => Value::String(value.clone()),
3191 Expr::Bool { value, .. } => Value::Bool(*value),
3192 Expr::Null { .. } => Value::Null,
3193 Expr::Local { name, .. } => {
3194 let binding = self.array_bindings.iter().rev().find(|binding| {
3195 binding.element == *name || binding.index.as_deref() == Some(name)
3196 });
3197 match binding {
3198 Some(binding) if binding.element == *name => {
3199 return Ok(self.push_call("currentArrayElement", Vec::new()));
3200 }
3201 Some(_) => return Ok(self.push_call("currentArrayIndex", Vec::new())),
3202 None => {
3203 return Err(self.unsupported(
3204 format!("local '{name}' is not inside a supported array callback"),
3205 span,
3206 ));
3207 }
3208 }
3209 }
3210 Expr::Type { .. } => {
3211 return Err(self.unsupported(
3212 "type expressions are only valid as createWorkshopSetting type arguments",
3213 span,
3214 ));
3215 }
3216 Expr::GlobalVar { name, .. } => {
3217 let id = *self.globals.get(name).ok_or_else(|| {
3218 self.unsupported(format!("unknown global variable '{name}'"), span)
3219 })?;
3220 Value::GlobalVariable(id)
3221 }
3222 Expr::PlayerVar { player, name, .. } => {
3223 let player = self.lower_value(player)?;
3224 let id = *self.players.get(name).ok_or_else(|| {
3225 self.unsupported(format!("unknown player variable '{name}'"), span)
3226 })?;
3227 Value::PlayerVariable {
3228 player,
3229 variable: id,
3230 }
3231 }
3232 Expr::EventPlayer { .. } => Value::EventPlayer,
3233 Expr::HostPlayer { .. } => Value::Call {
3234 name: "hostPlayer".to_string(),
3235 args: Vec::new(),
3236 },
3237 Expr::Enum {
3238 value_type, value, ..
3239 } => {
3240 if self
3241 .compiler
3242 .catalog
3243 .enum_spelling(value_type, &Locale::new("en-US"), value)
3244 .is_none()
3245 {
3246 return Err(self.unsupported(
3247 format!("unknown catalog enum member '{value_type}.{value}'"),
3248 span,
3249 ));
3250 }
3251 Value::Enum {
3252 value_type: value_type.clone(),
3253 value: value.clone(),
3254 }
3255 }
3256 Expr::Array { elements, .. } => {
3257 let elements = elements
3258 .iter()
3259 .map(|element| self.lower_value(element))
3260 .collect::<Result<Vec<_>, _>>()?;
3261 return self.lower_array(elements, span);
3262 }
3263 Expr::Vector { x, y, z, .. } => Value::Call {
3264 name: "vector".to_string(),
3265 args: vec![
3266 self.lower_value(x)?,
3267 self.lower_value(y)?,
3268 self.lower_value(z)?,
3269 ],
3270 },
3271 Expr::Constant { name, .. } => {
3272 let const_expr = *self
3273 .constants
3274 .get(name)
3275 .ok_or_else(|| self.unsupported(format!("unknown constant '{name}'"), span))?;
3276 return self.lower_value(const_expr);
3277 }
3278 Expr::Index { array, index, .. } => {
3279 if matches!(index.as_ref(), Expr::Number { value, .. } if *value == 0.0) {
3283 Value::Call {
3284 name: "firstOf".to_string(),
3285 args: vec![self.lower_value(array)?],
3286 }
3287 } else {
3288 Value::Call {
3289 name: "valueInArray".to_string(),
3290 args: vec![self.lower_value(array)?, self.lower_value(index)?],
3291 }
3292 }
3293 }
3294 Expr::Format { text, args, .. } => {
3295 let text_node = self.wir.values.push(ValueNode::new(
3296 Value::String(canonical_format_text(text)),
3297 self.wir_span(span)?,
3298 ));
3299 let mut call_args = vec![text_node];
3300 for arg in args {
3301 call_args.push(self.lower_value(arg)?);
3302 }
3303 Value::Call {
3304 name: "customString".to_string(),
3305 args: call_args,
3306 }
3307 }
3308 Expr::Conditional {
3309 then_value,
3310 condition,
3311 else_value,
3312 ..
3313 } => Value::Call {
3314 name: "ifThenElse".to_string(),
3315 args: vec![
3316 self.lower_value(condition)?,
3317 self.lower_value(then_value)?,
3318 self.lower_value(else_value)?,
3319 ],
3320 },
3321 Expr::Binary {
3322 op, left, right, ..
3323 } => {
3324 let left = self.lower_value(left)?;
3325 let right = self.lower_value(right)?;
3326 if let Some(value) = self.fold_numeric_binary(op, left, right) {
3327 Value::Number {
3328 value,
3329 text: computed_number_text(value),
3330 }
3331 } else {
3332 match op.as_str() {
3333 "==" | "!=" | "<" | "<=" | ">" | ">=" => Value::Call {
3334 name: op.clone(),
3335 args: vec![left, right],
3336 },
3337 "+" => Value::Call {
3338 name: "add".to_string(),
3339 args: vec![left, right],
3340 },
3341 "-" => Value::Call {
3342 name: "subtract".to_string(),
3343 args: vec![left, right],
3344 },
3345 "*" => Value::Call {
3346 name: "multiply".to_string(),
3347 args: vec![left, right],
3348 },
3349 "/" => Value::Call {
3350 name: "divide".to_string(),
3351 args: vec![left, right],
3352 },
3353 "%" => Value::Call {
3354 name: "modulo".to_string(),
3355 args: vec![left, right],
3356 },
3357 "**" => Value::Call {
3358 name: "raiseToPower".to_string(),
3359 args: vec![left, right],
3360 },
3361 "and" => Value::Call {
3362 name: "and".to_string(),
3363 args: vec![left, right],
3364 },
3365 "or" => Value::Call {
3366 name: "or".to_string(),
3367 args: vec![left, right],
3368 },
3369 "in" => Value::Call {
3370 name: "arrayContains".to_string(),
3371 args: vec![right, left],
3372 },
3373 "not in" => {
3374 let wir_span = self.wir_span(span)?;
3375 let contains = self.wir.values.push(ValueNode::new(
3376 Value::Call {
3377 name: "arrayContains".to_string(),
3378 args: vec![right, left],
3379 },
3380 wir_span,
3381 ));
3382 Value::Call {
3383 name: "not".to_string(),
3384 args: vec![contains],
3385 }
3386 }
3387 _ => {
3388 return Err(self.unsupported(
3389 format!(
3390 "binary operator '{op}' is not currently representable in canonical WIR"
3391 ),
3392 span,
3393 ));
3394 }
3395 }
3396 }
3397 }
3398 Expr::Unary { op, operand, .. } => match op.as_str() {
3399 "not" => {
3400 if let Expr::Binary {
3405 op: comparison,
3406 left,
3407 right,
3408 ..
3409 } = operand.as_ref()
3410 {
3411 if let Some(negated) = negated_comparison(comparison) {
3412 Value::Call {
3413 name: negated.to_string(),
3414 args: vec![self.lower_value(left)?, self.lower_value(right)?],
3415 }
3416 } else {
3417 Value::Call {
3418 name: "not".to_string(),
3419 args: vec![self.lower_value(operand)?],
3420 }
3421 }
3422 } else {
3423 Value::Call {
3424 name: "not".to_string(),
3425 args: vec![self.lower_value(operand)?],
3426 }
3427 }
3428 }
3429 "-" => Value::Call {
3430 name: "-".to_string(),
3431 args: vec![self.lower_value(operand)?],
3432 },
3433 "+" => return self.lower_value(operand),
3434 _ => {
3435 return Err(self.unsupported(
3436 format!(
3437 "unary operator '{op}' is not currently representable in canonical WIR"
3438 ),
3439 span,
3440 ));
3441 }
3442 },
3443 Expr::Call { name, args, .. } => {
3444 if name == "createWorkshopSetting" {
3445 return self.lower_workshop_setting(args, span);
3446 }
3447 if name == "vect" && args.len() == 3 {
3448 Value::Vector {
3449 x: self.lower_value(&args[0])?,
3450 y: self.lower_value(&args[1])?,
3451 z: self.lower_value(&args[2])?,
3452 }
3453 } else if name == "sorted" {
3454 let (array, key) = match args.as_slice() {
3455 [array] => (
3456 self.lower_value(array)?,
3457 self.push_call("currentArrayElement", Vec::new()),
3458 ),
3459 [
3460 array,
3461 Expr::Lambda {
3462 params, body, span, ..
3463 },
3464 ] => {
3465 let array = self.lower_value(array)?;
3466 let key = self.lower_array_callback(params, body, *span)?;
3467 (array, key)
3468 }
3469 _ => {
3470 return Err(self.unsupported(
3471 "sorted requires an array and an optional lambda key",
3472 span,
3473 ));
3474 }
3475 };
3476 Value::Call {
3477 name: "sortedArray".to_string(),
3478 args: vec![array, key],
3479 }
3480 } else {
3481 let function = self
3482 .compiler
3483 .manifest
3484 .resolve_function(name)
3485 .ok_or_else(|| self.unsupported(format!("unknown value '{name}'"), span))?;
3486 if !matches!(function.kind, FunctionKind::Value) {
3487 return Err(
3488 self.unsupported(format!("'{name}' is not a generic OPY value"), span)
3489 );
3490 }
3491 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
3492 self.unsupported(
3493 format!(
3494 "value '{}' requires a special lowering not in #46",
3495 function.id
3496 ),
3497 span,
3498 )
3499 })?;
3500 Value::Call {
3501 name: catalog_id.clone(),
3502 args: args
3503 .iter()
3504 .map(|arg| self.lower_value(arg))
3505 .collect::<Result<Vec<_>, _>>()?,
3506 }
3507 }
3508 }
3509 Expr::ReceiverCall {
3510 receiver,
3511 name,
3512 args,
3513 ..
3514 } => {
3515 let function = self.compiler.manifest.resolve_member(name).ok_or_else(|| {
3516 self.unsupported(format!("unknown member value '{name}'"), span)
3517 })?;
3518 if !matches!(function.kind, FunctionKind::MemberValue) {
3519 return Err(self.unsupported(format!("'{name}' is not a member value"), span));
3520 }
3521 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
3522 self.unsupported(
3523 format!(
3524 "member value '{}' has no canonical catalog identity",
3525 function.id
3526 ),
3527 span,
3528 )
3529 })?;
3530 let mut lowered = Vec::with_capacity(args.len() + 1);
3531 lowered.push(self.lower_value(receiver)?);
3532 lowered.extend(
3533 args.iter()
3534 .map(|arg| self.lower_value(arg))
3535 .collect::<Result<Vec<_>, _>>()?,
3536 );
3537 Value::Call {
3538 name: catalog_id.clone(),
3539 args: lowered,
3540 }
3541 }
3542 Expr::Member {
3543 receiver, member, ..
3544 } => {
3545 let receiver = self.lower_value(receiver)?;
3546 let member = self.wir.values.push(ValueNode::new(
3547 Value::String(member.clone()),
3548 self.wir_span(span)?,
3549 ));
3550 Value::Call {
3551 name: "memberAccess".to_string(),
3552 args: vec![receiver, member],
3553 }
3554 }
3555 Expr::Comprehension {
3556 element,
3557 variable,
3558 index,
3559 iterable,
3560 condition,
3561 span: comprehension_span,
3562 ..
3563 } => {
3564 if condition.is_some() && index.is_some() {
3565 return Err(self.unsupported(
3566 "comprehensions with both a filter and an index binder are not currently representable in canonical WIR",
3567 *comprehension_span,
3568 ));
3569 }
3570 let iterable = self.lower_value(iterable)?;
3571 let binding = ArrayBinding {
3572 element: variable.clone(),
3573 index: index.clone(),
3574 };
3575 self.array_bindings.push(binding);
3576 let predicate = condition
3577 .as_deref()
3578 .map(|condition| self.lower_value(condition));
3579 let element = self.lower_value(element);
3580 self.array_bindings.pop();
3581 let element = element?;
3582 let iterable = if let Some(predicate) = predicate {
3583 let predicate = predicate?;
3584 self.push_call("filteredArray", vec![iterable, predicate])
3585 } else {
3586 iterable
3587 };
3588 Value::Call {
3589 name: "mappedArray".to_string(),
3590 args: vec![iterable, element],
3591 }
3592 }
3593 Expr::Lambda { span, .. } => {
3594 return Err(self.unsupported(
3595 "lambda expressions are only representable as supported array operation arguments",
3596 *span,
3597 ));
3598 }
3599 _ => {
3600 return Err(self.unsupported(
3601 format!(
3602 "expression '{}' is not currently representable in canonical WIR",
3603 expr.kind_name()
3604 ),
3605 span,
3606 ));
3607 }
3608 };
3609 Ok(self
3610 .wir
3611 .values
3612 .push(ValueNode::new(value, self.wir_span(span)?)))
3613 }
3614
3615 fn lower_array_callback(
3616 &mut self,
3617 params: &[String],
3618 body: &Expr,
3619 span: Option<HirSpan>,
3620 ) -> Result<wir::ValueId, IntegrationError> {
3621 if !(1..=2).contains(¶ms.len()) {
3622 return Err(self.unsupported(
3623 "array callbacks require one element parameter and at most one index parameter",
3624 span,
3625 ));
3626 }
3627 if params.windows(2).any(|pair| pair[0] == pair[1]) {
3628 return Err(
3629 self.unsupported("array callback parameters must have distinct names", span)
3630 );
3631 }
3632 self.array_bindings.push(ArrayBinding {
3633 element: params[0].clone(),
3634 index: params.get(1).cloned(),
3635 });
3636 let result = self.lower_value(body);
3637 self.array_bindings.pop();
3638 result
3639 }
3640
3641 fn lower_workshop_setting(
3642 &mut self,
3643 args: &[Expr],
3644 span: Option<HirSpan>,
3645 ) -> Result<wir::ValueId, IntegrationError> {
3646 let [
3647 Expr::Type {
3648 name: setting_type,
3649 args: type_args,
3650 span: type_span,
3651 },
3652 category,
3653 setting_name,
3654 default,
3655 sort_order,
3656 ] = args
3657 else {
3658 return Err(self.unsupported(
3659 "createWorkshopSetting requires a type and four value arguments",
3660 span,
3661 ));
3662 };
3663
3664 let catalog_name = match (setting_type.as_str(), type_args.as_slice()) {
3665 ("bool", []) => "createWorkshopSettingBool",
3666 ("int", [_, _]) => "createWorkshopSettingInt",
3667 ("float", [_, _]) => "createWorkshopSettingFloat",
3668 ("int", []) | ("float", []) => {
3669 return Err(self.unsupported(
3670 format!("createWorkshopSetting type '{setting_type}' requires a numeric range"),
3671 type_span.or(span),
3672 ));
3673 }
3674 _ => {
3675 return Err(self.unsupported(
3676 format!("unsupported createWorkshopSetting type '{setting_type}'"),
3677 type_span.or(span),
3678 ));
3679 }
3680 };
3681
3682 let category = match category {
3685 Expr::String { value, .. } if value.is_empty() => {
3686 self.push_value(Value::String("\u{3000}".to_string()))
3687 }
3688 _ => self.lower_value(category)?,
3689 };
3690 let mut lowered = vec![
3691 category,
3692 self.lower_value(setting_name)?,
3693 self.lower_value(default)?,
3694 ];
3695 if let [minimum, maximum] = type_args.as_slice() {
3696 lowered.push(self.lower_value(minimum)?);
3697 lowered.push(self.lower_value(maximum)?);
3698 }
3699 lowered.push(self.lower_value(sort_order)?);
3700 Ok(self.push_call(catalog_name, lowered))
3701 }
3702
3703 fn wir_span(&self, span: Option<HirSpan>) -> Result<Option<WorkshopSpan>, IntegrationError> {
3704 let Some(span) = span else {
3705 return Ok(None);
3706 };
3707 let file = *self.files.get(&span.file).ok_or_else(|| {
3708 IntegrationError::new(
3709 "source-file",
3710 format!("HIR span references unknown source file id {}", span.file),
3711 Some(span),
3712 )
3713 })?;
3714 Ok(Some(WorkshopSpan::new(
3715 file,
3716 WorkshopPosition::new(span.start.line, span.start.col),
3717 WorkshopPosition::new(span.end.line, span.end.col),
3718 )))
3719 }
3720
3721 fn hir_span_from_workshop(&self, span: WorkshopSpan) -> Option<HirSpan> {
3722 let file = *self.wir_to_hir_files.get(span.file.index())?;
3723 Some(HirSpan {
3724 file,
3725 start: hir::Position {
3726 line: span.start.line,
3727 col: span.start.col,
3728 },
3729 end: hir::Position {
3730 line: span.end.line,
3731 col: span.end.col,
3732 },
3733 })
3734 }
3735
3736 fn unsupported(&self, message: impl Into<String>, span: Option<HirSpan>) -> IntegrationError {
3737 IntegrationError::new("unsupported-integration-surface", message, span)
3738 }
3739}
3740
3741fn implicit_default_variables(
3745 hir: &hir::Program,
3746) -> (
3747 BTreeMap<String, Option<HirSpan>>,
3748 BTreeMap<String, Option<HirSpan>>,
3749) {
3750 let declared_globals = hir
3751 .declarations
3752 .iter()
3753 .filter_map(|declaration| match declaration {
3754 hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()),
3755 _ => None,
3756 })
3757 .collect::<HashSet<_>>();
3758 let declared_players = hir
3759 .declarations
3760 .iter()
3761 .filter_map(|declaration| match declaration {
3762 hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()),
3763 _ => None,
3764 })
3765 .collect::<HashSet<_>>();
3766 let mut globals = BTreeMap::new();
3767 let mut players = BTreeMap::new();
3768 for declaration in &hir.declarations {
3769 let initializer = match declaration {
3770 hir::Declaration::GlobalVariable { initializer, .. }
3771 | hir::Declaration::PlayerVariable { initializer, .. } => initializer.as_ref(),
3772 hir::Declaration::Constant { value, .. } => Some(value),
3773 _ => None,
3774 };
3775 if let Some(expr) = initializer {
3776 collect_implicit_expr(
3777 expr,
3778 &declared_globals,
3779 &declared_players,
3780 &mut globals,
3781 &mut players,
3782 );
3783 }
3784 }
3785 for entry in &hir.rules {
3786 match entry {
3787 RuleEntry::Rule(rule) => {
3788 for condition in &rule.conditions {
3789 collect_implicit_expr(
3790 condition,
3791 &declared_globals,
3792 &declared_players,
3793 &mut globals,
3794 &mut players,
3795 );
3796 }
3797 collect_implicit_stmts(
3798 &rule.actions,
3799 &declared_globals,
3800 &declared_players,
3801 &mut globals,
3802 &mut players,
3803 );
3804 }
3805 RuleEntry::SubroutineDef { body, .. } => collect_implicit_stmts(
3806 body,
3807 &declared_globals,
3808 &declared_players,
3809 &mut globals,
3810 &mut players,
3811 ),
3812 }
3813 }
3814 (globals, players)
3815}
3816
3817fn collect_implicit_stmts(
3818 statements: &[Stmt],
3819 declared_globals: &HashSet<&str>,
3820 declared_players: &HashSet<&str>,
3821 globals: &mut BTreeMap<String, Option<HirSpan>>,
3822 players: &mut BTreeMap<String, Option<HirSpan>>,
3823) {
3824 for statement in statements {
3825 match statement {
3826 Stmt::Expr { expr, .. } => {
3827 collect_implicit_expr(expr, declared_globals, declared_players, globals, players)
3828 }
3829 Stmt::Assign { target, value, .. } => {
3830 collect_implicit_expr(target, declared_globals, declared_players, globals, players);
3831 collect_implicit_expr(value, declared_globals, declared_players, globals, players);
3832 }
3833 Stmt::If {
3834 branches, r#else, ..
3835 } => {
3836 for branch in branches {
3837 collect_implicit_expr(
3838 &branch.condition,
3839 declared_globals,
3840 declared_players,
3841 globals,
3842 players,
3843 );
3844 collect_implicit_stmts(
3845 &branch.body,
3846 declared_globals,
3847 declared_players,
3848 globals,
3849 players,
3850 );
3851 }
3852 if let Some(default_body) = r#else {
3853 collect_implicit_stmts(
3854 default_body,
3855 declared_globals,
3856 declared_players,
3857 globals,
3858 players,
3859 );
3860 }
3861 }
3862 Stmt::For {
3863 variable,
3864 iterable,
3865 body,
3866 ..
3867 } => {
3868 collect_implicit_expr(
3869 variable,
3870 declared_globals,
3871 declared_players,
3872 globals,
3873 players,
3874 );
3875 collect_implicit_expr(
3876 iterable,
3877 declared_globals,
3878 declared_players,
3879 globals,
3880 players,
3881 );
3882 collect_implicit_stmts(body, declared_globals, declared_players, globals, players);
3883 }
3884 Stmt::While {
3885 condition, body, ..
3886 }
3887 | Stmt::DoWhile {
3888 condition, body, ..
3889 } => {
3890 collect_implicit_expr(
3891 condition,
3892 declared_globals,
3893 declared_players,
3894 globals,
3895 players,
3896 );
3897 collect_implicit_stmts(body, declared_globals, declared_players, globals, players);
3898 }
3899 Stmt::Switch { value, arms, .. } => {
3900 collect_implicit_expr(value, declared_globals, declared_players, globals, players);
3901 for arm in arms {
3902 match arm {
3903 SwitchArm::Case { value, body, .. } => {
3904 collect_implicit_expr(
3905 value,
3906 declared_globals,
3907 declared_players,
3908 globals,
3909 players,
3910 );
3911 collect_implicit_stmts(
3912 body,
3913 declared_globals,
3914 declared_players,
3915 globals,
3916 players,
3917 );
3918 }
3919 SwitchArm::Default { body, .. } => {
3920 collect_implicit_stmts(
3921 body,
3922 declared_globals,
3923 declared_players,
3924 globals,
3925 players,
3926 );
3927 }
3928 }
3929 }
3930 }
3931 Stmt::Break { .. } | Stmt::CallSubroutine { .. } | Stmt::Pass { .. } => {}
3932 }
3933 }
3934}
3935
3936fn collect_implicit_expr(
3937 expr: &Expr,
3938 declared_globals: &HashSet<&str>,
3939 declared_players: &HashSet<&str>,
3940 globals: &mut BTreeMap<String, Option<HirSpan>>,
3941 players: &mut BTreeMap<String, Option<HirSpan>>,
3942) {
3943 match expr {
3944 Expr::GlobalVar { name, span } => {
3945 if !declared_globals.contains(name.as_str()) && default_var_index(name).is_some() {
3946 globals.entry(name.clone()).or_insert(*span);
3947 }
3948 }
3949 Expr::Array { elements, .. } => {
3950 for element in elements {
3951 collect_implicit_expr(
3952 element,
3953 declared_globals,
3954 declared_players,
3955 globals,
3956 players,
3957 );
3958 }
3959 }
3960 Expr::Dict { entries, .. } => {
3961 for entry in entries {
3962 collect_implicit_expr(
3963 &entry.key,
3964 declared_globals,
3965 declared_players,
3966 globals,
3967 players,
3968 );
3969 collect_implicit_expr(
3970 &entry.value,
3971 declared_globals,
3972 declared_players,
3973 globals,
3974 players,
3975 );
3976 }
3977 }
3978 Expr::Comprehension {
3979 element,
3980 iterable,
3981 condition,
3982 ..
3983 } => {
3984 collect_implicit_expr(
3985 element,
3986 declared_globals,
3987 declared_players,
3988 globals,
3989 players,
3990 );
3991 collect_implicit_expr(
3992 iterable,
3993 declared_globals,
3994 declared_players,
3995 globals,
3996 players,
3997 );
3998 if let Some(condition) = condition {
3999 collect_implicit_expr(
4000 condition,
4001 declared_globals,
4002 declared_players,
4003 globals,
4004 players,
4005 );
4006 }
4007 }
4008 Expr::Lambda { body, .. } => {
4009 collect_implicit_expr(body, declared_globals, declared_players, globals, players)
4010 }
4011 Expr::Type { args, .. } => {
4012 for arg in args {
4013 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4014 }
4015 }
4016 Expr::Vector { x, y, z, .. } => {
4017 collect_implicit_expr(x, declared_globals, declared_players, globals, players);
4018 collect_implicit_expr(y, declared_globals, declared_players, globals, players);
4019 collect_implicit_expr(z, declared_globals, declared_players, globals, players);
4020 }
4021 Expr::PlayerVar {
4022 player,
4023 name,
4024 member_span,
4025 span,
4026 } => {
4027 if !declared_players.contains(name.as_str()) && default_var_index(name).is_some() {
4028 players.entry(name.clone()).or_insert(member_span.or(*span));
4029 }
4030 collect_implicit_expr(player, declared_globals, declared_players, globals, players);
4031 }
4032 Expr::Member {
4033 receiver,
4034 member,
4035 span,
4036 ..
4037 } => {
4038 if !declared_players.contains(member.as_str()) && default_var_index(member).is_some() {
4039 players.entry(member.clone()).or_insert(*span);
4040 }
4041 collect_implicit_expr(
4042 receiver,
4043 declared_globals,
4044 declared_players,
4045 globals,
4046 players,
4047 );
4048 }
4049 Expr::Call { args, .. } | Expr::MacroCall { args, .. } => {
4050 for arg in args {
4051 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4052 }
4053 }
4054 Expr::ReceiverCall { receiver, args, .. } => {
4055 collect_implicit_expr(
4056 receiver,
4057 declared_globals,
4058 declared_players,
4059 globals,
4060 players,
4061 );
4062 for arg in args {
4063 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4064 }
4065 }
4066 Expr::Binary { left, right, .. } => {
4067 collect_implicit_expr(left, declared_globals, declared_players, globals, players);
4068 collect_implicit_expr(right, declared_globals, declared_players, globals, players);
4069 }
4070 Expr::Conditional {
4071 then_value,
4072 condition,
4073 else_value,
4074 ..
4075 } => {
4076 collect_implicit_expr(
4077 then_value,
4078 declared_globals,
4079 declared_players,
4080 globals,
4081 players,
4082 );
4083 collect_implicit_expr(
4084 condition,
4085 declared_globals,
4086 declared_players,
4087 globals,
4088 players,
4089 );
4090 collect_implicit_expr(
4091 else_value,
4092 declared_globals,
4093 declared_players,
4094 globals,
4095 players,
4096 );
4097 }
4098 Expr::Unary { operand, .. } => collect_implicit_expr(
4099 operand,
4100 declared_globals,
4101 declared_players,
4102 globals,
4103 players,
4104 ),
4105 Expr::Index { array, index, .. } => {
4106 collect_implicit_expr(array, declared_globals, declared_players, globals, players);
4107 collect_implicit_expr(index, declared_globals, declared_players, globals, players);
4108 }
4109 Expr::Format { args, .. } => {
4110 for arg in args {
4111 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4112 }
4113 }
4114 Expr::Number { .. }
4115 | Expr::String { .. }
4116 | Expr::Bool { .. }
4117 | Expr::Null { .. }
4118 | Expr::StringModifier { .. }
4119 | Expr::Local { .. }
4120 | Expr::Enum { .. }
4121 | Expr::EventPlayer { .. }
4122 | Expr::HostPlayer { .. }
4123 | Expr::Constant { .. }
4124 | Expr::MacroParam { .. } => {}
4125 }
4126}
4127
4128fn allocate_indices(
4129 entries: &[(Option<u32>, Option<HirSpan>)],
4130 pre_reserved: &HashSet<u32>,
4131 kind: &str,
4132) -> Result<Vec<u32>, IntegrationError> {
4133 let mut reserved = pre_reserved.clone();
4134 for (index, span) in entries {
4135 let Some(index) = index else {
4136 continue;
4137 };
4138 if !reserved.insert(*index) {
4139 return Err(IntegrationError::new(
4140 "index-collision",
4141 format!("duplicate explicit {kind} index {index}"),
4142 *span,
4143 ));
4144 }
4145 }
4146
4147 let mut next = 0;
4152 let mut allocated = Vec::with_capacity(entries.len());
4153 for (index, span) in entries {
4154 let assigned = if let Some(index) = index {
4155 *index
4156 } else {
4157 while reserved.contains(&next) {
4158 next = next.checked_add(1).ok_or_else(|| {
4159 IntegrationError::new(
4160 "index-exhausted",
4161 format!("no available {kind} index remains"),
4162 *span,
4163 )
4164 })?;
4165 }
4166 reserved.insert(next);
4167 let assigned = next;
4168 next = next.checked_add(1).ok_or_else(|| {
4169 IntegrationError::new(
4170 "index-exhausted",
4171 format!("no available {kind} index remains"),
4172 *span,
4173 )
4174 })?;
4175 assigned
4176 };
4177 allocated.push(assigned);
4178 }
4179 Ok(allocated)
4180}
4181
4182fn player_event_kind(name: &str) -> Option<PlayerEventKind> {
4183 Some(match name {
4184 "playerDealtDamage" => PlayerEventKind::DealtDamage,
4185 "playerDealtFinalBlow" => PlayerEventKind::DealtFinalBlow,
4186 "playerDealtHealing" => PlayerEventKind::DealtHealing,
4187 "playerDied" => PlayerEventKind::Died,
4188 "playerEarnedElimination" => PlayerEventKind::EarnedElimination,
4189 "playerJoined" => PlayerEventKind::Joined,
4190 "playerLeft" => PlayerEventKind::Left,
4191 "playerReceivedHealing" => PlayerEventKind::ReceivedHealing,
4192 "playerTookDamage" => PlayerEventKind::TookDamage,
4193 _ => return None,
4194 })
4195}
4196
4197fn is_zero_initializer(expr: &hir::Expr) -> bool {
4198 match expr {
4199 hir::Expr::Number { text, value, .. } => text == "0" && *value == 0.0,
4200 _ => false,
4201 }
4202}
4203
4204fn canonical_number_text(value: f64, text: &str) -> String {
4205 if text.starts_with("0x") || text.starts_with("0X") {
4206 value.to_string()
4207 } else {
4208 text.to_string()
4209 }
4210}
4211
4212fn computed_number_text(value: f64) -> String {
4213 if value.fract() == 0.0 {
4214 format!("{value:.0}")
4215 } else {
4216 value.to_string()
4217 }
4218}
4219
4220fn canonical_format_text(text: &str) -> String {
4221 let mut output = String::with_capacity(text.len());
4222 let mut chars = text.chars().peekable();
4223 let mut index = 0;
4224 while let Some(character) = chars.next() {
4225 if character == '{' && chars.peek() == Some(&'}') {
4226 chars.next();
4227 output.push('{');
4228 output.push_str(&index.to_string());
4229 output.push('}');
4230 index += 1;
4231 } else {
4232 output.push(character);
4233 }
4234 }
4235 output
4236}
4237
4238fn debug_expr_text(expr: &Expr) -> String {
4239 match expr {
4240 Expr::Number { text, .. } => text.clone(),
4241 Expr::String { value, .. } => {
4242 format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
4243 }
4244 Expr::Bool { value, .. } => value.to_string(),
4245 Expr::Null { .. } => "null".to_string(),
4246 Expr::Array { elements, .. } => format!(
4247 "[{}]",
4248 elements
4249 .iter()
4250 .map(debug_expr_text)
4251 .collect::<Vec<_>>()
4252 .join(", ")
4253 ),
4254 Expr::Dict { entries, .. } => format!(
4255 "{{{}}}",
4256 entries
4257 .iter()
4258 .map(|entry| format!(
4259 "{}: {}",
4260 debug_expr_text(&entry.key),
4261 debug_expr_text(&entry.value)
4262 ))
4263 .collect::<Vec<_>>()
4264 .join(", ")
4265 ),
4266 Expr::Comprehension {
4267 element,
4268 variable,
4269 iterable,
4270 condition,
4271 ..
4272 } => {
4273 let condition = condition
4274 .as_deref()
4275 .map(|condition| format!(" if {}", debug_expr_text(condition)))
4276 .unwrap_or_default();
4277 format!(
4278 "[{} for {} in {}{}]",
4279 debug_expr_text(element),
4280 variable,
4281 debug_expr_text(iterable),
4282 condition
4283 )
4284 }
4285 Expr::Lambda { params, body, .. } => {
4286 format!("lambda {}: {}", params.join(", "), debug_expr_text(body))
4287 }
4288 Expr::StringModifier {
4289 modifier, value, ..
4290 } => format!("{}\"{}\"", modifier, value),
4291 Expr::Local { name, .. }
4292 | Expr::GlobalVar { name, .. }
4293 | Expr::Constant { name, .. }
4294 | Expr::MacroParam { name, .. } => name.clone(),
4295 Expr::Type { name, args, .. } => {
4296 if args.is_empty() {
4297 name.clone()
4298 } else {
4299 format!(
4300 "{}[{}]",
4301 name,
4302 args.iter()
4303 .map(debug_expr_text)
4304 .collect::<Vec<_>>()
4305 .join(": ")
4306 )
4307 }
4308 }
4309 Expr::Vector { x, y, z, .. } => format!(
4310 "vect({}, {}, {})",
4311 debug_expr_text(x),
4312 debug_expr_text(y),
4313 debug_expr_text(z)
4314 ),
4315 Expr::Enum {
4316 value_type, value, ..
4317 } => format!("{}.{}", value_type, value),
4318 Expr::PlayerVar { player, name, .. } => {
4319 format!("{}.{}", debug_expr_text(player), name)
4320 }
4321 Expr::Member {
4322 receiver, member, ..
4323 } => format!("{}.{}", debug_expr_text(receiver), member),
4324 Expr::EventPlayer { .. } => "eventPlayer".to_string(),
4325 Expr::HostPlayer { .. } => "hostPlayer".to_string(),
4326 Expr::Call { name, args, .. } if name == "sorted" && args.len() == 2 => {
4327 format!(
4328 "sorted({}, key = {})",
4329 debug_expr_text(&args[0]),
4330 debug_expr_text(&args[1])
4331 )
4332 }
4333 Expr::Call { name, args, .. } | Expr::MacroCall { name, args, .. } => format!(
4334 "{}({})",
4335 name,
4336 args.iter()
4337 .map(debug_expr_text)
4338 .collect::<Vec<_>>()
4339 .join(", ")
4340 ),
4341 Expr::ReceiverCall {
4342 receiver,
4343 name,
4344 args,
4345 ..
4346 } => format!(
4347 "{}.{}({})",
4348 debug_expr_text(receiver),
4349 name,
4350 args.iter()
4351 .map(debug_expr_text)
4352 .collect::<Vec<_>>()
4353 .join(", ")
4354 ),
4355 Expr::Binary {
4356 left, op, right, ..
4357 } => format!(
4358 "{} {} {}",
4359 debug_expr_text(left),
4360 op,
4361 debug_expr_text(right)
4362 ),
4363 Expr::Conditional {
4364 then_value,
4365 condition,
4366 else_value,
4367 ..
4368 } => format!(
4369 "{} if {} else {}",
4370 debug_expr_text(then_value),
4371 debug_expr_text(condition),
4372 debug_expr_text(else_value)
4373 ),
4374 Expr::Unary { op, operand, .. } => format!("{} {}", op, debug_expr_text(operand)),
4375 Expr::Index { array, index, .. } => {
4376 format!("{}[{}]", debug_expr_text(array), debug_expr_text(index))
4377 }
4378 Expr::Format { text, args, .. } => format!(
4379 "\"{}\".format({})",
4380 text,
4381 args.iter()
4382 .map(debug_expr_text)
4383 .collect::<Vec<_>>()
4384 .join(", ")
4385 ),
4386 }
4387}
4388
4389fn canonical_debug_text(text: &str) -> String {
4390 text.chars()
4391 .map(|character| match character {
4392 'a' => 'ạ',
4393 'b' => 'ḅ',
4394 'c' => 'ƈ',
4395 'd' => 'ḍ',
4396 'e' => 'ẹ',
4397 'f' => 'ƒ',
4398 'g' => 'ǥ',
4399 'h' => 'һ',
4400 'i' => 'і',
4401 'j' => 'ј',
4402 'k' => 'ḳ',
4403 'l' => 'I',
4404 'm' => 'ṃ',
4405 'n' => 'ṇ',
4406 'o' => 'ο',
4407 'p' => 'ṗ',
4408 'q' => 'ǫ',
4409 'r' => 'ṛ',
4410 's' => 'ѕ',
4411 't' => 'ṭ',
4412 'u' => 'υ',
4413 'v' => 'ν',
4414 'w' => 'ẉ',
4415 'x' => 'ҳ',
4416 'y' => 'ỵ',
4417 'z' => 'ẓ',
4418 _ => character,
4419 })
4420 .collect()
4421}
4422
4423fn negated_comparison(op: &str) -> Option<&'static str> {
4424 Some(match op {
4425 "==" => "!=",
4426 "!=" => "==",
4427 "<" => ">=",
4428 ">" => "<=",
4429 "<=" => ">",
4430 ">=" => "<",
4431 _ => return None,
4432 })
4433}
4434
4435fn modify_op_from_str(op: &str) -> Option<wir::ModifyOp> {
4436 match op {
4437 "+" => Some(wir::ModifyOp::Add),
4438 "-" => Some(wir::ModifyOp::Subtract),
4439 "*" => Some(wir::ModifyOp::Multiply),
4440 "/" => Some(wir::ModifyOp::Divide),
4441 "%" => Some(wir::ModifyOp::Modulo),
4442 "**" => Some(wir::ModifyOp::RaiseToPower),
4443 _ => None,
4444 }
4445}
4446
4447fn modify_catalog_name_from_str(op: &str) -> Option<&'static str> {
4448 match op {
4449 "+" => Some("add"),
4450 "-" => Some("subtract"),
4451 "*" => Some("multiply"),
4452 "/" => Some("divide"),
4453 "%" => Some("modulo"),
4454 "**" => Some("raiseToPower"),
4455 _ => None,
4456 }
4457}
4458
4459fn hir_span_from_diag(span: crate::diag::Span) -> HirSpan {
4460 HirSpan {
4461 file: span.file,
4462 start: hir::Position {
4463 line: span.start.line,
4464 col: span.start.col,
4465 },
4466 end: hir::Position {
4467 line: span.end.line,
4468 col: span.end.col,
4469 },
4470 }
4471}
4472
4473fn workshop_error_span(error: &workshop_rs::WorkshopError) -> Option<WorkshopSpan> {
4474 match error {
4475 workshop_rs::WorkshopError::Unknown { span, .. }
4476 | workshop_rs::WorkshopError::Malformed { span, .. }
4477 | workshop_rs::WorkshopError::Unsupported { span, .. } => *span,
4478 workshop_rs::WorkshopError::Catalog(_)
4479 | workshop_rs::WorkshopError::MissingMapping { .. } => None,
4480 }
4481}
4482
4483#[cfg(test)]
4484mod tests {
4485 use super::{
4486 COMPILE_SCHEMA_VERSION, CompileFailureClass, CompileStatus, Compiler, WORKSHOP_RS_VERSION,
4487 cross_check_manifest,
4488 };
4489 use crate::manifest::Manifest;
4490 use std::path::Path;
4491 use workshop_rs::catalog::{Catalog, Locale};
4492
4493 #[test]
4494 fn public_contract_is_pinned_and_manifest_links_are_checked() {
4495 let compiler = Compiler::new().expect("released workshop contract must load");
4496 let identity = compiler.catalog_identity();
4497 assert_eq!(identity.implementation_version, WORKSHOP_RS_VERSION);
4498 assert!(compiler.link_report().catalog_ids_checked > 0);
4499 assert!(compiler.link_report().domains_checked > 0);
4500 }
4501
4502 #[test]
4503 fn compile_report_is_versioned_and_contains_reproducibility_identity() {
4504 let compiler = Compiler::new().unwrap();
4505 let report = compiler.compile_source_report_with_locale(
4506 "rule \"report\":\n @Event global\n disableInspector()\n",
4507 "report.opy",
4508 Path::new("."),
4509 &Locale::new("en-US"),
4510 );
4511 assert_eq!(report.schema_version, COMPILE_SCHEMA_VERSION);
4512 assert_eq!(report.compiler.name, "opy-rs");
4513 assert_eq!(report.catalog.implementation_version, WORKSHOP_RS_VERSION);
4514 assert_eq!(report.compile.status, CompileStatus::Success);
4515 assert_eq!(report.compile.exit_code, 0);
4516 assert!(report.compile.diagnostics.is_empty());
4517 assert_eq!(
4518 report.compile.workshop,
4519 report
4520 .compile
4521 .workshop_exact
4522 .trim_end_matches('\n')
4523 .to_owned()
4524 + "\n"
4525 );
4526 assert!(serde_json::to_value(report).unwrap()["catalog"]["catalog-version"].is_string());
4527 }
4528
4529 #[test]
4530 fn compile_report_preserves_frontend_failure_class_and_source_path() {
4531 let compiler = Compiler::new().unwrap();
4532 let report = compiler.compile_source_report_with_locale(
4533 "rule \"broken\":\n @Event global\n missing()\n",
4534 "broken.opy",
4535 Path::new("."),
4536 &Locale::new("en-US"),
4537 );
4538 assert_eq!(report.compile.status, CompileStatus::Failure);
4539 assert_eq!(
4540 report.compile.failure_class,
4541 Some(CompileFailureClass::Frontend)
4542 );
4543 assert_eq!(report.compile.exit_code, 1);
4544 let diagnostic = &report.compile.diagnostics[0];
4545 assert_eq!(diagnostic.code, "unknown-action");
4546 assert_eq!(diagnostic.span.as_ref().unwrap().path, "broken.opy");
4547 }
4548
4549 #[test]
4550 fn compile_report_preserves_integration_failure_class_and_source_path() {
4551 let compiler = Compiler::new().unwrap();
4552 let report = compiler.compile_source_report_with_locale(
4553 "globalvar A\nrule \"broken\":\n @Event global\n A[1][2] = 3\n",
4554 "broken.opy",
4555 Path::new("."),
4556 &Locale::new("en-US"),
4557 );
4558 assert_eq!(report.compile.status, CompileStatus::Failure);
4559 assert_eq!(
4560 report.compile.failure_class,
4561 Some(CompileFailureClass::Integration)
4562 );
4563 assert_eq!(
4564 report.compile.diagnostics[0].span.as_ref().unwrap().path,
4565 "broken.opy"
4566 );
4567 }
4568
4569 #[test]
4570 fn vertical_slice_preserves_source_files_spans_and_emits_workshop() {
4571 let compiler = Compiler::new().unwrap();
4572 let hir = crate::compile(
4573 "globalvar A\nrule \"issue 35 integration\":\n @Event global\n A = 1\n disableInspector()\n",
4574 "issue-35-integration.opy",
4575 Path::new("."),
4576 )
4577 .unwrap();
4578 let artifact = compiler.compile_hir(&hir).unwrap();
4579 assert_eq!(
4580 artifact
4581 .wir
4582 .files
4583 .get(workshop_rs::source::FileId::from_index(0))
4584 .unwrap()
4585 .path,
4586 "issue-35-integration.opy"
4587 );
4588 let rule = artifact
4589 .wir
4590 .rules
4591 .get(workshop_rs::wir::RuleId::from_index(0))
4592 .unwrap();
4593 assert_eq!(rule.span.unwrap().file.index(), 0);
4594 assert_eq!(rule.name_span.unwrap().start.line, 2);
4595 assert!(artifact.emitted.contains("Disable Inspector Recording;"));
4596 assert_eq!(artifact.catalog_identity.implementation_version, "0.1.16");
4597 }
4598
4599 #[test]
4600 fn stale_catalog_links_fail_explicitly() {
4601 let manifest = Manifest::builtin().unwrap().clone();
4602 let mut stale = manifest;
4603 stale.functions[0].catalog_id = Some("missing-catalog-id".to_string());
4604 let error = cross_check_manifest(&stale, &Catalog::builtin().unwrap()).unwrap_err();
4605 assert_eq!(error.diagnostic.code, "catalog-link-missing");
4606 }
4607
4608 #[test]
4609 fn while_lowering_is_source_attributed() {
4610 let compiler = Compiler::new().unwrap();
4611 let hir = crate::compile(
4612 "rule \"while\":\n @Event global\n while true:\n disableInspector()\n",
4613 "while.opy",
4614 Path::new("."),
4615 )
4616 .unwrap();
4617 let artifact = compiler.compile_hir(&hir).unwrap();
4618 let rule = artifact
4619 .wir
4620 .rules
4621 .get(workshop_rs::wir::RuleId::from_index(0))
4622 .unwrap();
4623 assert!(matches!(
4624 artifact.wir.actions.get(rule.actions[0]),
4625 Some(workshop_rs::wir::Action::While { .. })
4626 ));
4627 assert!(artifact.emitted.contains("While(True);"));
4628 }
4629
4630 #[test]
4631 fn structural_subroutines_lower_to_canonical_wir() {
4632 let compiler = Compiler::new().unwrap();
4633 let hir = crate::compile(
4634 "globalvar score\nsubroutine showStatus\ndef showStatus():\n @Name \"Friendly\"\n @SuppressWarnings unusedVariable\n disableInspector()\nrule \"caller\":\n @Event global\n showStatus()\n",
4635 "structure.opy",
4636 Path::new("."),
4637 )
4638 .unwrap();
4639 let artifact = compiler.compile_hir(&hir).unwrap();
4640 let subroutine = artifact
4641 .wir
4642 .subroutines
4643 .get(workshop_rs::wir::SubroutineId::from_index(0))
4644 .unwrap();
4645 assert_eq!(subroutine.name, "showStatus");
4646 assert_eq!(subroutine.index, 0);
4647 assert_eq!(subroutine.name_span.unwrap().start.line, 2);
4648 assert_eq!(artifact.wir.rules.len(), 2);
4649 let subroutine_rule = artifact
4650 .wir
4651 .rules
4652 .get(workshop_rs::wir::RuleId::from_index(0))
4653 .unwrap();
4654 let workshop_rs::wir::Event::Subroutine(subroutine_id) = subroutine_rule.event else {
4655 panic!("expected a subroutine event");
4656 };
4657 assert_eq!(
4658 artifact.wir.subroutines.get(subroutine_id).unwrap().name,
4659 "showStatus"
4660 );
4661 assert!(matches!(
4662 artifact
4663 .wir
4664 .actions
4665 .get(workshop_rs::wir::ActionId::from_index(1))
4666 .unwrap(),
4667 workshop_rs::wir::Action::CallSubroutine { .. }
4668 ));
4669 assert!(artifact.emitted.contains("Subroutine Friendly"));
4670 }
4671
4672 #[test]
4673 fn player_event_filters_resolve_through_canonical_catalog() {
4674 let compiler = Compiler::new().unwrap();
4675 let hir = crate::compile(
4676 "rule \"joined\":\n @Event playerJoined\n @Team 1\n @Slot 2\n disableInspector()\n",
4677 "filters.opy",
4678 Path::new("."),
4679 )
4680 .unwrap();
4681 let artifact = compiler.compile_hir(&hir).unwrap();
4682 assert!(matches!(
4683 &artifact
4684 .wir
4685 .rules
4686 .get(workshop_rs::wir::RuleId::from_index(0))
4687 .unwrap()
4688 .event,
4689 workshop_rs::wir::Event::Player {
4690 kind: workshop_rs::wir::PlayerEventKind::Joined,
4691 team: workshop_rs::wir::EventTeam::Team1,
4692 target: workshop_rs::wir::EventTarget::Slot(2),
4693 }
4694 ));
4695 assert!(artifact.emitted.contains("Player Joined Match;"));
4696 }
4697
4698 #[test]
4699 fn explicit_indices_are_reserved_before_deterministic_allocation() {
4700 let compiler = Compiler::new().unwrap();
4701 let hir = crate::compile(
4702 "globalvar first\nglobalvar reserved 0\nglobalvar next\nrule \"indices\":\n @Event global\n disableInspector()\n",
4703 "indices.opy",
4704 Path::new("."),
4705 )
4706 .unwrap();
4707 let artifact = compiler.compile_hir(&hir).unwrap();
4708 let by_name = artifact
4709 .wir
4710 .global_variables
4711 .iter()
4712 .map(|variable| (variable.name.as_str(), variable.index))
4713 .collect::<std::collections::BTreeMap<_, _>>();
4714 assert_eq!(
4715 by_name,
4716 std::collections::BTreeMap::from([("first", 1), ("reserved", 0), ("next", 2)])
4717 );
4718 let indices = artifact
4720 .wir
4721 .global_variables
4722 .iter()
4723 .map(|variable| variable.index)
4724 .collect::<Vec<_>>();
4725 assert_eq!(indices, vec![0, 1, 2]);
4726 }
4727
4728 #[test]
4729 fn implicit_default_variables_use_reference_fixed_slots() {
4730 let compiler = Compiler::new().unwrap();
4731 let hir = crate::compile(
4732 r#"
4733globalvar timer
4734globalvar extra 5
4735
4736rule "implicit":
4737 @Event global
4738 A = timer + 1
4739 B = A
4740 B += 2
4741 A[0] = 7
4742 DX = B * A
4743"#,
4744 "implicit.opy",
4745 Path::new("."),
4746 )
4747 .unwrap();
4748 let artifact = compiler.compile_hir(&hir).unwrap();
4749 let globals = artifact
4750 .wir
4751 .global_variables
4752 .iter()
4753 .map(|variable| (variable.name.clone(), variable.index))
4754 .collect::<Vec<_>>();
4755 assert_eq!(
4760 globals,
4761 vec![
4762 ("A".to_string(), 0),
4763 ("B".to_string(), 1),
4764 ("timer".to_string(), 2),
4765 ("extra".to_string(), 5),
4766 ("DX".to_string(), 127),
4767 ]
4768 );
4769 assert!(
4770 artifact
4771 .emitted
4772 .contains("Set Global Variable(A, Add(Global.timer, 1));")
4773 );
4774 assert!(
4775 artifact
4776 .emitted
4777 .contains("Set Global Variable(B, Global.A);")
4778 );
4779 assert!(
4780 artifact
4781 .emitted
4782 .contains("Modify Global Variable(B, Add, 2);")
4783 );
4784 assert!(
4785 artifact
4786 .emitted
4787 .contains("Set Global Variable At Index(A, 0, 7);")
4788 );
4789 assert!(
4790 artifact
4791 .emitted
4792 .contains("Set Global Variable(DX, Multiply(Global.B, Global.A));")
4793 );
4794 }
4795
4796 #[test]
4797 fn implicit_default_variable_slot_collision_is_source_attributed() {
4798 let compiler = Compiler::new().unwrap();
4799 let hir = crate::compile(
4800 "globalvar x 0\nrule \"collision\":\n @Event global\n x = 1\n A = 2\n",
4801 "collision.opy",
4802 Path::new("."),
4803 )
4804 .unwrap();
4805 let error = match compiler.compile_hir(&hir) {
4806 Ok(_) => panic!("slot collision unexpectedly succeeded"),
4807 Err(error) => error,
4808 };
4809 assert_eq!(error.diagnostic.code, "index-collision");
4810 assert_eq!(error.diagnostic.span.unwrap().start.line, 5);
4811 assert!(error.diagnostic.message.contains("'A' and 'x'"));
4812 }
4813
4814 #[test]
4815 fn implicit_default_player_variables_use_independent_reference_slots() {
4816 let compiler = Compiler::new().unwrap();
4817 let hir = crate::compile(
4818 r#"
4819playervar declaredPlayer
4820
4821rule "implicit player variables":
4822 @Event eachPlayer
4823 A = 1
4824 eventPlayer.A = 1
4825 eventPlayer.A += 2
4826 eventPlayer.E = eventPlayer.A
4827 eventPlayer.DX = eventPlayer.E
4828 eventPlayer.declaredPlayer = eventPlayer.A
4829"#,
4830 "implicit-player.opy",
4831 Path::new("."),
4832 )
4833 .unwrap();
4834 let artifact = compiler.compile_hir(&hir).unwrap();
4835 let globals = artifact
4836 .wir
4837 .global_variables
4838 .iter()
4839 .map(|variable| (variable.name.as_str(), variable.index))
4840 .collect::<std::collections::BTreeMap<_, _>>();
4841 let players = artifact
4842 .wir
4843 .player_variables
4844 .iter()
4845 .map(|variable| (variable.name.as_str(), variable.index))
4846 .collect::<std::collections::BTreeMap<_, _>>();
4847 assert_eq!(globals.get("A"), Some(&0));
4848 assert_eq!(players.get("A"), Some(&0));
4849 assert_eq!(players.get("declaredPlayer"), Some(&1));
4850 assert_eq!(players.get("E"), Some(&4));
4851 assert_eq!(players.get("DX"), Some(&127));
4852 assert!(
4853 artifact
4854 .emitted
4855 .contains("Set Player Variable(Event Player, A, 1);")
4856 );
4857 assert!(
4858 artifact
4859 .emitted
4860 .contains("Modify Player Variable(Event Player, A, Add, 2);")
4861 );
4862 assert!(
4863 artifact
4864 .emitted
4865 .contains("Set Player Variable(Event Player, E, (Event Player).A);")
4866 );
4867 }
4868
4869 #[test]
4870 fn implicit_default_player_slot_collision_is_source_attributed() {
4871 let compiler = Compiler::new().unwrap();
4872 let hir = crate::compile(
4873 "playervar declared 0\nrule \"collision\":\n @Event eachPlayer\n eventPlayer.A = 1\n",
4874 "player-collision.opy",
4875 Path::new("."),
4876 )
4877 .unwrap();
4878 let error = match compiler.compile_hir(&hir) {
4879 Ok(_) => panic!("player slot collision unexpectedly succeeded"),
4880 Err(error) => error,
4881 };
4882 assert_eq!(error.diagnostic.code, "index-collision");
4883 assert!(
4884 error
4885 .diagnostic
4886 .message
4887 .contains("player variables 'A' and 'declared'")
4888 );
4889 assert_eq!(error.diagnostic.span.unwrap().start.line, 4);
4890 }
4891
4892 #[test]
4893 fn power_augmented_assignment_lowers_from_source() {
4894 let compiler = Compiler::new().unwrap();
4895 let hir = crate::compile(
4896 "globalvar g\nrule \"power\":\n @Event global\n g = 2\n g **= 3\n",
4897 "power.opy",
4898 Path::new("."),
4899 )
4900 .unwrap();
4901 let artifact = compiler.compile_hir(&hir).unwrap();
4902 assert!(artifact.emitted.contains("Set Global Variable(g, 2);"));
4903 assert!(
4904 artifact
4905 .emitted
4906 .contains("Modify Global Variable(g, Raise To Power, 3);")
4907 );
4908 }
4909
4910 #[test]
4911 fn opy_hex_numbers_are_normalized_at_the_wir_boundary() {
4912 let compiler = Compiler::new().unwrap();
4913 let hir = crate::compile(
4914 "globalvar large = 0x124BC\nglobalvar small = 0x124\nglobalvar scientific = 1e10\n",
4915 "numbers.opy",
4916 Path::new("."),
4917 )
4918 .unwrap();
4919 let artifact = compiler.compile_hir(&hir).unwrap();
4920 assert!(
4921 artifact
4922 .emitted
4923 .contains("Set Global Variable(large, 74940);")
4924 );
4925 assert!(
4926 artifact
4927 .emitted
4928 .contains("Set Global Variable(small, 292);")
4929 );
4930 assert!(
4931 artifact
4932 .emitted
4933 .contains("Set Global Variable(scientific, 1e10);")
4934 );
4935 assert!(!artifact.emitted.contains("0x124BC"));
4936 assert!(!artifact.emitted.contains("0x124"));
4937 }
4938
4939 #[test]
4940 fn unsupported_primitive_lowering_is_stable_and_source_attributed() {
4941 let compiler = Compiler::new().unwrap();
4942 let hir = crate::compile(
4943 "globalvar total\nrule \"negative\":\n @Event global\n total = {\"a\": 1, \"b\": 2}[\"a\"]\n",
4944 "negative.opy",
4945 Path::new("."),
4946 )
4947 .unwrap();
4948 let error = match compiler.compile_hir(&hir) {
4949 Ok(_) => panic!("dict primitive lowering unexpectedly succeeded"),
4950 Err(error) => error,
4951 };
4952 assert_eq!(error.diagnostic.code, "unsupported-integration-surface");
4953 assert!(error.diagnostic.message.contains("dict"));
4954 assert_eq!(error.diagnostic.span.unwrap().start.line, 4);
4955 }
4956
4957 #[test]
4958 fn auto_allocation_fills_free_slots_below_early_explicit_indices() {
4959 let compiler = Compiler::new().unwrap();
4960 let hir = crate::compile(
4961 r#"
4962globalvar reserved 5
4963globalvar auto1
4964globalvar auto2
4965
4966rule "allocation":
4967 @Event global
4968 auto1 = 1
4969 auto2 = 2
4970 B = 3
4971"#,
4972 "allocation.opy",
4973 Path::new("."),
4974 )
4975 .unwrap();
4976 let artifact = compiler.compile_hir(&hir).unwrap();
4977 let by_name = artifact
4978 .wir
4979 .global_variables
4980 .iter()
4981 .map(|variable| (variable.name.clone(), variable.index))
4982 .collect::<std::collections::BTreeMap<_, _>>();
4983 assert_eq!(
4988 by_name,
4989 std::collections::BTreeMap::from([
4990 ("B".to_string(), 1),
4991 ("auto1".to_string(), 0),
4992 ("auto2".to_string(), 2),
4993 ("reserved".to_string(), 5),
4994 ])
4995 );
4996 }
4997
4998 #[test]
4999 fn power_expressions_lower_through_the_canonical_contract() {
5000 let compiler = Compiler::new().unwrap();
5001 let hir = crate::compile(
5002 "globalvar a = [2, 4]\nglobalvar out\nrule \"power\":\n @Event global\n out = a ** 2\n a **= 2\n a[0] **= 2\n",
5003 "power.opy",
5004 Path::new("."),
5005 )
5006 .unwrap();
5007 let artifact = compiler.compile_hir(&hir).unwrap();
5008 assert!(
5009 artifact
5010 .emitted
5011 .contains("Set Global Variable(out, Raise To Power(Global.a, 2));")
5012 );
5013 assert!(
5014 artifact
5015 .emitted
5016 .contains("Modify Global Variable(a, Raise To Power, 2);")
5017 );
5018 assert!(
5019 artifact
5020 .emitted
5021 .contains("Modify Global Variable At Index(a, 0, Raise To Power, 2);")
5022 );
5023 }
5024
5025 #[test]
5026 fn unsupported_rule_metadata_is_explicit_and_source_attributed() {
5027 let compiler = Compiler::new().unwrap();
5028 let hir = crate::compile(
5029 "rule \"metadata\":\n @Event global\n @NewPage \"section\"\n disableInspector()\n",
5030 "metadata.opy",
5031 Path::new("."),
5032 )
5033 .unwrap();
5034 let error = match compiler.compile_hir(&hir) {
5035 Ok(_) => panic!("unsupported metadata unexpectedly succeeded"),
5036 Err(error) => error,
5037 };
5038 assert_eq!(error.diagnostic.code, "unsupported-integration-surface");
5039 assert_eq!(error.diagnostic.span.unwrap().start.line, 3);
5040 }
5041
5042 #[test]
5043 fn issue_40_oracle_fixture_and_wir_lowering_agree() {
5044 let compiler = Compiler::new().unwrap();
5045 let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
5046 .join("../../compatibility/fixtures/synthetic/issue-40-structural");
5047 let source = std::fs::read_to_string(fixture.join("source.opy")).unwrap();
5048 let hir = crate::compile(&source, "source.opy", &fixture).unwrap();
5049 let artifact = compiler.compile_hir(&hir).unwrap();
5050 let oracle: serde_json::Value =
5051 serde_json::from_str(&std::fs::read_to_string(fixture.join("oracle.json")).unwrap())
5052 .unwrap();
5053 let oracle_workshop = oracle["compile"]["workshop"].as_str().unwrap();
5054 let oracle_wir = workshop_rs::parser::parse(
5055 oracle_workshop,
5056 &Catalog::builtin().unwrap(),
5057 &Locale::new("en-US"),
5058 )
5059 .unwrap();
5060 assert!(workshop_rs::roundtrip::equivalent(
5061 &artifact.wir,
5062 &oracle_wir
5063 ));
5064
5065 assert!(oracle_workshop.contains("0: reserved"));
5066 assert!(oracle_workshop.contains("1: first"));
5067 assert!(oracle_workshop.contains("2: explicit"));
5068 assert!(oracle_workshop.contains("3: next"));
5069 assert!(oracle_workshop.contains("0: helper"));
5070 assert!(oracle_workshop.contains("Subroutine;\n helper;"));
5071 assert!(oracle_workshop.contains("Player Joined Match;\n Team 1;\n Slot 2;"));
5072
5073 let indices = artifact
5074 .wir
5075 .global_variables
5076 .iter()
5077 .map(|variable| variable.index)
5078 .collect::<Vec<_>>();
5079 assert_eq!(indices, vec![0, 1, 2, 3]);
5080 assert_eq!(
5081 artifact.wir.subroutines.iter().next().unwrap().name,
5082 "helper"
5083 );
5084 assert!(artifact.emitted.contains("[Source] renamed helper"));
5085 assert!(matches!(
5086 artifact
5087 .wir
5088 .rules
5089 .get(workshop_rs::wir::RuleId::from_index(1))
5090 .unwrap()
5091 .event,
5092 workshop_rs::wir::Event::Player {
5093 kind: workshop_rs::wir::PlayerEventKind::Joined,
5094 team: workshop_rs::wir::EventTeam::Team1,
5095 target: workshop_rs::wir::EventTarget::Slot(2),
5096 }
5097 ));
5098 }
5099
5100 #[test]
5101 fn assignments_and_modifications_lower_to_canonical_wir() {
5102 let compiler = Compiler::new().unwrap();
5103 let hir = crate::compile(
5104 r#"
5105globalvar g1
5106globalvar g2
5107playervar p1
5108playervar p2 = [1, 2, 3]
5109
5110rule "assignments":
5111 @Event eachPlayer
5112 g1 = 10
5113 g1 += 5
5114 g1 -= 2
5115 g1 *= 3
5116 g1 /= 2
5117 g1 %= 4
5118 g2 = [1, 2, 3]
5119 g2[0] = 99
5120 g2[1] += 1
5121 eventPlayer.p1 = 42
5122 eventPlayer.p1 += 8
5123 eventPlayer.p1 *= 2
5124 eventPlayer.p2[2] = 7
5125 eventPlayer.p2[0] -= 3
5126"#,
5127 "assign.opy",
5128 Path::new("."),
5129 )
5130 .unwrap();
5131 let artifact = compiler.compile_hir(&hir).unwrap();
5132 assert!(artifact.emitted.contains("Set Global Variable(g1, 10);"));
5133 assert!(
5134 artifact
5135 .emitted
5136 .contains("Modify Global Variable(g1, Add, 5);")
5137 );
5138 assert!(
5139 artifact
5140 .emitted
5141 .contains("Modify Global Variable(g1, Subtract, 2);")
5142 );
5143 assert!(
5144 artifact
5145 .emitted
5146 .contains("Modify Global Variable(g1, Multiply, 3);")
5147 );
5148 assert!(
5149 artifact
5150 .emitted
5151 .contains("Modify Global Variable(g1, Divide, 2);")
5152 );
5153 assert!(
5154 artifact
5155 .emitted
5156 .contains("Modify Global Variable(g1, Modulo, 4);")
5157 );
5158 assert!(
5159 artifact
5160 .emitted
5161 .contains("Set Global Variable At Index(g2, 0, 99);")
5162 );
5163 assert!(
5164 artifact
5165 .emitted
5166 .contains("Modify Global Variable At Index(g2, 1, Add, 1);")
5167 );
5168 assert!(
5169 artifact
5170 .emitted
5171 .contains("Set Player Variable(Event Player, p1, 42);")
5172 );
5173 assert!(
5174 artifact
5175 .emitted
5176 .contains("Modify Player Variable(Event Player, p1, Add, 8);")
5177 );
5178 assert!(
5179 artifact
5180 .emitted
5181 .contains("Modify Player Variable(Event Player, p1, Multiply, 2);")
5182 );
5183 assert!(
5184 artifact
5185 .emitted
5186 .contains("Set Player Variable At Index((Event Player).p2, 2, 7);")
5187 );
5188 assert!(
5189 artifact
5190 .emitted
5191 .contains("Modify Player Variable At Index((Event Player).p2, 0, Subtract, 3);")
5192 );
5193
5194 let rule = artifact
5198 .wir
5199 .rules
5200 .get(workshop_rs::wir::RuleId::from_index(1))
5201 .unwrap();
5202 let direct = artifact.wir.actions.get(rule.actions[0]).unwrap();
5203 match direct {
5204 workshop_rs::wir::Action::SetGlobalVariable {
5205 span,
5206 target_span,
5207 variable,
5208 ..
5209 } => {
5210 assert_eq!(span.unwrap().start.line, 9);
5211 assert_eq!(target_span.unwrap().start.line, 9);
5212 assert_eq!(
5213 artifact.wir.global_variables.get(*variable).unwrap().name,
5214 "g1"
5215 );
5216 }
5217 other => panic!("expected a direct global assignment, got {other:?}"),
5218 }
5219 let indexed = artifact.wir.actions.get(rule.actions[7]).unwrap();
5220 match indexed {
5221 workshop_rs::wir::Action::Call { span, .. } => {
5222 assert_eq!(span.unwrap().start.line, 16);
5223 }
5224 other => panic!("expected an indexed assignment call, got {other:?}"),
5225 }
5226 }
5227
5228 #[test]
5229 fn expressions_and_values_lower_to_canonical_wir() {
5230 let compiler = Compiler::new().unwrap();
5231 let hir = crate::compile(
5232 r#"
5233enum Consts:
5234 BASE
5235
5236globalvar total
5237globalvar arr = [1, 2, 3]
5238globalvar pos = vect(1, 2, 3)
5239
5240rule "expressions":
5241 @Event global
5242 @Condition total == 0
5243 @Condition not (pos == vect(0, 0, 0))
5244 @Condition 2 in arr
5245 total = Consts.BASE + arr[1] * 2 - (10 / 2) + (5 % 2)
5246 print("Total: {}".format(total))
5247 debug(pos)
5248"#,
5249 "expr.opy",
5250 Path::new("."),
5251 )
5252 .unwrap();
5253 let artifact = compiler.compile_hir(&hir).unwrap();
5254 assert!(artifact.emitted.contains("Global.total == 0;"));
5255 assert!(artifact.emitted.contains("Global.pos != Vector(0, 0, 0);"));
5258 assert!(
5259 artifact
5260 .emitted
5261 .contains("Array Contains(Global.arr, 2) == True;")
5262 );
5263 assert!(
5264 artifact
5265 .emitted
5266 .contains("Custom String(\"Total: {0}\", Global.total)")
5267 );
5268 }
5269
5270 #[test]
5271 fn pass_is_supported_as_source_level_noop() {
5272 let compiler = Compiler::new().unwrap();
5273 let hir = crate::compile(
5274 r#"
5275subroutine emptySub
5276
5277def emptySub():
5278 pass
5279
5280rule "empty rule":
5281 @Event global
5282 pass
5283"#,
5284 "pass.opy",
5285 Path::new("."),
5286 )
5287 .unwrap();
5288 let artifact = compiler.compile_hir(&hir).unwrap();
5289 let rule0 = artifact
5290 .wir
5291 .rules
5292 .get(workshop_rs::wir::RuleId::from_index(0))
5293 .unwrap();
5294 assert!(rule0.actions.is_empty());
5295 let rule1 = artifact
5296 .wir
5297 .rules
5298 .get(workshop_rs::wir::RuleId::from_index(1))
5299 .unwrap();
5300 assert!(rule1.actions.is_empty());
5301 }
5302
5303 #[test]
5304 fn variable_initializers_synthesize_initialize_rules() {
5305 let compiler = Compiler::new().unwrap();
5306 let hir = crate::compile(
5307 r#"
5308globalvar j = 5
5309globalvar h = 0
5310globalvar k = 0.0
5311playervar p = 7
5312playervar q = 0
5313
5314rule "main":
5315 @Event global
5316 disableInspector()
5317"#,
5318 "init.opy",
5319 Path::new("."),
5320 )
5321 .unwrap();
5322 let artifact = compiler.compile_hir(&hir).unwrap();
5323 assert_eq!(
5324 artifact
5325 .wir
5326 .rules
5327 .get(workshop_rs::wir::RuleId::from_index(0))
5328 .unwrap()
5329 .name,
5330 "Initialize global variables"
5331 );
5332 assert_eq!(
5333 artifact
5334 .wir
5335 .rules
5336 .get(workshop_rs::wir::RuleId::from_index(1))
5337 .unwrap()
5338 .name,
5339 "Initialize player variables"
5340 );
5341 assert_eq!(
5342 artifact
5343 .wir
5344 .rules
5345 .get(workshop_rs::wir::RuleId::from_index(2))
5346 .unwrap()
5347 .name,
5348 "main"
5349 );
5350 assert!(artifact.emitted.contains("Set Global Variable(j, 5);"));
5351 assert!(artifact.emitted.contains("Set Global Variable(k, 0.0);"));
5352 assert!(!artifact.emitted.contains("Set Global Variable(h,"));
5353 assert!(
5354 artifact
5355 .emitted
5356 .contains("Set Player Variable(Event Player, p, 7);")
5357 );
5358 assert!(
5359 !artifact
5360 .emitted
5361 .contains("Set Player Variable(Event Player, q,")
5362 );
5363 }
5364
5365 #[test]
5366 fn settings_lower_through_workshop_owned_emission() {
5367 let compiler = Compiler::new().unwrap();
5368 let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
5369 .join("../../compatibility/fixtures/synthetic/settings");
5370 let source = std::fs::read_to_string(fixture.join("source.opy")).unwrap();
5371 let hir = crate::compile(&source, "source.opy", &fixture).unwrap();
5372 let artifact = compiler.compile_hir(&hir).unwrap();
5373 let oracle: serde_json::Value =
5374 serde_json::from_str(&std::fs::read_to_string(fixture.join("oracle.json")).unwrap())
5375 .unwrap();
5376 let expected = oracle["compile"]["workshop"]
5377 .as_str()
5378 .unwrap()
5379 .split("\n\nrule")
5380 .next()
5381 .unwrap();
5382 let actual = artifact.emitted.split("\n\nrule").next().unwrap();
5383 let oracle_wir = workshop_rs::parser::parse(
5384 oracle["compile"]["workshop"].as_str().unwrap(),
5385 &Catalog::builtin().unwrap(),
5386 &Locale::new("en-US"),
5387 )
5388 .unwrap();
5389 assert!(workshop_rs::roundtrip::equivalent(
5390 &artifact.wir,
5391 &oracle_wir
5392 ));
5393 assert_eq!(
5394 normalize_workshop_structural_whitespace(actual),
5395 normalize_workshop_structural_whitespace(expected)
5396 );
5397 }
5398
5399 #[test]
5400 fn unsupported_locale_has_no_fabricated_source_span() {
5401 let compiler = Compiler::new().unwrap();
5402 let hir = crate::compile(
5403 "#!translations en\nrule \"r\":\n @Event global\n pass\n",
5404 "locale.opy",
5405 Path::new("."),
5406 )
5407 .unwrap();
5408 let error = match compiler.compile_hir_with_locale(&hir, &Locale::new("xx-XX")) {
5409 Ok(_) => panic!("unsupported locale unexpectedly compiled"),
5410 Err(error) => error,
5411 };
5412 assert_eq!(error.diagnostic.code, "locale-unsupported");
5413 assert_eq!(error.diagnostic.span, None);
5414 }
5415
5416 #[test]
5417 fn locale_selection_emits_catalog_localized_workshop() {
5418 let compiler = Compiler::new().unwrap();
5419 let hir = crate::compile(
5420 "rule \"locale\":\n @Event global\n disableInspector()\n",
5421 "locale.opy",
5422 Path::new("."),
5423 )
5424 .unwrap();
5425 let artifact = compiler
5426 .compile_hir_with_locale(&hir, &Locale::new("zh-CN"))
5427 .unwrap();
5428 assert!(artifact.emitted.contains("规则 (\"locale\")"));
5429 assert!(artifact.emitted.contains("禁用查看器录制"));
5430 }
5431
5432 #[test]
5433 fn unsupported_backend_directives_fail_at_their_source_anchor() {
5434 let compiler = Compiler::new().unwrap();
5435 let hir = crate::compile(
5436 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
5437 "directives.opy",
5438 Path::new("."),
5439 )
5440 .unwrap();
5441 let error = match compiler.compile_hir(&hir) {
5442 Ok(_) => panic!("backend directive unexpectedly compiled"),
5443 Err(error) => error,
5444 };
5445 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
5446 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
5447 }
5448
5449 #[test]
5450 fn optimizer_directives_remain_non_blocking_presentation_controls() {
5451 let compiler = Compiler::new().unwrap();
5452 let hir = crate::compile(
5453 "#!disableOptimizations\nrule \"r\":\n @Event global\n pass\n",
5454 "optimization.opy",
5455 Path::new("."),
5456 )
5457 .unwrap();
5458 compiler.compile_hir(&hir).unwrap();
5459 }
5460
5461 #[test]
5462 fn replacement_directive_records_are_checked_even_if_final_state_is_restored() {
5463 let compiler = Compiler::new().unwrap();
5464 let mut hir = crate::compile(
5465 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
5466 "directives.opy",
5467 Path::new("."),
5468 )
5469 .unwrap();
5470 hir.preprocessing.replacements.clear();
5471 let error = match compiler.compile_hir(&hir) {
5472 Ok(_) => panic!("replacement directive unexpectedly compiled"),
5473 Err(error) => error,
5474 };
5475 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
5476 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
5477 }
5478
5479 #[test]
5480 fn active_replacement_state_is_checked_without_directive_history() {
5481 let compiler = Compiler::new().unwrap();
5482 let mut hir = crate::compile(
5483 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
5484 "directives.opy",
5485 Path::new("."),
5486 )
5487 .unwrap();
5488 hir.preprocessing.directives.clear();
5489 hir.preprocessing.replacements[0].span = None;
5490 let error = match compiler.compile_hir(&hir) {
5491 Ok(_) => panic!("active replacement state unexpectedly compiled"),
5492 Err(error) => error,
5493 };
5494 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
5495 assert_eq!(error.diagnostic.span, None);
5496 }
5497
5498 #[test]
5499 fn post_compile_hook_receives_exact_emitted_workshop() {
5500 let compiler = Compiler::new().unwrap();
5501 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/macros");
5502 let source = "#!postCompileHook \"hook.js\"\n\nrule \"setup\":\n pass\n";
5503 let artifact = compiler
5504 .compile_source_with_locale(source, "hook.opy", &root, &Locale::new("en-US"))
5505 .unwrap();
5506 assert!(artifact.emitted.contains("rule (\"setup\")"));
5507 assert!(artifact.final_output.contains("rule (\"transformed\")"));
5508 assert_ne!(artifact.final_output, artifact.emitted);
5509 }
5510
5511 #[test]
5512 fn post_compile_hook_failure_keeps_script_provenance_and_directive_anchor() {
5513 let compiler = Compiler::new().unwrap();
5514 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/macros");
5515 let source = "#!postCompileHook \"hook-boom.js\"\n\nrule \"setup\":\n pass\n";
5516 let error = match compiler.compile_source_with_locale(
5517 source,
5518 "hook.opy",
5519 &root,
5520 &Locale::new("en-US"),
5521 ) {
5522 Ok(_) => panic!("failing post-compile hook unexpectedly compiled"),
5523 Err(error) => error,
5524 };
5525 assert_eq!(error.diagnostic.code, "post-compile-hook");
5526 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
5527 let script = error.diagnostic.script.unwrap();
5528 assert_eq!(script.source_name.as_deref(), Some("hook-boom.js"));
5529 assert_eq!(script.line, Some(1));
5530 assert!(script.stack.unwrap().contains("hook-boom.js:1"));
5531 }
5532
5533 fn normalize_workshop_structural_whitespace(text: &str) -> String {
5534 let mut normalized = String::with_capacity(text.len());
5535 let mut quote = None;
5536 let mut escaped = false;
5537 for character in text.chars() {
5538 if let Some(delimiter) = quote {
5539 normalized.push(character);
5540 if escaped {
5541 escaped = false;
5542 } else if character == '\\' {
5543 escaped = true;
5544 } else if character == delimiter {
5545 quote = None;
5546 }
5547 } else if matches!(character, '\"' | '\'') {
5548 quote = Some(character);
5549 normalized.push(character);
5550 } else if !character.is_whitespace() {
5551 normalized.push(character);
5552 }
5553 }
5554 normalized
5555 }
5556
5557 #[test]
5558 fn settings_whitespace_normalization_preserves_quoted_values() {
5559 assert_ne!(
5560 normalize_workshop_structural_whitespace("Description: \"a b\""),
5561 normalize_workshop_structural_whitespace("Description: \"ab\"")
5562 );
5563 }
5564}