1use std::collections::{HashMap, HashSet};
29
30use crate::hir::types::{
31 Annotation as HirAnnotation, AnnotationArg as HirAnnotationArg, Declaration, Define,
32 DictEntry as HirDictEntry, Event, Expr as HirExpr, Generator, IfBranch, PROTOCOL_VERSION,
33 Position, PreprocessingState, Program as HirProgram, Protocol, Rule, RuleEntry,
34 Settings as HirSettings, SettingsNode as HirSettingsNode, SourceFile, Span as HirSpan,
35 Stmt as HirStmt, SwitchArm as HirSwitchArm, default_var_index,
36};
37
38use crate::cst::{self, CallArg, Decl, Expr, RuleEntry as CstRuleEntry, Stmt, TopLevel};
39use crate::diag::{OpyError, OpyResult, Span};
40use crate::manifest::{
41 Function, FunctionContext, FunctionKind, Manifest, Param, ParamDefault, ReceiverCategory,
42};
43use workshop_rs::catalog::{Catalog, Locale};
44
45const PROTOCOL_NAME: &str = "wright/opy-hir";
47
48#[derive(Clone, Copy, PartialEq, Eq)]
51enum CallPosition {
52 Statement,
54 Value,
56 ForIterable,
58 LambdaArgument,
60}
61
62struct Lowerer {
64 global_declarations: HashMap<String, usize>,
65 player_declarations: HashMap<String, usize>,
66 subroutine_declarations: HashMap<String, usize>,
67 subroutine_definitions: Vec<(String, usize)>,
68 macro_declarations: HashMap<String, usize>,
69 enums: HashMap<String, Vec<String>>,
70 enum_declarations: HashMap<String, usize>,
71 locals: Vec<String>,
72 current_order: usize,
73 allow_dict_literal: bool,
74 manifest: &'static Manifest,
76 catalog: Catalog,
78 errors: Vec<OpyError>,
79}
80
81pub fn lower(
83 program: &cst::Program,
84 files: Vec<SourceFile>,
85 defines: Vec<Define>,
86) -> OpyResult<HirProgram> {
87 lower_with_preprocessing(program, files, defines, &PreprocessingState::default())
88}
89
90pub fn lower_with_preprocessing(
91 program: &cst::Program,
92 files: Vec<SourceFile>,
93 defines: Vec<Define>,
94 preprocessing: &PreprocessingState,
95) -> OpyResult<HirProgram> {
96 let manifest = match Manifest::builtin() {
97 Ok(manifest) => manifest,
98 Err(error) => {
99 return Err(OpyError::new(
100 "manifest-error",
101 format!("cannot load the OPY semantic compatibility manifest: {error}"),
102 ));
103 }
104 };
105 let catalog = match Catalog::builtin() {
106 Ok(catalog) => catalog,
107 Err(error) => {
108 return Err(OpyError::new(
109 "catalog-error",
110 format!("cannot load the Workshop catalog: {error}"),
111 ));
112 }
113 };
114 let mut lowerer = Lowerer {
115 global_declarations: HashMap::new(),
116 player_declarations: HashMap::new(),
117 subroutine_declarations: HashMap::new(),
118 subroutine_definitions: Vec::new(),
119 macro_declarations: HashMap::new(),
120 enums: HashMap::new(),
121 enum_declarations: HashMap::new(),
122 locals: Vec::new(),
123 current_order: 0,
124 allow_dict_literal: false,
125 manifest,
126 catalog,
127 errors: Vec::new(),
128 };
129 lowerer.collect_symbols(program);
130
131 let mut declarations = Vec::new();
132 let mut rules = Vec::new();
133 let mut implicit_subroutines = HashSet::new();
134 for (order, item) in program.top_level.iter().enumerate() {
135 lowerer.current_order = order;
136 match item {
137 TopLevel::Declaration(decl) => match decl {
138 Decl::GlobalVariable {
139 name,
140 index,
141 span,
142 name_span,
143 initializer,
144 } => declarations.push(Declaration::GlobalVariable {
145 name: name.clone(),
146 index: *index,
147 span: Some(span.into()),
148 name_span: Some(name_span.into()),
149 initializer: lowerer.initializer(initializer.as_ref()),
150 }),
151 Decl::PlayerVariable {
152 name,
153 index,
154 span,
155 name_span,
156 initializer,
157 } => declarations.push(Declaration::PlayerVariable {
158 name: name.clone(),
159 index: *index,
160 span: Some(span.into()),
161 name_span: Some(name_span.into()),
162 initializer: lowerer.initializer(initializer.as_ref()),
163 }),
164 Decl::Subroutine {
165 name,
166 span,
167 name_span,
168 } => declarations.push(Declaration::Subroutine {
169 name: name.clone(),
170 index: None,
171 span: Some(span.into()),
172 name_span: Some(name_span.into()),
173 }),
174 Decl::Enum { .. } => {
175 }
178 Decl::Macro {
179 name,
180 args,
181 body,
182 span,
183 } => {
184 let lowered_body = lowerer.lower_macro_body(body, args);
185 declarations.push(Declaration::Macro {
186 name: name.clone(),
187 args: args.clone(),
188 span: Some(span.into()),
189 body: lowered_body,
190 });
191 }
192 },
193 TopLevel::Rule(CstRuleEntry::Rule(rule)) => rules.push(RuleEntry::Rule(
194 lowerer.lower_rule(rule, files.as_slice(), preprocessing)?,
195 )),
196 TopLevel::Rule(CstRuleEntry::SubroutineDef {
197 name,
198 presentation_name,
199 span,
200 name_span,
201 body,
202 annotations,
203 rule_prefix,
204 }) => {
205 if !lowerer.subroutine_declarations.contains_key(name)
206 && implicit_subroutines.insert(name.clone())
207 {
208 declarations.push(Declaration::Subroutine {
209 name: name.clone(),
210 index: None,
211 span: Some(span.into()),
212 name_span: Some(name_span.into()),
213 });
214 }
215 let base_name = presentation_name
216 .as_deref()
217 .map(str::to_string)
218 .unwrap_or_else(|| name.clone());
219 let generated_name = render_rule_name(
220 &base_name,
221 rule_prefix.as_deref(),
222 false,
223 *span,
224 files.as_slice(),
225 preprocessing,
226 )?;
227 rules.push(RuleEntry::SubroutineDef {
228 kind: "subroutineDef".to_string(),
229 name: generated_name,
230 source_name: name.clone(),
231 span: Some(span.into()),
232 name_span: Some(name_span.into()),
233 body: lowerer.lower_block(body, &[], false, true, false),
234 annotations: lower_annotations(annotations),
235 });
236 }
237 }
238 }
239
240 if !lowerer.errors.is_empty() {
241 return Err(lowerer.errors.swap_remove(0));
242 }
243
244 Ok(HirProgram {
245 protocol: Protocol {
246 name: PROTOCOL_NAME.to_string(),
247 version: PROTOCOL_VERSION.to_string(),
248 },
249 generator: Generator {
250 name: crate::LANGUAGE_NAME.to_string(),
251 version: crate::LANGUAGE_VERSION.to_string(),
252 frontend: crate::LANGUAGE_NAME.to_string(),
253 },
254 files,
255 defines,
256 declarations,
257 rules,
258 settings: program.settings.as_ref().map(lower_settings),
259 preprocessing: preprocessing.clone(),
260 })
261}
262
263fn prefixed_rule_name(name: &str, prefix: Option<&str>, delimiter: bool) -> String {
264 match prefix {
265 Some(prefix) if !prefix.is_empty() && !delimiter && !name.is_empty() => {
266 format!("[{prefix}] {name}")
267 }
268 _ => name.to_string(),
269 }
270}
271
272#[derive(Clone, Debug)]
273enum TemplateValue {
274 String(String),
275 Bool(bool),
276}
277
278fn render_rule_name(
279 name: &str,
280 prefix: Option<&str>,
281 delimiter: bool,
282 span: Span,
283 files: &[SourceFile],
284 preprocessing: &PreprocessingState,
285) -> OpyResult<String> {
286 let Some(template) = preprocessing
287 .rule_prefix_template
288 .as_ref()
289 .map(|value| value.value.as_str())
290 else {
291 return Ok(prefixed_rule_name(name, prefix, delimiter));
292 };
293 let (file, path) = rule_file_parts(span.file, files);
294 let prefix = prefix.unwrap_or_default();
295 let values = [
296 ("$rule", TemplateValue::String(name.to_string())),
297 ("$prefix", TemplateValue::String(prefix.to_string())),
298 ("$file", TemplateValue::String(file.clone())),
299 ("$path", TemplateValue::String(path.clone())),
300 ("$isDelimiter", TemplateValue::Bool(delimiter)),
301 ("$prefixTitle", TemplateValue::String(title_case(prefix))),
302 ("$prefixUpper", TemplateValue::String(prefix.to_uppercase())),
303 ("$prefixLower", TemplateValue::String(prefix.to_lowercase())),
304 ("$fileTitle", TemplateValue::String(title_case(&file))),
305 ("$fileUpper", TemplateValue::String(file.to_uppercase())),
306 ("$fileLower", TemplateValue::String(file.to_lowercase())),
307 ("$pathTitle", TemplateValue::String(title_case(&path))),
308 ("$pathUpper", TemplateValue::String(path.to_uppercase())),
309 ("$pathLower", TemplateValue::String(path.to_lowercase())),
310 ];
311 evaluate_template(template, &values).map_err(|message| {
312 OpyError::at(
313 "rule-prefix-template-invalid",
314 format!("could not resolve rule prefix template: {message}"),
315 span,
316 )
317 })
318}
319
320fn rule_file_parts(file_id: u32, files: &[SourceFile]) -> (String, String) {
321 let path = files
322 .iter()
323 .find(|file| file.id == file_id)
324 .map(|file| file.path.replace('\\', "/"))
325 .unwrap_or_default();
326 let without_extension = path
327 .strip_suffix(".opy")
328 .or_else(|| path.strip_suffix(".OPY"))
329 .unwrap_or(&path)
330 .to_string();
331 let file = without_extension
332 .rsplit('/')
333 .next()
334 .unwrap_or_default()
335 .to_string();
336 (file, without_extension)
337}
338
339fn title_case(value: &str) -> String {
340 let mut result = String::with_capacity(value.len());
341 let mut capitalize = true;
342 for ch in value.chars() {
343 if ch == '_' {
344 result.push(' ');
345 capitalize = true;
346 } else if capitalize && ch.is_ascii_alphabetic() {
347 result.push(ch.to_ascii_uppercase());
348 capitalize = false;
349 } else {
350 result.push(ch);
351 if !ch.is_whitespace() && ch != '/' {
352 capitalize = false;
353 }
354 }
355 if ch == '/' || ch.is_whitespace() {
356 capitalize = true;
357 }
358 }
359 result
360}
361
362fn evaluate_template(template: &str, values: &[(&str, TemplateValue)]) -> Result<String, String> {
363 if let Some((then_value, condition, else_value)) = split_conditional(template) {
364 let branch = if evaluate_condition(condition, values)? {
365 then_value
366 } else {
367 else_value
368 };
369 return evaluate_string(branch, values);
370 }
371 evaluate_string(template, values)
372}
373
374fn split_conditional(value: &str) -> Option<(&str, &str, &str)> {
375 let mut quote = None;
376 let mut depth = 0usize;
377 let mut if_start = None;
378 let mut else_start = None;
379 for (index, ch) in value.char_indices() {
380 match (ch, quote) {
381 ('"' | '\'', None) => quote = Some(ch),
382 (ch, Some(current)) if ch == current => quote = None,
383 ('{', None) => depth += 1,
384 ('}', None) => depth = depth.saturating_sub(1),
385 _ => {}
386 }
387 if quote.is_none() && depth == 0 {
388 if value[index..].starts_with(" if ") && if_start.is_none() {
389 if_start = Some(index);
390 } else if value[index..].starts_with(" else ") && else_start.is_none() {
391 else_start = Some(index);
392 }
393 }
394 }
395 let (Some(if_start), Some(else_start)) = (if_start, else_start) else {
396 return None;
397 };
398 Some((
399 value[..if_start].trim(),
400 value[if_start + 4..else_start].trim(),
401 value[else_start + 6..].trim(),
402 ))
403}
404
405fn evaluate_condition(value: &str, values: &[(&str, TemplateValue)]) -> Result<bool, String> {
406 let value = value.trim();
407 if let Some(rest) = value.strip_prefix("not ") {
408 return Ok(!evaluate_condition(rest, values)?);
409 }
410 if let Some((left, right)) = value.split_once(" or ") {
411 return Ok(evaluate_condition(left, values)? || evaluate_condition(right, values)?);
412 }
413 if let Some((left, right)) = value.split_once(" and ") {
414 return Ok(evaluate_condition(left, values)? && evaluate_condition(right, values)?);
415 }
416 match lookup_template_value(value, values)? {
417 TemplateValue::Bool(value) => Ok(value),
418 TemplateValue::String(value) => Ok(!value.is_empty()),
419 }
420}
421
422fn evaluate_string(value: &str, values: &[(&str, TemplateValue)]) -> Result<String, String> {
423 let value = value.trim();
424 if let Some(body) = value
425 .strip_prefix("f\"")
426 .and_then(|body| body.strip_suffix('"'))
427 {
428 return interpolate_fstring(body, values);
429 }
430 if let Some(body) = value
431 .strip_prefix("f'")
432 .and_then(|body| body.strip_suffix('\''))
433 {
434 return interpolate_fstring(body, values);
435 }
436 if value.len() >= 2
437 && ((value.starts_with('"') && value.ends_with('"'))
438 || (value.starts_with('\'') && value.ends_with('\'')))
439 {
440 return Ok(value[1..value.len() - 1].to_string());
441 }
442 match lookup_template_value(value, values)? {
443 TemplateValue::String(value) => Ok(value),
444 TemplateValue::Bool(value) => Ok(value.to_string()),
445 }
446}
447
448fn interpolate_fstring(body: &str, values: &[(&str, TemplateValue)]) -> Result<String, String> {
449 let mut result = String::new();
450 let mut remaining = body;
451 while let Some(start) = remaining.find('{') {
452 result.push_str(&remaining[..start]);
453 let end = remaining[start + 1..]
454 .find('}')
455 .ok_or_else(|| "unterminated interpolation".to_string())?
456 + start
457 + 1;
458 result.push_str(&evaluate_string(&remaining[start + 1..end], values)?);
459 remaining = &remaining[end + 1..];
460 }
461 result.push_str(remaining);
462 Ok(result)
463}
464
465fn lookup_template_value(
466 value: &str,
467 values: &[(&str, TemplateValue)],
468) -> Result<TemplateValue, String> {
469 let value = value.trim();
470 let (base, mut methods) = value
471 .split_once('.')
472 .map_or((value, ""), |(base, methods)| (base, methods));
473 let mut result = values
474 .iter()
475 .find(|(name, _)| *name == base)
476 .map(|(_, value)| value.clone())
477 .ok_or_else(|| format!("unsupported expression '{value}'"))?;
478 while !methods.is_empty() {
479 let (method, rest) = methods
480 .split_once('.')
481 .map_or((methods, ""), |(method, rest)| (method, rest));
482 if method == "upper()" {
483 result = TemplateValue::String(as_string(&result).to_uppercase());
484 } else if method == "lower()" {
485 result = TemplateValue::String(as_string(&result).to_lowercase());
486 } else if let Some(args) = method
487 .strip_prefix("replace(")
488 .and_then(|v| v.strip_suffix(')'))
489 {
490 let (from, to) = args
491 .split_once(',')
492 .ok_or_else(|| "replace expects two arguments".to_string())?;
493 let from = unquote_template_arg(from.trim())?;
494 let to = unquote_template_arg(to.trim())?;
495 result = TemplateValue::String(as_string(&result).replace(&from, &to));
496 } else {
497 return Err(format!("unsupported method '{method}'"));
498 }
499 methods = rest;
500 }
501 Ok(result)
502}
503
504fn as_string(value: &TemplateValue) -> String {
505 match value {
506 TemplateValue::String(value) => value.clone(),
507 TemplateValue::Bool(value) => value.to_string(),
508 }
509}
510
511fn unquote_template_arg(value: &str) -> Result<String, String> {
512 if value.len() >= 2
513 && ((value.starts_with('"') && value.ends_with('"'))
514 || (value.starts_with('\'') && value.ends_with('\'')))
515 {
516 Ok(value[1..value.len() - 1].to_string())
517 } else {
518 Err(format!("expected a quoted string argument, got '{value}'"))
519 }
520}
521
522fn lower_annotations(annotations: &[cst::Annotation]) -> Vec<HirAnnotation> {
523 annotations
524 .iter()
525 .map(|annotation| HirAnnotation {
526 name: annotation.name.clone(),
527 args: annotation
528 .args
529 .iter()
530 .map(|arg| HirAnnotationArg {
531 text: arg.text.clone(),
532 span: Some(arg.span.into()),
533 })
534 .collect(),
535 span: Some(annotation.span.into()),
536 })
537 .collect()
538}
539
540fn lower_settings(settings: &cst::Settings) -> HirSettings {
542 HirSettings {
543 span: Some(settings.span.into()),
544 children: settings.children.iter().map(lower_settings_node).collect(),
545 }
546}
547
548fn lower_settings_node(node: &cst::SettingsNode) -> HirSettingsNode {
549 match node {
550 cst::SettingsNode::Group {
551 name,
552 children,
553 span,
554 } => HirSettingsNode::Group {
555 name: name.clone(),
556 children: children.iter().map(lower_settings_node).collect(),
557 span: Some((*span).into()),
558 },
559 cst::SettingsNode::Number { name, value, span } => HirSettingsNode::Number {
560 name: name.clone(),
561 value: *value,
562 span: Some((*span).into()),
563 },
564 cst::SettingsNode::Bool { name, value, span } => HirSettingsNode::Bool {
565 name: name.clone(),
566 value: *value,
567 span: Some((*span).into()),
568 },
569 cst::SettingsNode::String { name, value, span } => HirSettingsNode::String {
570 name: name.clone(),
571 value: value.clone(),
572 span: Some((*span).into()),
573 },
574 cst::SettingsNode::List {
575 name,
576 elements,
577 span,
578 } => HirSettingsNode::List {
579 name: name.clone(),
580 elements: elements
581 .iter()
582 .map(|element| crate::hir::types::SettingsListElement {
583 value: element.value.clone(),
584 span: Some(element.span.into()),
585 })
586 .collect(),
587 span: Some((*span).into()),
588 },
589 }
590}
591
592impl Lowerer {
593 fn collect_symbols(&mut self, program: &cst::Program) {
594 for (order, item) in program.top_level.iter().enumerate() {
595 let TopLevel::Declaration(decl) = item else {
596 continue;
597 };
598 match decl {
599 Decl::GlobalVariable { name, span, .. } => {
600 let duplicate = self.global_declarations.contains_key(name);
601 self.global_declarations
602 .entry(name.clone())
603 .or_insert(order);
604 if duplicate {
605 self.error_at(
606 "duplicate-declaration",
607 format!("duplicate global variable '{name}'"),
608 *span,
609 );
610 }
611 }
612 Decl::PlayerVariable { name, span, .. } => {
613 let duplicate = self.player_declarations.contains_key(name);
614 self.player_declarations
615 .entry(name.clone())
616 .or_insert(order);
617 if duplicate {
618 self.error_at(
619 "duplicate-declaration",
620 format!("duplicate player variable '{name}'"),
621 *span,
622 );
623 }
624 }
625 Decl::Subroutine { name, span, .. } => {
626 let duplicate = self.subroutine_declarations.contains_key(name);
627 self.subroutine_declarations
628 .entry(name.clone())
629 .or_insert(order);
630 if duplicate {
631 self.error_at(
632 "duplicate-declaration",
633 format!("duplicate subroutine '{name}'"),
634 *span,
635 );
636 }
637 }
638 Decl::Enum { name, members, .. } => {
639 self.enum_declarations.entry(name.clone()).or_insert(order);
640 self.enums.entry(name.clone()).or_insert_with(|| {
641 members.iter().map(|(member, _)| member.clone()).collect()
642 });
643 }
644 Decl::Macro { name, .. } => {
645 self.macro_declarations.entry(name.clone()).or_insert(order);
646 }
647 }
648 }
649 for (order, item) in program.top_level.iter().enumerate() {
650 let TopLevel::Rule(CstRuleEntry::SubroutineDef { name, span, .. }) = item else {
651 continue;
652 };
653 if self
654 .subroutine_definitions
655 .iter()
656 .any(|(defined, _)| defined == name)
657 {
658 self.error_at(
659 "duplicate-definition",
660 format!("duplicate subroutine definition '{name}'"),
661 *span,
662 );
663 }
664 self.subroutine_definitions.push((name.clone(), order));
665 }
666 }
667
668 fn subroutine_visible(&self, name: &str) -> bool {
669 self.subroutine_declarations
670 .get(name)
671 .is_some_and(|order| *order <= self.current_order)
672 || self
673 .subroutine_definitions
674 .iter()
675 .any(|(definition, order)| definition == name && *order <= self.current_order)
676 }
677
678 fn global_visible(&self, name: &str) -> bool {
679 self.global_declarations
680 .get(name)
681 .is_some_and(|order| *order <= self.current_order)
682 }
683
684 fn player_visible(&self, name: &str) -> bool {
685 self.player_declarations
686 .get(name)
687 .is_some_and(|order| *order <= self.current_order)
688 }
689
690 fn macro_visible(&self, name: &str) -> bool {
691 self.macro_declarations
692 .get(name)
693 .is_some_and(|order| *order <= self.current_order)
694 }
695
696 fn enum_visible(&self, name: &str) -> bool {
697 self.enum_declarations
698 .get(name)
699 .is_some_and(|order| *order <= self.current_order)
700 }
701
702 fn initializer(&mut self, initializer: Option<&Expr>) -> Option<Box<HirExpr>> {
706 let initializer = initializer?;
707 let lowered = self.lower_expr(initializer, &[], CallPosition::Value);
708 match &lowered {
709 HirExpr::Number { text, .. } if text == "0" => None,
710 other => Some(Box::new(other.clone())),
711 }
712 }
713
714 fn lower_rule(
715 &mut self,
716 rule: &cst::Rule,
717 files: &[SourceFile],
718 preprocessing: &PreprocessingState,
719 ) -> OpyResult<Rule> {
720 let conditions = rule
721 .conditions
722 .iter()
723 .map(|condition| self.lower_expr(condition, &[], CallPosition::Value))
724 .collect();
725 let actions = self.lower_block(&rule.actions, &[], false, true, false);
726 Ok(Rule {
727 name: render_rule_name(
728 &rule.name,
729 rule.rule_prefix.as_deref(),
730 rule.delimiter,
731 rule.span,
732 files,
733 preprocessing,
734 )?,
735 span: Some(rule.span.into()),
736 name_span: Some(rule.name_span.into()),
737 disabled: rule.disabled,
738 delimiter: rule.delimiter,
739 new_page: rule.new_page.clone(),
740 annotations: lower_annotations(&rule.annotations),
741 event: Event {
742 name: rule.event.name.clone(),
743 args: rule
744 .event
745 .args
746 .iter()
747 .map(|arg| self.lower_expr(arg, &[], CallPosition::Value))
748 .collect(),
749 span: Some(rule.event.span.into()),
750 },
751 conditions,
752 actions,
753 })
754 }
755
756 fn lower_block(
758 &mut self,
759 stmts: &[Stmt],
760 macro_params: &[String],
761 breakable: bool,
762 allow_do_while: bool,
763 loopable: bool,
764 ) -> Vec<HirStmt> {
765 stmts
766 .iter()
767 .enumerate()
768 .map(|(index, stmt)| {
769 if matches!(stmt, Stmt::DoWhile { .. })
770 && (!allow_do_while
771 || stmts[..index]
772 .iter()
773 .any(|previous| !matches!(previous, Stmt::Pass { .. })))
774 {
775 self.error_at(
776 "do-while-placement",
777 "do-while must be at the beginning of a rule, subroutine, or do-while body; only pass statements may precede it".to_string(),
778 stmt.span(),
779 );
780 }
781 self.lower_stmt(stmt, macro_params, breakable, loopable)
782 })
783 .collect()
784 }
785
786 fn lower_stmt(
787 &mut self,
788 stmt: &Stmt,
789 macro_params: &[String],
790 breakable: bool,
791 loopable: bool,
792 ) -> HirStmt {
793 match stmt {
794 Stmt::Expr { expr, span } => {
795 if let Expr::Call { name, args, .. } = expr {
798 if self.subroutine_visible(name) && args.is_empty() {
799 return HirStmt::CallSubroutine {
800 name: name.clone(),
801 span: Some(span.into()),
802 };
803 }
804 }
805 HirStmt::Expr {
808 expr: Box::new(self.lower_expr(expr, macro_params, CallPosition::Statement)),
809 span: Some(span.into()),
810 }
811 }
812 Stmt::Assign {
813 target,
814 value,
815 span,
816 } => HirStmt::Assign {
817 target: Box::new(self.lower_expr(target, macro_params, CallPosition::Value)),
818 value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)),
819 span: Some(span.into()),
820 },
821 Stmt::If {
822 branches,
823 r#else,
824 span,
825 } => HirStmt::If {
826 branches: branches
827 .iter()
828 .map(|branch| IfBranch {
829 condition: Box::new(self.lower_expr(
830 &branch.condition,
831 macro_params,
832 CallPosition::Value,
833 )),
834 body: self.lower_block(
835 &branch.body,
836 macro_params,
837 breakable,
838 false,
839 loopable,
840 ),
841 })
842 .collect(),
843 r#else: r#else
844 .as_ref()
845 .map(|body| self.lower_block(body, macro_params, breakable, false, loopable)),
846 span: Some(span.into()),
847 },
848 Stmt::For {
849 variable,
850 iterable,
851 body,
852 span,
853 } => {
854 let iterable_position = if matches!(iterable, Expr::Call { name, .. } if name == "range")
858 {
859 CallPosition::ForIterable
860 } else {
861 self.error_at(
862 "invalid-iterable",
863 "for-loop iterable must be a range(...) call".to_string(),
864 iterable.span(),
865 );
866 CallPosition::Value
867 };
868 HirStmt::For {
869 variable: Box::new(self.lower_for_binder(variable, macro_params)),
870 iterable: Box::new(self.lower_expr(iterable, macro_params, iterable_position)),
871 body: self.lower_block(body, macro_params, true, false, true),
872 span: Some(span.into()),
873 }
874 }
875 Stmt::While {
876 condition,
877 body,
878 span,
879 } => HirStmt::While {
880 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
881 body: self.lower_block(body, macro_params, true, false, true),
882 span: Some(span.into()),
883 },
884 Stmt::DoWhile {
885 condition,
886 body,
887 span,
888 } => HirStmt::DoWhile {
889 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
890 body: self.lower_block(body, macro_params, true, true, true),
891 span: Some(span.into()),
892 },
893 Stmt::Switch { value, arms, span } => HirStmt::Switch {
894 value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)),
895 arms: arms
896 .iter()
897 .map(|arm| match arm {
898 cst::SwitchArm::Case { value, body, span } => HirSwitchArm::Case {
899 value: Box::new(self.lower_expr(
900 value,
901 macro_params,
902 CallPosition::Value,
903 )),
904 body: self.lower_block(body, macro_params, true, false, loopable),
905 span: Some((*span).into()),
906 },
907 cst::SwitchArm::Default { body, span } => HirSwitchArm::Default {
908 body: self.lower_block(body, macro_params, true, false, loopable),
909 span: Some((*span).into()),
910 },
911 })
912 .collect(),
913 span: Some(span.into()),
914 },
915 Stmt::Delete { target, span } => HirStmt::Delete {
916 target: Box::new(self.lower_expr(target, macro_params, CallPosition::Value)),
917 span: Some(span.into()),
918 },
919 Stmt::Break { span } => {
920 if !breakable {
921 self.error_at(
922 "break-context",
923 "break is only valid inside a switch or loop".to_string(),
924 *span,
925 );
926 }
927 HirStmt::Break {
928 span: Some(span.into()),
929 }
930 }
931 Stmt::Return { span } => HirStmt::Return {
932 span: Some(span.into()),
933 },
934 Stmt::Continue { span } => {
935 if !loopable {
936 self.error_at(
937 "continue-context",
938 "continue is only valid inside a loop".to_string(),
939 *span,
940 );
941 }
942 HirStmt::Continue {
943 span: Some(span.into()),
944 }
945 }
946 Stmt::Goto {
947 label,
948 offset,
949 rule_start,
950 span,
951 } => HirStmt::Goto {
952 label: label.clone(),
953 offset: offset.as_ref().map(|offset| {
954 Box::new(self.lower_expr(offset, macro_params, CallPosition::Value))
955 }),
956 rule_start: *rule_start,
957 span: Some(span.into()),
958 },
959 Stmt::Label { name, span } => HirStmt::Label {
960 name: name.clone(),
961 span: Some(span.into()),
962 },
963 Stmt::Pass { span } => HirStmt::Pass {
964 span: Some(span.into()),
965 },
966 }
967 }
968
969 fn lower_for_binder(&mut self, variable: &Expr, macro_params: &[String]) -> HirExpr {
970 if let Expr::Member {
971 receiver,
972 member,
973 member_span,
974 span,
975 } = variable
976 {
977 if !default_var_index(member).is_some() && !self.player_visible(member) {
978 self.error_at(
979 "unknown-identifier",
980 format!("unknown player variable '{member}'"),
981 *member_span,
982 );
983 return HirExpr::Null { span: None };
984 }
985 return HirExpr::PlayerVar {
986 player: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
987 name: member.clone(),
988 member_span: Some((*member_span).into()),
989 span: Some((*span).into()),
990 };
991 }
992 self.lower_expr(variable, macro_params, CallPosition::Value)
993 }
994
995 fn lower_macro_body(&mut self, body: &[Stmt], params: &[String]) -> Vec<HirStmt> {
996 self.lower_block(body, params, false, false, false)
997 }
998
999 fn lower_expr(
1000 &mut self,
1001 expr: &Expr,
1002 macro_params: &[String],
1003 position: CallPosition,
1004 ) -> HirExpr {
1005 match expr {
1006 Expr::Number { value, text, span } => HirExpr::Number {
1007 value: *value,
1008 text: text.clone(),
1009 span: Some(span.into()),
1010 },
1011 Expr::String { value, span } => HirExpr::String {
1012 value: value.clone(),
1013 span: Some(span.into()),
1014 },
1015 Expr::Bool { value, span } => HirExpr::Bool {
1016 value: *value,
1017 span: Some(span.into()),
1018 },
1019 Expr::Null { span } => HirExpr::Null {
1020 span: Some(span.into()),
1021 },
1022 Expr::Array { elements, span } => HirExpr::Array {
1023 elements: elements
1024 .iter()
1025 .map(|element| self.lower_expr(element, macro_params, CallPosition::Value))
1026 .collect(),
1027 span: Some(span.into()),
1028 },
1029 Expr::Dict { entries, span } => {
1030 if !self.allow_dict_literal {
1031 self.error_at(
1032 "dict-access",
1033 "dictionary literals must be accessed by a key".to_string(),
1034 *span,
1035 );
1036 return HirExpr::Null { span: None };
1037 }
1038 HirExpr::Dict {
1039 entries: entries
1040 .iter()
1041 .map(|entry| HirDictEntry {
1042 key: Box::new(self.lower_expr(
1043 &entry.key,
1044 macro_params,
1045 CallPosition::Value,
1046 )),
1047 value: Box::new(self.lower_expr(
1048 &entry.value,
1049 macro_params,
1050 CallPosition::Value,
1051 )),
1052 span: Some(entry.span.into()),
1053 })
1054 .collect(),
1055 span: Some(span.into()),
1056 }
1057 }
1058 Expr::Comprehension {
1059 element,
1060 variable,
1061 variable_span,
1062 index,
1063 iterable,
1064 condition,
1065 span,
1066 } => {
1067 let iterable = self.lower_expr(iterable, macro_params, CallPosition::Value);
1068 let previous = std::mem::take(&mut self.locals);
1069 self.locals.push(variable.clone());
1070 if let Some((index, _)) = index {
1071 self.locals.push(index.clone());
1072 }
1073 let element = self.lower_expr(element, macro_params, CallPosition::Value);
1074 let condition = condition.as_ref().map(|condition| {
1075 Box::new(self.lower_expr(condition, macro_params, CallPosition::Value))
1076 });
1077 self.locals = previous;
1078 HirExpr::Comprehension {
1079 element: Box::new(element),
1080 variable: variable.clone(),
1081 variable_span: Some(variable_span.into()),
1082 index: index.as_ref().map(|(name, _)| name.clone()),
1083 index_span: index.as_ref().map(|(_, span)| (*span).into()),
1084 iterable: Box::new(iterable),
1085 condition,
1086 span: Some(span.into()),
1087 }
1088 }
1089 Expr::Lambda { params, body, span } => {
1090 if position != CallPosition::LambdaArgument {
1091 self.error_at(
1092 "lambda-context",
1093 "lambda expressions are only valid as array operation arguments"
1094 .to_string(),
1095 *span,
1096 );
1097 return HirExpr::Null { span: None };
1098 }
1099 let previous = std::mem::take(&mut self.locals);
1100 self.locals = params.iter().map(|(name, _)| name.clone()).collect();
1101 let body = self.lower_expr(body, macro_params, CallPosition::Value);
1102 self.locals = previous;
1103 HirExpr::Lambda {
1104 params: params.iter().map(|(name, _)| name.clone()).collect(),
1105 param_spans: params
1106 .iter()
1107 .map(|(_, span)| Some((*span).into()))
1108 .collect(),
1109 body: Box::new(body),
1110 span: Some(span.into()),
1111 }
1112 }
1113 Expr::StringModifier {
1114 modifier,
1115 value,
1116 format_text,
1117 interpolations,
1118 span,
1119 } => {
1120 if *modifier == 'f' {
1121 if let Some(format_text) = format_text {
1122 if !interpolations.is_empty() {
1123 return HirExpr::Format {
1124 text: format_text.clone(),
1125 args: interpolations
1126 .iter()
1127 .map(|expr| {
1128 self.lower_expr(expr, macro_params, CallPosition::Value)
1129 })
1130 .collect(),
1131 span: Some(span.into()),
1132 };
1133 }
1134 return HirExpr::String {
1135 value: format_text.clone(),
1136 span: Some(span.into()),
1137 };
1138 }
1139 }
1140 HirExpr::StringModifier {
1141 modifier: modifier.to_string(),
1142 value: value.clone(),
1143 span: Some(span.into()),
1144 }
1145 }
1146 Expr::Name { name, span } => self.lower_name(name, *span, macro_params),
1147 Expr::Type { name, args, span } => HirExpr::Type {
1148 name: name.clone(),
1149 args: args
1150 .iter()
1151 .map(|arg| self.lower_expr(arg, macro_params, CallPosition::Value))
1152 .collect(),
1153 span: Some(span.into()),
1154 },
1155 Expr::Member {
1156 receiver,
1157 member,
1158 member_span,
1159 span,
1160 } => self.lower_member(receiver, member, *member_span, *span, macro_params),
1161 Expr::Index { array, index, span } => {
1162 let previous = self.allow_dict_literal;
1163 self.allow_dict_literal = true;
1164 let array = self.lower_expr(array, macro_params, CallPosition::Value);
1165 self.allow_dict_literal = previous;
1166 HirExpr::Index {
1167 array: Box::new(array),
1168 index: Box::new(self.lower_expr(index, macro_params, CallPosition::Value)),
1169 span: Some(span.into()),
1170 }
1171 }
1172 Expr::Call { name, args, span } => {
1173 self.lower_call(name, args, *span, macro_params, position)
1174 }
1175 Expr::ReceiverCall {
1176 receiver,
1177 name,
1178 args,
1179 span,
1180 } => self.lower_receiver_call(receiver, name, args, *span, macro_params, position),
1181 Expr::Binary {
1182 op,
1183 left,
1184 right,
1185 span,
1186 } => HirExpr::Binary {
1187 op: op.clone(),
1188 left: Box::new(self.lower_expr(left, macro_params, CallPosition::Value)),
1189 right: Box::new(self.lower_expr(right, macro_params, CallPosition::Value)),
1190 span: Some(span.into()),
1191 },
1192 Expr::Conditional {
1193 then_value,
1194 condition,
1195 else_value,
1196 span,
1197 } => HirExpr::Conditional {
1198 then_value: Box::new(self.lower_expr(
1199 then_value,
1200 macro_params,
1201 CallPosition::Value,
1202 )),
1203 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
1204 else_value: Box::new(self.lower_expr(
1205 else_value,
1206 macro_params,
1207 CallPosition::Value,
1208 )),
1209 span: Some((*span).into()),
1210 },
1211 Expr::Unary { op, operand, span } => HirExpr::Unary {
1212 op: op.clone(),
1213 operand: Box::new(self.lower_expr(operand, macro_params, CallPosition::Value)),
1214 span: Some(span.into()),
1215 },
1216 }
1217 }
1218
1219 fn lower_name(&mut self, name: &str, span: Span, macro_params: &[String]) -> HirExpr {
1220 if macro_params.iter().any(|param| param == name) {
1221 return HirExpr::MacroParam {
1222 name: name.to_string(),
1223 span: Some(span.into()),
1224 };
1225 }
1226 if self.locals.iter().any(|local| local == name) {
1227 return HirExpr::Local {
1228 name: name.to_string(),
1229 span: Some(span.into()),
1230 };
1231 }
1232 if let Some(player) = context_player_expr(name, Some(span)) {
1233 return player;
1234 }
1235 match name {
1236 "RULE_CONDITION" | "ruleCondition" => HirExpr::Call {
1237 name: "ruleCondition".to_string(),
1238 args: Vec::new(),
1239 span: Some(span.into()),
1240 },
1241 "eventAbility" | "eventDamage" => HirExpr::Call {
1242 name: name.to_string(),
1243 args: Vec::new(),
1244 span: Some(span.into()),
1245 },
1246 _ if self.global_visible(name) => HirExpr::GlobalVar {
1247 name: name.to_string(),
1248 span: Some(span.into()),
1249 },
1250 _ if self.player_visible(name) => HirExpr::PlayerVar {
1251 player: Box::new(HirExpr::EventPlayer { span: None }),
1252 name: name.to_string(),
1253 member_span: None,
1254 span: Some(span.into()),
1255 },
1256 _ if self.enum_visible(name) => {
1257 self.error_at(
1258 "enum-type-without-member",
1259 format!("enum type '{name}' must be used with a member (e.g. {name}.MEMBER)"),
1260 span,
1261 );
1262 HirExpr::Null { span: None }
1263 }
1264 _ if default_var_index(name).is_some() => HirExpr::GlobalVar {
1271 name: name.to_string(),
1272 span: Some(span.into()),
1273 },
1274 _ => {
1275 self.error_at(
1276 "unknown-identifier",
1277 format!("unknown identifier '{name}'"),
1278 span,
1279 );
1280 HirExpr::Null { span: None }
1281 }
1282 }
1283 }
1284
1285 fn lower_member(
1286 &mut self,
1287 receiver: &Expr,
1288 member: &str,
1289 member_span: Span,
1290 span: Span,
1291 macro_params: &[String],
1292 ) -> HirExpr {
1293 if let Expr::Name { name, .. } = receiver {
1294 if self.enum_visible(name) {
1296 let members = self.enums.get(name).expect("enum span and members agree");
1297 return match members.iter().position(|candidate| candidate == member) {
1298 Some(index) => HirExpr::Number {
1299 value: index as f64,
1300 text: index.to_string(),
1301 span: Some(span.into()),
1302 },
1303 None => {
1304 self.error_at(
1305 "unknown-enum-member",
1306 format!("enum '{name}' has no member '{member}'"),
1307 span,
1308 );
1309 HirExpr::Null { span: None }
1310 }
1311 };
1312 }
1313 let catalog_domain = match name.as_str() {
1319 "Clip" => "Clipping",
1320 _ => name.as_str(),
1321 };
1322 if self.manifest.domain_identity(name)
1323 || (name == "Clip" && self.manifest.domain_identity(catalog_domain))
1324 {
1325 let locale = Locale::new("en-US");
1326 let catalog_member = match (name.as_str(), member) {
1327 ("Clip", "NONE") => "DO_NOT_CLIP",
1328 ("SpecVisibility", "ALWAYS") => "VISIBLE_ALWAYS",
1329 ("SpecVisibility", "NEVER") => "VISIBLE_NEVER",
1330 ("EffectReeval", "VISIBILITY_POSITION_AND_RADIUS") => {
1331 "VISIBLE_TO_POSITION_AND_RADIUS"
1332 }
1333 ("Hero", "MCCREE") => "CASSIDY",
1334 ("Hero", "HAMMOND") => "WRECKING_BALL",
1335 _ => member,
1336 };
1337 let canonical_member = self
1338 .catalog
1339 .enum_domain(catalog_domain)
1340 .and_then(|domain| {
1341 domain
1342 .members
1343 .iter()
1344 .find(|candidate| candidate.member == catalog_member)
1345 .map(|candidate| candidate.member.clone())
1346 })
1347 .or_else(|| {
1348 if name == "Team" && member.parse::<u32>().is_ok() {
1349 self.catalog
1350 .resolve_enum_member(name, &locale, &format!("{name} {member}"))
1351 .map(|(_, member)| member)
1352 } else {
1353 None
1354 }
1355 });
1356 let Some(canonical_member) = canonical_member else {
1357 self.error_at(
1358 "unknown-enum-member",
1359 format!("enum '{name}' has no member '{member}'"),
1360 span,
1361 );
1362 return HirExpr::Null { span: None };
1363 };
1364 return HirExpr::Enum {
1365 value_type: catalog_domain.to_string(),
1366 value: canonical_member,
1367 span: Some(span.into()),
1368 };
1369 }
1370 if matches!(
1372 name.as_str(),
1373 "eventPlayer" | "hostPlayer" | "localPlayer" | "attacker" | "victim"
1374 ) {
1375 if !default_var_index(member).is_some() && !self.player_visible(member) {
1376 self.error_at(
1377 "unknown-identifier",
1378 format!("unknown player variable '{member}'"),
1379 member_span,
1380 );
1381 return HirExpr::Null { span: None };
1382 }
1383 let player = context_player_expr(
1384 name,
1385 (!matches!(name.as_str(), "eventPlayer" | "hostPlayer"))
1386 .then_some(receiver.span()),
1387 )
1388 .expect("context-player receiver name is exhaustive");
1389 return HirExpr::PlayerVar {
1390 player: Box::new(player),
1391 name: member.to_string(),
1392 member_span: Some(member_span.into()),
1393 span: Some(span.into()),
1394 };
1395 }
1396 if name == "random" {
1398 self.error_at(
1399 "unsupported-member",
1400 format!("module member '{name}.{member}' must be called"),
1401 span,
1402 );
1403 return HirExpr::Null { span: None };
1404 }
1405 if self.player_visible(member) {
1406 return HirExpr::PlayerVar {
1407 player: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1408 name: member.to_string(),
1409 member_span: Some(member_span.into()),
1410 span: Some(span.into()),
1411 };
1412 }
1413 if self.global_visible(name)
1418 || self.player_visible(name)
1419 || default_var_index(name).is_some()
1420 {
1421 let receiver = if default_var_index(name).is_some() {
1422 HirExpr::GlobalVar {
1423 name: name.to_string(),
1424 span: Some(receiver.span().into()),
1425 }
1426 } else {
1427 self.lower_name(name, receiver.span(), &[])
1428 };
1429 return HirExpr::Member {
1430 receiver: Box::new(receiver),
1431 member: member.to_string(),
1432 member_span: Some(member_span.into()),
1433 span: Some(span.into()),
1434 };
1435 }
1436 }
1437 if self.player_visible(member) {
1438 return HirExpr::PlayerVar {
1439 player: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1440 name: member.to_string(),
1441 member_span: Some(member_span.into()),
1442 span: Some(span.into()),
1443 };
1444 }
1445 if matches!(member, "x" | "y" | "z") {
1446 return HirExpr::Member {
1447 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1448 member: member.to_string(),
1449 member_span: Some(member_span.into()),
1450 span: Some(span.into()),
1451 };
1452 }
1453 self.error_at(
1454 "unsupported-member",
1455 "unsupported member access on this expression".to_string(),
1456 span,
1457 );
1458 HirExpr::Null { span: None }
1459 }
1460
1461 fn lower_call(
1462 &mut self,
1463 name: &str,
1464 args: &[cst::CallArg],
1465 span: Span,
1466 macro_params: &[String],
1467 position: CallPosition,
1468 ) -> HirExpr {
1469 if name == "createWorkshopSetting" {
1470 return self.lower_workshop_setting(args, span, macro_params);
1471 }
1472 if !self.macro_visible(name) && !self.subroutine_visible(name) && name != "sorted" {
1475 match self.manifest.resolve_function(name) {
1476 Some(entry) => self.check_call_position(name, entry, position, span),
1477 None => {
1478 let (code, message) = match position {
1479 CallPosition::Statement => {
1480 ("unknown-action", format!("unknown action '{name}'"))
1481 }
1482 CallPosition::Value => ("unknown-value", format!("unknown value '{name}'")),
1483 CallPosition::ForIterable => (
1484 "invalid-iterable",
1485 format!("for-loop iterable '{name}' must be a range(...) call"),
1486 ),
1487 CallPosition::LambdaArgument => {
1488 ("unknown-value", format!("unknown value '{name}'"))
1489 }
1490 };
1491 self.error_at(code, message, span);
1492 }
1493 }
1494 }
1495 match name {
1496 "sorted" => HirExpr::Call {
1497 name: name.to_string(),
1498 args: self.lower_arg_values_with_lambda(args, macro_params, |index, arg| {
1499 index == 1 || arg.keyword.as_ref().is_some_and(|(name, _)| name == "key")
1500 }),
1501 span: Some(span.into()),
1502 },
1503 "vect" => {
1504 let (bound, _) = match self.manifest.resolve_function(name) {
1509 Some(entry) => self.bind_args(entry, args, macro_params),
1510 None => (self.lower_arg_values(args, macro_params), None),
1511 };
1512 if bound.len() < 3 {
1513 self.error_at(
1514 "vect-arity",
1515 format!(
1516 "vect() expects 3 arguments (x, y, z) but got {}",
1517 args.len()
1518 ),
1519 span,
1520 );
1521 return HirExpr::Null { span: None };
1522 }
1523 HirExpr::Vector {
1524 x: Box::new(bound[0].clone()),
1525 y: Box::new(bound[1].clone()),
1526 z: Box::new(bound[2].clone()),
1527 span: Some(span.into()),
1528 }
1529 }
1530 _ => {
1531 if self.macro_visible(name) {
1532 for arg in args {
1536 if let Some((keyword, span)) = &arg.keyword {
1537 self.error_at(
1538 "keyword-unsupported",
1539 format!(
1540 "macro '{name}' does not accept keyword \
1541 arguments ('{keyword}')"
1542 ),
1543 *span,
1544 );
1545 }
1546 }
1547 return HirExpr::MacroCall {
1548 name: name.to_string(),
1549 args: self.lower_arg_values(args, macro_params),
1550 span: Some(span.into()),
1551 };
1552 }
1553 match self.manifest.resolve_function(name) {
1554 Some(entry) => {
1555 if self.subroutine_visible(name) {
1559 return HirExpr::Call {
1560 name: name.to_string(),
1561 args: self.lower_arg_values(args, macro_params),
1562 span: Some(span.into()),
1563 };
1564 }
1565 let (bound, selector) = self.bind_args(entry, args, macro_params);
1566 let (call_name, bound) =
1567 self.resolve_contextual_domain(entry, bound, selector.as_deref());
1568 HirExpr::Call {
1569 name: call_name,
1570 args: bound,
1571 span: Some(span.into()),
1572 }
1573 }
1574 None => HirExpr::Call {
1575 name: name.to_string(),
1576 args: self.lower_arg_values(args, macro_params),
1577 span: Some(span.into()),
1578 },
1579 }
1580 }
1581 }
1582 }
1583
1584 fn lower_workshop_setting(
1585 &mut self,
1586 args: &[cst::CallArg],
1587 span: Span,
1588 macro_params: &[String],
1589 ) -> HirExpr {
1590 if !(4..=5).contains(&args.len()) {
1591 self.error_at(
1592 "invalid-arity",
1593 format!(
1594 "function 'createWorkshopSetting' takes 4 or 5 arguments, received {}",
1595 args.len()
1596 ),
1597 span,
1598 );
1599 return HirExpr::Null { span: None };
1600 }
1601 for arg in args {
1602 if let Some((keyword, keyword_span)) = &arg.keyword {
1603 self.error_at(
1604 "keyword-unsupported",
1605 format!(
1606 "function 'createWorkshopSetting' does not accept keyword arguments ('{keyword}')"
1607 ),
1608 *keyword_span,
1609 );
1610 }
1611 }
1612
1613 let setting_type = match &args[0].value {
1614 Expr::Type {
1615 name,
1616 args: type_args,
1617 span: type_span,
1618 } => HirExpr::Type {
1619 name: name.clone(),
1620 args: type_args
1621 .iter()
1622 .map(|arg| self.lower_expr(arg, macro_params, CallPosition::Value))
1623 .collect(),
1624 span: Some((*type_span).into()),
1625 },
1626 Expr::Name {
1627 name,
1628 span: type_span,
1629 } if matches!(name.as_str(), "bool" | "int" | "float") => HirExpr::Type {
1630 name: name.clone(),
1631 args: Vec::new(),
1632 span: Some((*type_span).into()),
1633 },
1634 other => {
1635 self.error_at(
1636 "invalid-argument",
1637 "argument 1 of 'createWorkshopSetting' must be a setting type".to_string(),
1638 other.span(),
1639 );
1640 HirExpr::Null { span: None }
1641 }
1642 };
1643 let mut lowered = Vec::with_capacity(5);
1644 lowered.push(setting_type);
1645 lowered.extend(
1646 args[1..]
1647 .iter()
1648 .map(|arg| self.lower_expr(&arg.value, macro_params, CallPosition::Value)),
1649 );
1650 if args.len() == 4 {
1651 lowered.push(HirExpr::Number {
1652 value: 0.0,
1653 text: "0".to_string(),
1654 span: None,
1655 });
1656 }
1657 HirExpr::Call {
1658 name: "createWorkshopSetting".to_string(),
1659 args: lowered,
1660 span: Some(span.into()),
1661 }
1662 }
1663
1664 fn lower_arg_values(&mut self, args: &[cst::CallArg], macro_params: &[String]) -> Vec<HirExpr> {
1667 self.lower_arg_values_with_lambda(args, macro_params, |_, _| false)
1668 }
1669
1670 fn lower_arg_values_with_lambda(
1671 &mut self,
1672 args: &[cst::CallArg],
1673 macro_params: &[String],
1674 allows_lambda: impl Fn(usize, &cst::CallArg) -> bool,
1675 ) -> Vec<HirExpr> {
1676 args.iter()
1677 .enumerate()
1678 .map(|(index, arg)| {
1679 let position = if allows_lambda(index, arg) {
1680 CallPosition::LambdaArgument
1681 } else {
1682 CallPosition::Value
1683 };
1684 self.lower_expr(&arg.value, macro_params, position)
1685 })
1686 .collect()
1687 }
1688
1689 fn bind_args(
1701 &mut self,
1702 entry: &Function,
1703 args: &[cst::CallArg],
1704 macro_params: &[String],
1705 ) -> (Vec<HirExpr>, Option<String>) {
1706 let mut slots: Vec<Option<HirExpr>> = vec![None; entry.params.len()];
1707 let mut selector = None;
1708 let mut has_keyword = false;
1709 let mut binding_error = false;
1710 let contextual = entry.contextual_domain.as_ref();
1711
1712 let mut by_spelling: HashMap<&str, usize> = HashMap::new();
1716 for (index, param) in entry.params.iter().enumerate() {
1717 by_spelling.insert(param.name.as_str(), index);
1718 for alternate in ¶m.alternate_names {
1719 by_spelling.insert(alternate.as_str(), index);
1720 }
1721 }
1722
1723 for (arg_index, arg) in args.iter().enumerate() {
1724 match &arg.keyword {
1725 Some((keyword, name_span)) => {
1726 if !entry.keyword_args {
1727 binding_error = true;
1728 self.error_at(
1729 "keyword-unsupported",
1730 format!(
1731 "function '{}' does not accept keyword arguments ('{keyword}')",
1732 entry.id
1733 ),
1734 *name_span,
1735 );
1736 continue;
1737 }
1738 has_keyword = true;
1739 match by_spelling.get(keyword.as_str()) {
1740 None => {
1741 binding_error = true;
1742 self.error_at(
1743 "unknown-keyword",
1744 format!(
1745 "unknown keyword argument '{keyword}' for function '{}'",
1746 entry.id
1747 ),
1748 *name_span,
1749 );
1750 }
1751 Some(&index) => {
1752 let param = &entry.params[index];
1753 if param.positional_only {
1754 binding_error = true;
1755 self.error_at(
1756 "unknown-keyword",
1757 format!(
1758 "parameter '{}' of '{}' cannot be bound by keyword",
1759 param.name, entry.id
1760 ),
1761 *name_span,
1762 );
1763 } else if slots[index].is_some() {
1764 binding_error = true;
1765 self.error_at(
1766 "duplicate-argument",
1767 format!(
1768 "argument '{}' of function '{}' is defined twice",
1769 keyword, entry.id
1770 ),
1771 *name_span,
1772 );
1773 } else {
1774 slots[index] = Some(self.lower_call_arg_value(
1775 entry,
1776 index,
1777 arg,
1778 macro_params,
1779 ));
1780 if contextual.is_some_and(|c| c.by == param.name) {
1781 selector = Some(keyword.clone());
1782 }
1783 }
1784 }
1785 }
1786 }
1787 None => {
1788 if has_keyword && entry.contextual_domain.is_none() {
1794 binding_error = true;
1795 self.error_at(
1796 "positional-after-keyword",
1797 format!(
1798 "cannot use positional arguments after keyword \
1799 arguments in call to '{}'",
1800 entry.id
1801 ),
1802 arg.value.span(),
1803 );
1804 }
1805 let index = arg_index;
1809 if index < entry.params.len() {
1810 let param = &entry.params[index];
1811 if param.keyword_only {
1812 binding_error = true;
1813 self.error_at(
1814 "keyword-required",
1815 format!(
1816 "argument {} of '{}' must be passed as a keyword \
1817 (name = value; accepted names: {})",
1818 index + 1,
1819 entry.id,
1820 keyword_spellings(param).join(", ")
1821 ),
1822 arg.value.span(),
1823 );
1824 }
1825 if slots[index].is_none() {
1826 slots[index] =
1827 Some(self.lower_call_arg_value(entry, index, arg, macro_params));
1828 }
1829 } else {
1830 self.lower_expr(&arg.value, macro_params, CallPosition::Value);
1831 }
1832 }
1833 }
1834 }
1835
1836 if !binding_error && args.len() > entry.params.len() {
1842 self.check_arity(entry, args.len(), arg_span(args));
1843 }
1844
1845 let mut bound: Vec<HirExpr> = Vec::with_capacity(entry.params.len());
1849 for (index, param) in entry.params.iter().enumerate() {
1850 match &slots[index] {
1851 Some(value) => bound.push(value.clone()),
1852 None => match ¶m.default {
1853 Some(ParamDefault::Call { call }) => {
1854 bound.push(HirExpr::Call {
1855 name: call.clone(),
1856 args: Vec::new(),
1857 span: None,
1858 });
1859 }
1860 Some(ParamDefault::EnumMember(member)) => {
1861 let domain = param.domain.clone().unwrap_or_default();
1862 bound.push(HirExpr::Enum {
1863 value_type: domain,
1864 value: member.clone(),
1865 span: None,
1866 });
1867 }
1868 Some(ParamDefault::Number(number)) => {
1869 bound.push(HirExpr::Number {
1870 value: *number,
1871 text: format!("{number}"),
1872 span: None,
1873 });
1874 }
1875 None if param.optional => {
1876 }
1879 None => {
1880 self.error_at(
1881 "missing-argument",
1882 format!(
1883 "missing argument '{}' for function '{}'",
1884 param.name, entry.id
1885 ),
1886 arg_span(args),
1887 );
1888 bound.push(HirExpr::Null { span: None });
1889 }
1890 },
1891 }
1892 }
1893
1894 for (index, param) in entry.params.iter().enumerate() {
1897 if !param.variable {
1898 continue;
1899 }
1900 if let Some(Some(value)) = slots.get(index) {
1901 if !matches!(value, HirExpr::GlobalVar { .. } | HirExpr::PlayerVar { .. }) {
1902 self.error_at(
1903 "invalid-argument",
1904 format!(
1905 "argument {} of '{}' must be a variable (globalvar or \
1906 playervar)",
1907 index + 1,
1908 entry.id
1909 ),
1910 arg_span(args),
1911 );
1912 }
1913 }
1914 }
1915
1916 (bound, selector)
1917 }
1918
1919 fn lower_call_arg_value(
1926 &mut self,
1927 entry: &Function,
1928 param_index: usize,
1929 arg: &cst::CallArg,
1930 macro_params: &[String],
1931 ) -> HirExpr {
1932 if let Some(contextual) = &entry.contextual_domain {
1933 let is_contextual = entry.params[param_index]
1934 .domain
1935 .as_deref()
1936 .is_some_and(|domain| domain == contextual.domain);
1937 if is_contextual {
1938 if let Expr::Member {
1939 receiver,
1940 member,
1941 span,
1942 ..
1943 } = &arg.value
1944 {
1945 if let Expr::Name { name, .. } = receiver.as_ref() {
1946 if name == &contextual.domain {
1947 return HirExpr::Enum {
1948 value_type: contextual.domain.clone(),
1949 value: member.clone(),
1950 span: Some((*span).into()),
1951 };
1952 }
1953 }
1954 }
1955 }
1956 }
1957 self.lower_expr(&arg.value, macro_params, CallPosition::Value)
1958 }
1959
1960 fn resolve_contextual_domain(
1970 &mut self,
1971 entry: &Function,
1972 mut bound: Vec<HirExpr>,
1973 selector: Option<&str>,
1974 ) -> (String, Vec<HirExpr>) {
1975 let Some(contextual) = &entry.contextual_domain else {
1976 return (entry.id.clone(), bound);
1977 };
1978 let Some(contextual_param) = entry
1979 .params
1980 .iter()
1981 .position(|param| param.domain.as_deref() == Some(contextual.domain.as_str()))
1982 else {
1983 return (entry.id.clone(), bound);
1984 };
1985 let HirExpr::Enum {
1989 value_type,
1990 value,
1991 span: value_span,
1992 } = &bound[contextual_param]
1993 else {
1994 return (entry.id.clone(), bound);
1995 };
1996 if value_type != &contextual.domain {
1997 return (entry.id.clone(), bound);
1998 }
1999 let Some(keyword) = selector else {
2000 return (entry.id.clone(), bound);
2001 };
2002 let Some(option) = contextual.options.get(keyword) else {
2003 return (entry.id.clone(), bound);
2004 };
2005 bound[contextual_param] = HirExpr::Enum {
2006 value_type: option.domain.clone(),
2007 value: value.clone(),
2008 span: *value_span,
2009 };
2010 (option.target.clone(), bound)
2011 }
2012
2013 fn lower_receiver_call(
2014 &mut self,
2015 receiver: &Expr,
2016 name: &str,
2017 args: &[cst::CallArg],
2018 span: Span,
2019 macro_params: &[String],
2020 position: CallPosition,
2021 ) -> HirExpr {
2022 if matches!(name, "map" | "filter" | "all" | "any") {
2023 let lowered = HirExpr::ReceiverCall {
2024 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
2025 name: name.to_string(),
2026 args: self.lower_arg_values_with_lambda(args, macro_params, |index, _| index == 0),
2027 span: Some(span.into()),
2028 };
2029 return lowered;
2030 }
2031 if let Expr::Name { name: root, .. } = receiver {
2033 if root == "random" {
2034 return self.lower_call(
2035 &format!("random.{name}"),
2036 args,
2037 span,
2038 macro_params,
2039 position,
2040 );
2041 }
2042 }
2043 if let Expr::String { value, .. } = receiver {
2047 if name == "format" {
2048 if args.iter().any(|arg| arg.keyword.is_some()) {
2049 for arg in args {
2050 if let Some((keyword, span)) = &arg.keyword {
2051 self.error_at(
2052 "keyword-unsupported",
2053 format!(
2054 "function 'format' does not accept keyword \
2055 arguments ('{keyword}')"
2056 ),
2057 *span,
2058 );
2059 }
2060 }
2061 }
2062 let lowered: Vec<HirExpr> = self.lower_arg_values(args, macro_params);
2063 if let Some(entry) = self.manifest.resolve_member("format") {
2064 self.check_call_position("format", entry, position, span);
2065 }
2066 return HirExpr::Format {
2067 text: value.clone(),
2068 args: lowered,
2069 span: Some(span.into()),
2070 };
2071 }
2072 }
2073 let (member_name, lowered) = match self.manifest.resolve_member(name) {
2076 Some(entry) => {
2077 self.check_call_position(name, entry, position, span);
2078 if let Some(category) = entry.receiver {
2079 self.check_receiver(receiver, category, entry, span);
2080 }
2081 let (bound, _) = self.bind_args(entry, args, macro_params);
2082 (entry.id.clone(), bound)
2083 }
2084 None => {
2085 self.error_at("unknown-member", format!("unknown member '{name}'"), span);
2086 (name.to_string(), self.lower_arg_values(args, macro_params))
2087 }
2088 };
2089 if let Expr::Name { name: root, .. } = receiver {
2091 if matches!(
2092 root.as_str(),
2093 "eventPlayer" | "hostPlayer" | "localPlayer" | "attacker" | "victim"
2094 ) {
2095 return HirExpr::ReceiverCall {
2096 receiver: Box::new(
2097 context_player_expr(
2098 root,
2099 (!matches!(root.as_str(), "eventPlayer" | "hostPlayer"))
2100 .then_some(receiver.span()),
2101 )
2102 .expect("context-player receiver name is exhaustive"),
2103 ),
2104 name: member_name,
2105 args: lowered,
2106 span: Some(span.into()),
2107 };
2108 }
2109 }
2110 HirExpr::ReceiverCall {
2112 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
2113 name: member_name,
2114 args: lowered,
2115 span: Some(span.into()),
2116 }
2117 }
2118
2119 fn check_call_position(
2122 &mut self,
2123 name: &str,
2124 entry: &Function,
2125 position: CallPosition,
2126 span: Span,
2127 ) {
2128 match position {
2129 CallPosition::Statement => {
2130 if entry.context == Some(FunctionContext::ForIterable) {
2131 self.error_at(
2132 "invalid-call-context",
2133 format!("'{name}' is only valid as a for-loop iterable"),
2134 span,
2135 );
2136 } else if entry.kind.is_value() {
2137 self.error_at(
2138 "value-in-action-position",
2139 format!("value function '{name}' cannot be used as an action"),
2140 span,
2141 );
2142 }
2143 }
2144 CallPosition::Value => {
2145 if entry.kind.is_action() {
2146 self.error_at(
2147 "action-in-value-position",
2148 format!("action function '{name}' cannot be used as a value"),
2149 span,
2150 );
2151 } else if entry.context == Some(FunctionContext::ForIterable) {
2152 self.error_at(
2153 "invalid-call-context",
2154 format!("'{name}' is only valid as a for-loop iterable"),
2155 span,
2156 );
2157 }
2158 }
2159 CallPosition::ForIterable => {
2160 if entry.context != Some(FunctionContext::ForIterable) {
2161 self.error_at(
2162 "invalid-iterable",
2163 format!("for-loop iterable '{name}' must be a range(...) call"),
2164 span,
2165 );
2166 }
2167 }
2168 CallPosition::LambdaArgument => {
2169 if entry.kind.is_action() {
2170 self.error_at(
2171 "action-in-value-position",
2172 format!("action function '{name}' cannot be used as a value"),
2173 span,
2174 );
2175 } else if entry.context == Some(FunctionContext::ForIterable) {
2176 self.error_at(
2177 "invalid-call-context",
2178 format!("'{name}' is only valid as a for-loop iterable"),
2179 span,
2180 );
2181 }
2182 }
2183 }
2184 }
2185
2186 fn check_receiver(
2192 &mut self,
2193 receiver: &Expr,
2194 category: ReceiverCategory,
2195 entry: &Function,
2196 span: Span,
2197 ) {
2198 let mismatch = match category {
2199 ReceiverCategory::String => {
2200 !matches!(receiver, Expr::String { .. } | Expr::StringModifier { .. })
2201 }
2202 ReceiverCategory::Variable => !assignable_receiver(receiver),
2203 ReceiverCategory::Player | ReceiverCategory::Vector | ReceiverCategory::Any => false,
2204 };
2205 if mismatch {
2206 self.error_at(
2207 "invalid-receiver",
2208 format!(
2209 "member '{}' requires {} as its receiver",
2210 entry.id,
2211 category.describe()
2212 ),
2213 span,
2214 );
2215 }
2216 }
2217
2218 fn check_arity(&mut self, entry: &Function, got: usize, span: Span) {
2220 let (min, max) = entry.arity_bounds();
2221 let valid = got >= min && max.is_none_or(|max| got <= max);
2222 if !valid {
2223 let expects = match max {
2224 Some(max) if min == max => format!("exactly {min}"),
2225 Some(max) => format!("{min} to {max}"),
2226 None => format!("at least {min}"),
2227 };
2228 let role = match entry.kind {
2229 FunctionKind::Action => "action",
2230 FunctionKind::Value => "value",
2231 FunctionKind::MemberAction => "member action",
2232 FunctionKind::MemberValue => "member value",
2233 };
2234 self.error_at(
2235 "invalid-arity",
2236 format!(
2237 "{role} '{}' expects {expects} arguments but got {got}",
2238 entry.id
2239 ),
2240 span,
2241 );
2242 }
2243 }
2244
2245 fn error_at(&mut self, code: &str, message: String, span: Span) {
2246 self.errors.push(OpyError::at(code, message, span));
2247 }
2248}
2249
2250fn keyword_spellings(param: &Param) -> Vec<String> {
2252 let mut spellings = vec![param.name.clone()];
2253 spellings.extend(param.alternate_names.iter().cloned());
2254 spellings
2255}
2256
2257fn arg_span(args: &[CallArg]) -> Span {
2260 args.first().map(CallArg::span).unwrap_or_else(|| {
2261 Span::new(
2262 0,
2263 crate::diag::Position::new(1, 1),
2264 crate::diag::Position::new(1, 1),
2265 )
2266 })
2267}
2268
2269fn context_player_expr(name: &str, span: Option<Span>) -> Option<HirExpr> {
2274 match name {
2275 "eventPlayer" => Some(HirExpr::EventPlayer {
2276 span: span.map(Into::into),
2277 }),
2278 "localPlayer" => Some(HirExpr::Call {
2279 name: name.to_string(),
2280 args: Vec::new(),
2281 span: span.map(Into::into),
2282 }),
2283 "hostPlayer" => Some(HirExpr::HostPlayer {
2284 span: span.map(Into::into),
2285 }),
2286 "attacker" | "victim" => Some(HirExpr::Call {
2287 name: name.to_string(),
2288 args: Vec::new(),
2289 span: span.map(Into::into),
2290 }),
2291 _ => None,
2292 }
2293}
2294
2295fn assignable_receiver(receiver: &Expr) -> bool {
2300 match receiver {
2301 Expr::Name { name, .. } => !matches!(
2302 name.as_str(),
2303 "eventPlayer" | "hostPlayer" | "attacker" | "victim"
2304 ),
2305 Expr::Array { .. } | Expr::Index { .. } => true,
2306 _ => false,
2307 }
2308}
2309
2310impl From<Span> for HirSpan {
2311 fn from(span: Span) -> HirSpan {
2312 HirSpan {
2313 file: span.file,
2314 start: Position {
2315 line: span.start.line,
2316 col: span.start.col,
2317 },
2318 end: Position {
2319 line: span.end.line,
2320 col: span.end.col,
2321 },
2322 }
2323 }
2324}
2325
2326impl From<&Span> for HirSpan {
2327 fn from(span: &Span) -> HirSpan {
2328 (*span).into()
2329 }
2330}
2331
2332#[cfg(test)]
2333mod tests {
2334 use super::*;
2335 use crate::hir::types::{Expr as HirExpr, RuleEntry as HirRuleEntry, Stmt as HirStmt};
2336 use crate::lexer::{LexInput, lex};
2337 use crate::parser::parse;
2338
2339 fn lower_ok(text: &str) -> HirProgram {
2340 let tokens = lex(LexInput { file_id: 0, text }).expect("lexes");
2341 let output = parse(&tokens);
2342 assert!(
2343 output.errors.is_empty(),
2344 "unexpected parse errors: {:?}",
2345 output.errors
2346 );
2347 let program = output.program.expect("parse produces a program");
2348 lower(&program, vec![], vec![]).expect("lowers without errors")
2349 }
2350
2351 fn rule_conditions_and_actions(hir: &HirProgram) -> (&Vec<HirExpr>, &Vec<HirStmt>) {
2352 let HirRuleEntry::Rule(rule) = &hir.rules[0] else {
2353 panic!("expected a rule");
2354 };
2355 (&rule.conditions, &rule.actions)
2356 }
2357
2358 #[test]
2359 fn producer_emits_the_v2_ordered_switch_contract() {
2360 let hir = lower_ok(
2361 "globalvar value\nrule \"r\":\n @Event global\n switch value:\n default:\n value = 1\n case 2:\n value = 2\n",
2362 );
2363 assert_eq!(hir.protocol.name, "wright/opy-hir");
2364 assert_eq!(hir.protocol.version, "2.0.0");
2365 let value = serde_json::to_value(&hir).expect("HIR must serialize");
2366 let switch = &value["rules"][0]["actions"][0];
2367 assert!(switch.get("arms").is_some());
2368 assert!(switch.get("cases").is_none());
2369 assert!(switch.get("default").is_none());
2370 }
2371
2372 #[test]
2373 fn receiver_calls_lower_to_receiver_call_hir() {
2374 let hir = lower_ok(
2378 "globalvar target\nrule \"r\":\n @Event eachPlayer\n eventPlayer.setMoveSpeed(100)\n target.setMoveSpeed(50)\n",
2379 );
2380 let (_, actions) = rule_conditions_and_actions(&hir);
2381 assert_eq!(actions.len(), 2);
2382
2383 let HirStmt::Expr { expr, .. } = &actions[0] else {
2384 panic!("expected expression statement");
2385 };
2386 let HirExpr::ReceiverCall {
2387 receiver,
2388 name,
2389 args,
2390 ..
2391 } = expr.as_ref()
2392 else {
2393 panic!("expected receiver call, got {expr:?}");
2394 };
2395 assert_eq!(name, "setMoveSpeed");
2396 assert!(matches!(receiver.as_ref(), HirExpr::EventPlayer { .. }));
2397 assert_eq!(args.len(), 1);
2398 assert!(matches!(&args[0], HirExpr::Number { .. }));
2399
2400 let HirStmt::Expr { expr, .. } = &actions[1] else {
2401 panic!("expected expression statement");
2402 };
2403 let HirExpr::ReceiverCall { receiver, name, .. } = expr.as_ref() else {
2404 panic!("expected receiver call, got {expr:?}");
2405 };
2406 assert_eq!(name, "setMoveSpeed");
2407 assert!(
2408 matches!(receiver.as_ref(), HirExpr::GlobalVar { name, .. } if name == "target"),
2409 "globalvar receiver must resolve to a GlobalVar"
2410 );
2411 }
2412
2413 #[test]
2414 fn bare_variable_member_expression_preserves_receiver_and_member() {
2415 let hir = lower_ok(
2416 "globalvar A\nplayervar B\nrule \"receiver\":\n @Event eachPlayer\n A = B.C\n",
2417 );
2418 let HirStmt::Assign { value, .. } = &hir
2419 .rules
2420 .iter()
2421 .find_map(|entry| {
2422 let RuleEntry::Rule(rule) = entry else {
2423 return None;
2424 };
2425 rule.actions.first()
2426 })
2427 .expect("assignment")
2428 else {
2429 panic!("expected assignment");
2430 };
2431 let HirExpr::Member {
2432 receiver, member, ..
2433 } = value.as_ref()
2434 else {
2435 panic!("expected opaque member expression, got {value:?}");
2436 };
2437 assert_eq!(member, "C");
2438 assert!(matches!(receiver.as_ref(), HirExpr::GlobalVar { name, .. } if name == "B"));
2439 }
2440
2441 #[test]
2442 fn rule_prefix_template_is_global_and_subroutine_identity_is_preserved() {
2443 let text = "rule \"before\":\n pass\ndef source_name():\n @Name \"Friendly\"\n pass\nrule \"after\":\n pass\n";
2444 let tokens = lex(LexInput { file_id: 0, text }).expect("lexes");
2445 let output = parse(&tokens);
2446 assert!(
2447 output.errors.is_empty(),
2448 "unexpected parse errors: {:?}",
2449 output.errors
2450 );
2451 let program = output.program.expect("program");
2452 let preprocessing = PreprocessingState {
2453 rule_prefix_template: Some(crate::hir::types::DirectiveValue {
2454 value: "f\"[{$pathTitle.replace('_', ' ')}] {$rule}\" if $rule and not $isDelimiter else $rule".to_string(),
2455 span: None,
2456 }),
2457 ..PreprocessingState::default()
2458 };
2459 let hir = lower_with_preprocessing(
2460 &program,
2461 vec![SourceFile {
2462 id: 0,
2463 path: "main.opy".to_string(),
2464 }],
2465 vec![],
2466 &preprocessing,
2467 )
2468 .expect("lowers");
2469 let names: Vec<_> = hir
2470 .rules
2471 .iter()
2472 .map(|entry| match entry {
2473 HirRuleEntry::Rule(rule) => rule.name.clone(),
2474 HirRuleEntry::SubroutineDef { name, .. } => name.clone(),
2475 })
2476 .collect();
2477 assert_eq!(
2478 names,
2479 vec!["[Main] before", "[Main] Friendly", "[Main] after"]
2480 );
2481 let HirRuleEntry::SubroutineDef {
2482 name, source_name, ..
2483 } = &hir.rules[1]
2484 else {
2485 panic!("expected subroutine definition");
2486 };
2487 assert_eq!(name, "[Main] Friendly");
2488 assert_eq!(source_name, "source_name");
2489 }
2490
2491 #[test]
2492 fn receiver_call_values_lower_in_conditions() {
2493 let hir = lower_ok(
2497 "rule \"r\":\n @Event eachPlayer\n @Condition eventPlayer.isAlive()\n eventPlayer.teleport(eventPlayer.getPosition())\n",
2498 );
2499 let (conditions, actions) = rule_conditions_and_actions(&hir);
2500 assert_eq!(conditions.len(), 1);
2501 let HirExpr::ReceiverCall { name, args, .. } = &conditions[0] else {
2502 panic!("expected receiver call condition, got {:?}", conditions[0]);
2503 };
2504 assert_eq!(name, "isAlive");
2505 assert_eq!(args.len(), 0);
2506
2507 let HirStmt::Expr { expr, .. } = &actions[0] else {
2508 panic!("expected expression statement");
2509 };
2510 let HirExpr::ReceiverCall {
2511 name,
2512 args,
2513 receiver,
2514 ..
2515 } = expr.as_ref()
2516 else {
2517 panic!("expected receiver call, got {expr:?}");
2518 };
2519 assert_eq!(name, "teleport");
2520 assert!(matches!(receiver.as_ref(), HirExpr::EventPlayer { .. }));
2521 assert_eq!(args.len(), 1);
2522 assert!(matches!(
2523 &args[0],
2524 HirExpr::ReceiverCall { name, .. } if name == "getPosition"
2525 ));
2526 }
2527
2528 #[test]
2529 fn format_string_receiver_stays_a_format_node() {
2530 let hir = lower_ok(
2533 "rule \"r\":\n @Event global\n print(\"{} points\".format(len([1, 2])))\n",
2534 );
2535 let (_, actions) = rule_conditions_and_actions(&hir);
2536 let HirStmt::Expr { expr, .. } = &actions[0] else {
2537 panic!("expected expression statement");
2538 };
2539 assert!(
2540 has_format(expr),
2541 "string `.format()` must lower to a Format node"
2542 );
2543 }
2544
2545 fn has_format(expr: &HirExpr) -> bool {
2546 match expr {
2547 HirExpr::Format { .. } => true,
2548 HirExpr::Call { args, .. } => args.iter().any(has_format),
2549 HirExpr::ReceiverCall { args, .. } => args.iter().any(has_format),
2550 _ => false,
2551 }
2552 }
2553
2554 fn lowered_value(source: &str) -> HirExpr {
2556 let program = crate::compile(source, "test.opy", std::path::Path::new(""))
2557 .unwrap_or_else(|error| panic!("compile failed: {error}"));
2558 let RuleEntry::Rule(rule) = &program.rules[0] else {
2559 panic!("expected a rule");
2560 };
2561 let HirStmt::Assign { value, .. } = &rule.actions[0] else {
2562 panic!("expected an assign statement");
2563 };
2564 (**value).clone()
2565 }
2566
2567 #[test]
2568 fn chase_time_reeval_none_lowers_to_the_catalog_enum() {
2569 let value = lowered_value(
2570 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NONE\n",
2571 );
2572 assert_enum(&value, "ChaseTimeReeval", "NONE");
2573 }
2574
2575 #[test]
2576 fn chase_time_reeval_destination_and_duration_lowers_to_the_catalog_enum() {
2577 let value = lowered_value(
2578 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.DESTINATION_AND_DURATION\n",
2579 );
2580 assert_enum(&value, "ChaseTimeReeval", "DESTINATION_AND_DURATION");
2581 }
2582
2583 #[test]
2584 fn chase_rate_reeval_members_lower_to_the_catalog_enum() {
2585 for member in ["NONE", "DESTINATION_AND_RATE"] {
2586 let source = format!(
2587 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseRateReeval.{member}\n"
2588 );
2589 assert_enum(&lowered_value(&source), "ChaseRateReeval", member);
2590 }
2591 }
2592
2593 fn assert_enum(value: &HirExpr, domain: &str, member: &str) {
2596 match value {
2597 HirExpr::Enum {
2598 value_type, value, ..
2599 } => {
2600 assert_eq!(value_type, domain);
2601 assert_eq!(value, member);
2602 }
2603 other => panic!("expected enum {domain}.{member}, got {other:?}"),
2604 }
2605 }
2606
2607 #[test]
2608 fn unknown_chase_time_reeval_member_is_rejected_by_the_catalog() {
2609 let error = crate::compile(
2610 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NOPE\n",
2611 "test.opy",
2612 std::path::Path::new(""),
2613 )
2614 .expect_err("unknown catalog member must be rejected");
2615 assert_eq!(error.code, "unknown-enum-member");
2616 }
2617
2618 #[test]
2619 fn unknown_enum_receiver_is_an_unsupported_member_error() {
2620 let error = crate::compile(
2621 "globalvar g\nrule \"r\":\n @Event global\n g = NotARealEnum.MEMBER\n",
2622 "test.opy",
2623 std::path::Path::new(""),
2624 )
2625 .expect_err("an unknown enum type must fail");
2626 assert_eq!(error.code, "unsupported-member");
2627 let span = error.span.expect("the error is source-located");
2628 assert_eq!(span.start.line, 4);
2629 }
2630
2631 fn compile_error(source: &str, line: u32) -> OpyError {
2635 let error = crate::compile(source, "test.opy", std::path::Path::new(""))
2636 .expect_err("expected a compile failure");
2637 let span = error.span.expect("the error is source-located");
2638 assert_eq!(span.start.line, line, "code '{}'", error.code);
2639 error
2640 }
2641
2642 fn action_source(statement: &str) -> String {
2643 format!("globalvar g\nrule \"r\":\n @Event global\n {statement}\n")
2644 }
2645
2646 #[test]
2647 fn chase_over_time_resolves_and_compiles_with_reference_signatures() {
2648 let hir = crate::compile(
2650 &action_source("chaseOverTime(g, 10, 3, ChaseTimeReeval.NONE)"),
2651 "test.opy",
2652 std::path::Path::new(""),
2653 )
2654 .expect("reference-supported chaseOverTime compiles");
2655 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2656 panic!("expected a rule");
2657 };
2658 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2659 panic!("expected expression statement");
2660 };
2661 let HirExpr::Call { name, args, .. } = expr.as_ref() else {
2662 panic!("expected a call, got {expr:?}");
2663 };
2664 assert_eq!(name, "chaseOverTime");
2665 assert_eq!(args.len(), 4);
2666 assert!(matches!(
2667 &args[3],
2668 HirExpr::Enum { value_type, value, .. }
2669 if value_type == "ChaseTimeReeval" && value == "NONE"
2670 ));
2671
2672 let hir = crate::compile(
2674 &action_source("chaseOverTime(g, 10, 3)"),
2675 "test.opy",
2676 std::path::Path::new(""),
2677 )
2678 .expect("default-reevaluation chaseOverTime compiles");
2679 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2680 panic!("expected a rule");
2681 };
2682 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2683 panic!("expected expression statement");
2684 };
2685 let HirExpr::Call { args, .. } = expr.as_ref() else {
2686 panic!("expected a call");
2687 };
2688 assert_eq!(args.len(), 4);
2689 assert!(matches!(
2690 &args[3],
2691 HirExpr::Enum { value_type, value, .. }
2692 if value_type == "ChaseTimeReeval" && value == "DESTINATION_AND_DURATION"
2693 ));
2694 }
2695
2696 #[test]
2697 fn is_game_in_progress_resolves_as_a_builtin_value() {
2698 let hir = crate::compile(
2700 &action_source("@Condition isGameInProgress() == true"),
2701 "test.opy",
2702 std::path::Path::new(""),
2703 )
2704 .expect("reference-supported isGameInProgress compiles");
2705 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2706 panic!("expected a rule");
2707 };
2708 assert!(matches!(&rule.conditions[0], HirExpr::Binary { .. }));
2709 }
2710
2711 #[test]
2712 fn enum_gated_members_resolve_through_the_manifest() {
2713 let source = "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2717 @Condition eventPlayer.getThrottle() != vect(0, 0, 0)\n \
2718 @Condition worldVector(vect(1, 2, 3), eventPlayer, Transform.ROTATION) != vect(0, 0, 0)\n \
2719 eventPlayer.setInvisibility(Invis.ALL)\n \
2720 eventPlayer.setStatusEffect(eventPlayer, Status.ROOTED, 2)\n";
2721 let hir = crate::compile(source, "test.opy", std::path::Path::new(""))
2722 .expect("enum-gated members compile");
2723 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2724 panic!("expected a rule");
2725 };
2726 assert_eq!(rule.actions.len(), 2);
2727 }
2728
2729 #[test]
2730 fn get_players_in_radius_fills_reference_enum_defaults() {
2731 let hir = crate::compile(
2734 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2735 @Condition len(getPlayersInRadius(eventPlayer.getPosition(), 10)) > 0\n \
2736 disableInspector()\n",
2737 "test.opy",
2738 std::path::Path::new(""),
2739 )
2740 .expect("getPlayersInRadius with defaults compiles");
2741 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2742 panic!("expected a rule");
2743 };
2744 let HirExpr::Binary { left, .. } = &rule.conditions[0] else {
2745 panic!("expected a comparison");
2746 };
2747 let HirExpr::Call { name, args, .. } = left.as_ref() else {
2748 panic!("expected len call");
2749 };
2750 assert_eq!(name, "len");
2751 let HirExpr::Call { name, args, .. } = &args[0] else {
2752 panic!("expected getPlayersInRadius call");
2753 };
2754 assert_eq!(name, "getPlayersInRadius");
2755 assert_eq!(args.len(), 4);
2756 assert!(matches!(
2757 &args[2],
2758 HirExpr::Enum { value_type, value, .. }
2759 if value_type == "Team" && value == "ALL"
2760 ));
2761 assert!(matches!(
2762 &args[3],
2763 HirExpr::Enum { value_type, value, .. }
2764 if value_type == "LosCheck" && value == "OFF"
2765 ));
2766 }
2767
2768 #[test]
2769 fn value_call_in_action_position_is_rejected() {
2770 let error = compile_error(&action_source("isGameInProgress()"), 4);
2771 assert_eq!(error.code, "value-in-action-position");
2772 }
2773
2774 #[test]
2775 fn value_member_in_action_position_is_rejected() {
2776 let error = compile_error(
2779 "globalvar g\nrule \"r\":\n @Event eachPlayer\n eventPlayer.isAlive()\n",
2780 4,
2781 );
2782 assert_eq!(error.code, "value-in-action-position");
2783 }
2784
2785 fn first_action_expr(source: &str) -> HirExpr {
2790 let program = crate::compile(source, "test.opy", std::path::Path::new(""))
2791 .unwrap_or_else(|error| panic!("compile failed: {error}"));
2792 let RuleEntry::Rule(rule) = &program.rules[0] else {
2793 panic!("expected a rule");
2794 };
2795 match &rule.actions[0] {
2796 HirStmt::Expr { expr, .. } => (**expr).clone(),
2797 HirStmt::Assign { value, .. } => (**value).clone(),
2798 other => panic!("expected an expression or assignment, got {other:?}"),
2799 }
2800 }
2801
2802 fn strip_spans(value: &mut serde_json::Value) {
2805 match value {
2806 serde_json::Value::Object(map) => {
2807 map.remove("span");
2808 map.remove("name_span");
2809 for nested in map.values_mut() {
2810 strip_spans(nested);
2811 }
2812 }
2813 serde_json::Value::Array(items) => {
2814 for item in items {
2815 strip_spans(item);
2816 }
2817 }
2818 _ => {}
2819 }
2820 }
2821
2822 #[test]
2823 fn chase_keyword_forms_dispatch_to_the_concrete_chase_functions() {
2824 let expr = first_action_expr(&action_source("chase(g, 10, rate=2, ChaseReeval.NONE)"));
2829 let HirExpr::Call { name, args, .. } = &expr else {
2830 panic!("expected a call, got {expr:?}");
2831 };
2832 assert_eq!(name, "chaseAtRate");
2833 assert!(matches!(
2834 &args[3],
2835 HirExpr::Enum { value_type, value, .. }
2836 if value_type == "ChaseRateReeval" && value == "NONE"
2837 ));
2838
2839 let expr = first_action_expr(&action_source(
2840 "chase(g, 10, duration=3, ChaseReeval.DESTINATION_AND_DURATION)",
2841 ));
2842 let HirExpr::Call { name, args, .. } = &expr else {
2843 panic!("expected a call, got {expr:?}");
2844 };
2845 assert_eq!(name, "chaseOverTime");
2846 assert!(matches!(
2847 &args[3],
2848 HirExpr::Enum { value_type, value, .. }
2849 if value_type == "ChaseTimeReeval" && value == "DESTINATION_AND_DURATION"
2850 ));
2851
2852 let expr = first_action_expr(
2855 "playervar P\nrule \"r\":\n @Event eachPlayer\n \
2856 chase(eventPlayer.P, 0, rate=1, ChaseReeval.NONE)\n",
2857 );
2858 let HirExpr::Call { name, args, .. } = &expr else {
2859 panic!("expected a call, got {expr:?}");
2860 };
2861 assert_eq!(name, "chaseAtRate");
2862 assert!(matches!(&args[0], HirExpr::PlayerVar { .. }));
2863 }
2864
2865 #[test]
2866 fn chase_reeval_is_only_a_standalone_identity_inside_the_chase_context() {
2867 let error = compile_error(&action_source("g = ChaseReeval.NONE"), 4);
2871 assert_eq!(error.code, "unsupported-member");
2872
2873 let expr = first_action_expr(&action_source(
2878 "chase(g, 10, rate=2, ChaseReeval.DESTINATION_AND_DURATION)",
2879 ));
2880 let HirExpr::Call { name, args, .. } = &expr else {
2881 panic!("expected a call, got {expr:?}");
2882 };
2883 assert_eq!(name, "chaseAtRate");
2884 assert!(matches!(
2885 &args[3],
2886 HirExpr::Enum { value_type, value, .. }
2887 if value_type == "ChaseRateReeval" && value == "DESTINATION_AND_DURATION"
2888 ));
2889
2890 let expr = first_action_expr(&action_source("chase(g, 10, rate=2, 5)"));
2893 let HirExpr::Call { name, args, .. } = &expr else {
2894 panic!("expected a call, got {expr:?}");
2895 };
2896 assert_eq!(name, "chase");
2897 assert!(matches!(&args[3], HirExpr::Number { .. }));
2898 }
2899
2900 #[test]
2901 fn chase_requires_the_keyword_rate_or_duration_third_argument() {
2902 let error = compile_error(&action_source("chase(g, 10, 2, ChaseReeval.NONE)"), 4);
2903 assert_eq!(error.code, "keyword-required");
2904 assert!(error.message.contains("rate"));
2905 }
2906
2907 #[test]
2908 fn chase_family_requires_a_variable_first_argument() {
2909 let error = compile_error(&action_source("chase(10, 10, rate=2, ChaseReeval.NONE)"), 4);
2914 assert_eq!(error.code, "invalid-argument");
2915
2916 let error = compile_error(
2917 &action_source("chaseOverTime(10, 0, 30, ChaseTimeReeval.NONE)"),
2918 4,
2919 );
2920 assert_eq!(error.code, "invalid-argument");
2921 }
2922
2923 #[test]
2924 fn keyword_binding_matches_positional_binding_in_hir() {
2925 fn without_spans(expr: &HirExpr) -> serde_json::Value {
2929 let mut value = serde_json::to_value(expr).unwrap();
2930 strip_spans(&mut value);
2931 value
2932 }
2933 let keyword = without_spans(&first_action_expr(&action_source(
2934 "chaseOverTime(g, 10, duration=3)",
2935 )));
2936 let positional = without_spans(&first_action_expr(&action_source(
2937 "chaseOverTime(g, 10, 3)",
2938 )));
2939 assert_eq!(keyword, positional);
2940
2941 let keyword = without_spans(&first_action_expr(&action_source("wait(time=1)")));
2942 let positional = without_spans(&first_action_expr(&action_source("wait(1)")));
2943 assert_eq!(keyword, positional);
2944
2945 let keyword = without_spans(&first_action_expr(&action_source(
2947 "wait(waitBehavior=Wait.IGNORE_CONDITION, time=2)",
2948 )));
2949 let positional = without_spans(&first_action_expr(&action_source("wait(2)")));
2950 assert_eq!(keyword, positional);
2951
2952 let keyword = without_spans(&first_action_expr(&action_source(
2953 "g = vect(x=1, y=2, z=3)",
2954 )));
2955 let positional = without_spans(&first_action_expr(&action_source("g = vect(1, 2, 3)")));
2956 assert_eq!(keyword, positional);
2957 }
2958
2959 #[test]
2960 fn keyword_binding_diagnostics_are_structured_and_source_located() {
2961 let error = compile_error(&action_source("chaseOverTime(g, 10, bogus=1)"), 4);
2963 assert_eq!(error.code, "unknown-keyword");
2964 assert!(error.message.contains("bogus"));
2965
2966 let error = compile_error(
2968 &action_source(
2969 "chaseOverTime(g, 10, 3, ChaseTimeReeval.NONE, \
2970 reevaluation=ChaseTimeReeval.NONE)",
2971 ),
2972 4,
2973 );
2974 assert_eq!(error.code, "duplicate-argument");
2975
2976 let error = compile_error(&action_source("chaseOverTime(g, duration=3, 5)"), 4);
2978 assert_eq!(error.code, "positional-after-keyword");
2979
2980 let error = compile_error(&action_source("chaseOverTime(g, 10)"), 4);
2982 assert_eq!(error.code, "missing-argument");
2983
2984 let error = compile_error(
2987 &action_source("chase(variable=g, destination=10, rate=2, ChaseReeval.NONE)"),
2988 4,
2989 );
2990 assert_eq!(error.code, "unknown-keyword");
2991 }
2992
2993 #[test]
2994 fn keyword_arguments_are_rejected_for_reference_special_cases() {
2995 let error = compile_error(
2998 "globalvar g\nrule \"r\":\n @Event global\n \
2999 for I in range(start=0, stop=3):\n debug(I)\n",
3000 4,
3001 );
3002 assert_eq!(error.code, "keyword-unsupported");
3003
3004 let error = compile_error(&action_source("g = random.uniform(min=1, max=2)"), 4);
3005 assert_eq!(error.code, "keyword-unsupported");
3006
3007 let error = compile_error(&action_source("print(\"{} points\".format(value=1))"), 4);
3008 assert_eq!(error.code, "keyword-unsupported");
3009 }
3010
3011 #[test]
3012 fn wait_uses_the_reference_keyword_names() {
3013 let error = compile_error(&action_source("wait(duration=1)"), 4);
3017 assert_eq!(error.code, "unknown-keyword");
3018 assert!(error.message.contains("duration"));
3019 }
3020
3021 #[test]
3022 fn action_call_in_value_position_is_rejected() {
3023 let error = compile_error(&action_source("g = wait(1)"), 4);
3024 assert_eq!(error.code, "action-in-value-position");
3025 }
3026
3027 #[test]
3028 fn missing_required_argument_is_a_source_located_diagnostic() {
3029 let error = compile_error(&action_source("chaseOverTime(g, 10)"), 4);
3033 assert_eq!(error.code, "missing-argument");
3034 assert!(error.message.contains("duration"));
3035
3036 let error = compile_error(&action_source("chaseOverTime(g, 10, 3, 4, 5)"), 4);
3037 assert_eq!(error.code, "invalid-arity");
3038 }
3039
3040 #[test]
3041 fn missing_member_argument_is_a_source_located_diagnostic() {
3042 let error = compile_error(
3047 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
3048 getPlayersInRadius(eventPlayer.getPosition(), 10).setStatusEffect(eventPlayer, 30)\n",
3049 4,
3050 );
3051 assert_eq!(error.code, "missing-argument");
3052 assert!(error.message.contains("duration"));
3053 }
3054
3055 #[test]
3056 fn invalid_receiver_categories_are_rejected() {
3057 let error = compile_error(&action_source("3.append(1)"), 4);
3060 assert_eq!(error.code, "invalid-receiver");
3061 assert!(error.message.contains("append"));
3062
3063 let error = compile_error(&action_source("attacker.append(1)"), 4);
3064 assert_eq!(error.code, "invalid-receiver");
3065 assert!(error.message.contains("append"));
3066
3067 let error = compile_error(&action_source("print(3.format(\"{}\"))"), 4);
3068 assert_eq!(error.code, "invalid-receiver");
3069 assert!(error.message.contains("format"));
3070 }
3071
3072 #[test]
3073 fn cross_domain_enum_arguments_resolve_as_opaque_identities() {
3074 let expr = first_action_expr(&action_source("chaseOverTime(g, 10, 3, Invis.ALL)"));
3078 let HirExpr::Call { args, .. } = &expr else {
3079 panic!("expected a call, got {expr:?}");
3080 };
3081 assert!(matches!(
3082 &args[3],
3083 HirExpr::Enum { value_type, value, .. }
3084 if value_type == "Invis" && value == "ALL"
3085 ));
3086
3087 let expr = first_action_expr(&action_source(
3088 "eventPlayer.setInvisibility(ChaseTimeReeval.NONE)",
3089 ));
3090 let HirExpr::ReceiverCall { args, .. } = &expr else {
3091 panic!("expected a receiver call, got {expr:?}");
3092 };
3093 assert!(matches!(
3094 &args[0],
3095 HirExpr::Enum { value_type, value, .. }
3096 if value_type == "ChaseTimeReeval" && value == "NONE"
3097 ));
3098 }
3099
3100 #[test]
3101 fn non_enum_arguments_for_enum_parameters_are_carried_structurally() {
3102 let expr = first_action_expr(&action_source("eventPlayer.setInvisibility(g)"));
3106 let HirExpr::ReceiverCall { args, .. } = &expr else {
3107 panic!("expected a receiver call, got {expr:?}");
3108 };
3109 assert!(matches!(
3110 &args[0],
3111 HirExpr::GlobalVar { name, .. } if name == "g"
3112 ));
3113
3114 let expr = first_action_expr(&action_source("eventPlayer.setInvisibility(3)"));
3115 let HirExpr::ReceiverCall { args, .. } = &expr else {
3116 panic!("expected a receiver call, got {expr:?}");
3117 };
3118 assert!(matches!(&args[0], HirExpr::Number { .. }));
3119 }
3120
3121 #[test]
3122 fn unknown_builtins_fail_at_resolution_not_emission() {
3123 let error = compile_error(&action_source("frobnicate()"), 4);
3124 assert_eq!(error.code, "unknown-action");
3125
3126 let error = compile_error(&action_source("g = frobnicate()"), 4);
3127 assert_eq!(error.code, "unknown-value");
3128
3129 let error = compile_error(
3130 "globalvar g\nrule \"r\":\n @Event eachPlayer\n eventPlayer.frobnicate()\n",
3131 4,
3132 );
3133 assert_eq!(error.code, "unknown-member");
3134 }
3135
3136 #[test]
3137 fn wright_only_catalog_names_are_rejected() {
3138 let error = compile_error(&action_source("createHudText(1)"), 4);
3142 assert_eq!(error.code, "unknown-action");
3143
3144 let error = compile_error(&action_source("g = squareRoot(9)"), 4);
3145 assert_eq!(error.code, "unknown-value");
3146 }
3147
3148 #[test]
3149 fn generic_member_only_actions_are_rejected() {
3150 let error = compile_error(&action_source("setMoveSpeed(eventPlayer, 100)"), 4);
3153 assert_eq!(error.code, "unknown-action");
3154 }
3155
3156 #[test]
3157 fn range_is_for_iterables_only() {
3158 let error = compile_error(&action_source("@Condition len(range(1, 5, 1)) > 0"), 4);
3161 assert_eq!(error.code, "invalid-call-context");
3162
3163 let error = compile_error(&action_source("for g in [1, 2]:\n debug(g)"), 4);
3164 assert_eq!(error.code, "invalid-iterable");
3165
3166 crate::compile(
3167 &action_source("for g in range(3):\n debug(g)"),
3168 "test.opy",
3169 std::path::Path::new(""),
3170 )
3171 .expect("the for-header range form compiles");
3172 }
3173
3174 #[test]
3175 fn source_aliases_resolve_to_canonical_names() {
3176 let hir = crate::compile(
3179 &action_source("stopChasingVariable(g)"),
3180 "test.opy",
3181 std::path::Path::new(""),
3182 )
3183 .expect("the alias target compiles");
3184 let RuleEntry::Rule(rule) = &hir.rules[0] else {
3185 panic!("expected a rule");
3186 };
3187 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
3188 panic!("expected expression statement");
3189 };
3190 let HirExpr::Call { name, .. } = expr.as_ref() else {
3191 panic!("expected a call");
3192 };
3193 assert_eq!(name, "stopChasingVariable");
3194
3195 let hir = crate::compile(
3196 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
3197 @Condition eventPlayer.getCurrentHero() != null\n \
3198 @Condition eventPlayer.hasStatusEffect(Status.BURNING) == false\n \
3199 disableInspector()\n",
3200 "test.opy",
3201 std::path::Path::new(""),
3202 )
3203 .expect("member aliases compile");
3204 let RuleEntry::Rule(rule) = &hir.rules[0] else {
3205 panic!("expected a rule");
3206 };
3207 let HirExpr::Binary { left, .. } = &rule.conditions[0] else {
3208 panic!("expected a comparison");
3209 };
3210 let HirExpr::ReceiverCall { name, .. } = left.as_ref() else {
3211 panic!("expected a receiver call");
3212 };
3213 assert_eq!(name, "getHero");
3214 }
3215
3216 #[test]
3217 fn unknown_catalog_enum_members_are_rejected() {
3218 for source in [
3219 "globalvar g\nrule \"r\":\n @Event global\n g = Color.CYAN\n",
3220 "globalvar g\nrule \"r\":\n @Event global\n g = DynamicEffect.SPARKLES\n",
3221 ] {
3222 let error = crate::compile(source, "test.opy", std::path::Path::new(""))
3223 .expect_err("unknown catalog member must be rejected");
3224 assert_eq!(error.code, "unknown-enum-member");
3225 }
3226 }
3227
3228 #[test]
3229 fn default_var_for_binder_resolves_at_all_range_arities() {
3230 for (binder, iterable) in [
3235 ("I", "range(0, 10)"),
3236 ("I", "range(3)"),
3237 ("I", "range(1, 5, 2)"),
3238 ] {
3239 let hir = lower_ok(&format!(
3240 "globalvar total\nrule \"r\":\n @Event global\n for {binder} in {iterable}:\n total += {binder}\n"
3241 ));
3242 let (_, actions) = rule_conditions_and_actions(&hir);
3243 let HirStmt::For { variable, body, .. } = &actions[0] else {
3244 panic!("expected a for statement");
3245 };
3246 assert!(
3247 matches!(variable.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3248 "the binder resolves to the implicit global 'I', got {variable:?}"
3249 );
3250 assert!(!body.is_empty(), "the loop body lowers");
3251 let HirStmt::Assign { value, .. } = &body[0] else {
3254 panic!("expected an assignment in the body");
3255 };
3256 let HirExpr::Binary { right, .. } = value.as_ref() else {
3257 panic!("expected a binary expression");
3258 };
3259 assert!(
3260 matches!(right.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3261 "the binder use inside the body resolves to the implicit global"
3262 );
3263 }
3264 }
3265
3266 #[test]
3267 fn player_variable_range_binder_preserves_host_player_receiver() {
3268 let hir = lower_ok(
3269 "playervar I\nrule \"r\":\n @Event global\n for hostPlayer.I in range(3):\n hostPlayer.I = 1\n",
3270 );
3271 let (_, actions) = rule_conditions_and_actions(&hir);
3272 let HirStmt::For { variable, .. } = &actions[0] else {
3273 panic!("expected a for statement");
3274 };
3275 let HirExpr::PlayerVar {
3276 player,
3277 name,
3278 member_span,
3279 span,
3280 } = variable.as_ref()
3281 else {
3282 panic!("expected a player-variable binder, got {variable:?}");
3283 };
3284 assert_eq!(name, "I");
3285 assert!(matches!(player.as_ref(), HirExpr::HostPlayer { .. }));
3286 assert_eq!(span.unwrap().start.line, 4);
3287 assert_eq!(span.unwrap().start.col, 9);
3288 assert_eq!(span.unwrap().end.line, 4);
3289 assert_eq!(span.unwrap().end.col, 21);
3290 let member_span = member_span.expect("player binder member span");
3291 assert_eq!(member_span.start.line, 4);
3292 assert_eq!(member_span.start.col, 20);
3293 assert_eq!(member_span.end.line, 4);
3294 assert_eq!(member_span.end.col, 21);
3295 }
3296
3297 #[test]
3298 fn default_var_names_resolve_as_implicit_globals() {
3299 let hir = lower_ok("rule \"r\":\n @Event global\n I = 5\n debug(I)\n");
3302 let (_, actions) = rule_conditions_and_actions(&hir);
3303 let HirStmt::Assign { target, .. } = &actions[0] else {
3304 panic!("expected an assignment");
3305 };
3306 assert!(
3307 matches!(target.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3308 "the implicit global resolves, got {target:?}"
3309 );
3310 assert_eq!(default_var_index("I"), Some(8));
3312 assert_eq!(default_var_index("AA"), Some(26));
3313 assert_eq!(default_var_index("Z"), Some(25));
3314 assert_eq!(default_var_index("DX"), Some(127));
3315 assert_eq!(default_var_index("DY"), None);
3316 assert_eq!(default_var_index("i"), None);
3317 }
3318
3319 #[test]
3320 fn nested_same_name_for_binders_reuse_the_implicit_global() {
3321 let hir = lower_ok(
3325 "rule \"r\":\n @Event global\n for I in range(3):\n for I in range(2):\n debug(I)\n",
3326 );
3327 let (_, actions) = rule_conditions_and_actions(&hir);
3328 let HirStmt::For {
3329 variable: outer,
3330 body,
3331 ..
3332 } = &actions[0]
3333 else {
3334 panic!("expected an outer for statement");
3335 };
3336 let HirStmt::For {
3337 variable: inner, ..
3338 } = &body[0]
3339 else {
3340 panic!("expected an inner for statement");
3341 };
3342 assert!(
3343 matches!(outer.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I")
3344 && matches!(inner.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3345 "both loops bind the same implicit global (spans differ per binder site)"
3346 );
3347 }
3348
3349 #[test]
3350 fn undeclared_lowercase_binder_is_still_an_unknown_identifier() {
3351 let error = compile_error(
3356 "rule \"r\":\n @Event global\n for i in range(3):\n debug(i)\n",
3357 3,
3358 );
3359 assert_eq!(error.code, "unknown-identifier");
3360 let span = error.span.expect("the error is source-located");
3361 assert_eq!(span.start.line, 3);
3362 }
3363
3364 #[test]
3365 fn issue_28_constructs_lower_to_provenance_preserving_hir() {
3366 let hir = lower_ok(
3367 "globalvar x\nrule \"r\":\n @Event global\n do:\n x = {\"x\": 1}[\"x\"]\n while x not in [2, 3]\n switch x:\n case 0x10:\n x = 1 in [1, 2]\n default:\n x = 2\n x = [value * 2 for value, index in [1, 2] if value > index]\n x = sorted([1, 2], key=lambda value: value)\n x = w\"wide\"\n",
3368 );
3369 let (_, actions) = rule_conditions_and_actions(&hir);
3370 let HirStmt::DoWhile { condition, .. } = &actions[0] else {
3371 panic!("expected do-while");
3372 };
3373 assert!(matches!(condition.as_ref(), HirExpr::Binary { op, .. } if op == "not in"));
3374 let HirStmt::Switch { arms, .. } = &actions[1] else {
3375 panic!("expected switch");
3376 };
3377 assert_eq!(arms.len(), 2);
3378 let HirSwitchArm::Case {
3379 value: case_value,
3380 body,
3381 ..
3382 } = &arms[0]
3383 else {
3384 panic!("expected case arm");
3385 };
3386 assert!(
3387 matches!(case_value.as_ref(), HirExpr::Number { value, .. } if *value == 0x10 as f64)
3388 );
3389 let HirStmt::Assign { value, .. } = &body[0] else {
3390 panic!("expected case assignment");
3391 };
3392 assert!(matches!(value.as_ref(), HirExpr::Binary { op, .. } if op == "in"));
3393 let HirSwitchArm::Default { body, .. } = &arms[1] else {
3394 panic!("expected default arm");
3395 };
3396 assert!(matches!(body[0], HirStmt::Assign { .. }));
3397 let HirStmt::Assign { value, .. } = &actions[2] else {
3398 panic!("expected comprehension assignment");
3399 };
3400 assert!(matches!(value.as_ref(), HirExpr::Comprehension { .. }));
3401 let HirStmt::Assign { value, .. } = &actions[3] else {
3402 panic!("expected sorted assignment");
3403 };
3404 assert!(
3405 matches!(value.as_ref(), HirExpr::Call { name, args, .. } if name == "sorted" && matches!(&args[1], HirExpr::Lambda { body, .. } if matches!(body.as_ref(), HirExpr::Local { name, .. } if name == "value")))
3406 );
3407 let HirStmt::Assign { value, .. } = &actions[4] else {
3408 panic!("expected string assignment");
3409 };
3410 assert!(
3411 matches!(value.as_ref(), HirExpr::StringModifier { modifier, .. } if modifier == "w")
3412 );
3413 }
3414
3415 #[test]
3416 fn issue_28_rejects_reference_invalid_bare_dict_and_lambda() {
3417 let dict_error = compile_error(
3418 "globalvar x\nrule \"r\":\n @Event global\n x = {\"x\": 1}\n",
3419 4,
3420 );
3421 assert_eq!(dict_error.code, "dict-access");
3422 let lambda_error = compile_error(
3423 "globalvar x\nrule \"r\":\n @Event global\n x = lambda value: value\n",
3424 4,
3425 );
3426 assert_eq!(lambda_error.code, "lambda-context");
3427 }
3428
3429 #[test]
3430 fn do_while_requires_rule_or_definition_prefix_position() {
3431 let error = compile_error(
3432 "globalvar value\nrule \"r\":\n @Event global\n value = 1\n do:\n value += 1\n while value < 2\n",
3433 5,
3434 );
3435 assert_eq!(error.code, "do-while-placement");
3436 assert_eq!(
3437 error.message,
3438 "do-while must be at the beginning of a rule, subroutine, or do-while body; only pass statements may precede it"
3439 );
3440 }
3441}