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