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};
39use crate::diag::{OpyError, OpyResult, Span};
40use crate::manifest::{
41 Function, FunctionContext, FunctionKind, Manifest, Param, ParamDefault, ReceiverCategory,
42};
43use workshop_rs::catalog::Catalog;
44
45const PROTOCOL_NAME: &str = "wright/opy-hir";
47
48#[derive(Clone, Copy, PartialEq, Eq)]
51enum CallPosition {
52 Statement,
54 Value,
56 ForIterable,
58 LambdaArgument,
60}
61
62struct Lowerer {
64 globals: HashSet<String>,
65 players: HashSet<String>,
66 subroutines: HashSet<String>,
67 macros: HashSet<String>,
68 enums: HashMap<String, Vec<String>>,
69 locals: Vec<String>,
70 allow_dict_literal: bool,
71 manifest: &'static Manifest,
73 catalog: Catalog,
75 errors: Vec<OpyError>,
76}
77
78pub fn lower(
80 program: &cst::Program,
81 files: Vec<SourceFile>,
82 defines: Vec<Define>,
83) -> OpyResult<HirProgram> {
84 lower_with_preprocessing(program, files, defines, &PreprocessingState::default())
85}
86
87pub fn lower_with_preprocessing(
88 program: &cst::Program,
89 files: Vec<SourceFile>,
90 defines: Vec<Define>,
91 preprocessing: &PreprocessingState,
92) -> OpyResult<HirProgram> {
93 let manifest = match Manifest::builtin() {
94 Ok(manifest) => manifest,
95 Err(error) => {
96 return Err(OpyError::new(
97 "manifest-error",
98 format!("cannot load the OPY semantic compatibility manifest: {error}"),
99 ));
100 }
101 };
102 let catalog = match Catalog::builtin() {
103 Ok(catalog) => catalog,
104 Err(error) => {
105 return Err(OpyError::new(
106 "catalog-error",
107 format!("cannot load the Workshop catalog: {error}"),
108 ));
109 }
110 };
111 let mut lowerer = Lowerer {
112 globals: HashSet::new(),
113 players: HashSet::new(),
114 subroutines: HashSet::new(),
115 macros: HashSet::new(),
116 enums: HashMap::new(),
117 locals: Vec::new(),
118 allow_dict_literal: false,
119 manifest,
120 catalog,
121 errors: Vec::new(),
122 };
123 lowerer.collect_symbols(program);
124
125 let mut declarations = Vec::new();
126 for decl in &program.declarations {
127 match decl {
128 Decl::GlobalVariable {
129 name,
130 index,
131 span,
132 name_span,
133 initializer,
134 } => {
135 declarations.push(Declaration::GlobalVariable {
136 name: name.clone(),
137 index: *index,
138 span: Some(span.into()),
139 name_span: Some(name_span.into()),
140 initializer: lowerer.initializer(initializer.as_ref()),
141 });
142 }
143 Decl::PlayerVariable {
144 name,
145 index,
146 span,
147 name_span,
148 initializer,
149 } => {
150 declarations.push(Declaration::PlayerVariable {
151 name: name.clone(),
152 index: *index,
153 span: Some(span.into()),
154 name_span: Some(name_span.into()),
155 initializer: lowerer.initializer(initializer.as_ref()),
156 });
157 }
158 Decl::Subroutine {
159 name,
160 span,
161 name_span,
162 } => {
163 declarations.push(Declaration::Subroutine {
164 name: name.clone(),
165 index: None,
166 span: Some(span.into()),
167 name_span: Some(name_span.into()),
168 });
169 }
170 Decl::Enum { .. } => {
171 }
174 Decl::Macro {
175 name,
176 args,
177 body,
178 span,
179 } => {
180 let lowered_body = lowerer.lower_macro_body(body, args);
181 declarations.push(Declaration::Macro {
182 name: name.clone(),
183 args: args.clone(),
184 span: Some(span.into()),
185 body: lowered_body,
186 });
187 }
188 }
189 }
190
191 let mut rules = Vec::new();
192 for entry in &program.rules {
193 match entry {
194 CstRuleEntry::Rule(rule) => rules.push(RuleEntry::Rule(lowerer.lower_rule(
195 rule,
196 files.as_slice(),
197 preprocessing,
198 )?)),
199 CstRuleEntry::SubroutineDef {
200 name,
201 presentation_name,
202 span,
203 name_span,
204 body,
205 annotations,
206 rule_prefix,
207 } => {
208 let base_name = presentation_name
209 .as_deref()
210 .map(str::to_string)
211 .unwrap_or_else(|| name.clone());
212 let generated_name = render_rule_name(
213 &base_name,
214 rule_prefix.as_deref(),
215 false,
216 *span,
217 files.as_slice(),
218 preprocessing,
219 )?;
220 rules.push(RuleEntry::SubroutineDef {
221 kind: "subroutineDef".to_string(),
222 name: generated_name,
223 source_name: name.clone(),
224 span: Some(span.into()),
225 name_span: Some(name_span.into()),
226 body: lowerer.lower_block(body, &[], false, true),
227 annotations: lower_annotations(annotations),
228 });
229 }
230 }
231 }
232
233 if !lowerer.errors.is_empty() {
234 return Err(lowerer.errors.swap_remove(0));
235 }
236
237 Ok(HirProgram {
238 protocol: Protocol {
239 name: PROTOCOL_NAME.to_string(),
240 version: PROTOCOL_VERSION.to_string(),
241 },
242 generator: Generator {
243 name: crate::LANGUAGE_NAME.to_string(),
244 version: crate::LANGUAGE_VERSION.to_string(),
245 frontend: crate::LANGUAGE_NAME.to_string(),
246 },
247 files,
248 defines,
249 declarations,
250 rules,
251 settings: program.settings.as_ref().map(lower_settings),
252 preprocessing: preprocessing.clone(),
253 })
254}
255
256fn prefixed_rule_name(name: &str, prefix: Option<&str>, delimiter: bool) -> String {
257 match prefix {
258 Some(prefix) if !prefix.is_empty() && !delimiter && !name.is_empty() => {
259 format!("[{prefix}] {name}")
260 }
261 _ => name.to_string(),
262 }
263}
264
265#[derive(Clone, Debug)]
266enum TemplateValue {
267 String(String),
268 Bool(bool),
269}
270
271fn render_rule_name(
272 name: &str,
273 prefix: Option<&str>,
274 delimiter: bool,
275 span: Span,
276 files: &[SourceFile],
277 preprocessing: &PreprocessingState,
278) -> OpyResult<String> {
279 let Some(template) = preprocessing
280 .rule_prefix_template
281 .as_ref()
282 .map(|value| value.value.as_str())
283 else {
284 return Ok(prefixed_rule_name(name, prefix, delimiter));
285 };
286 let (file, path) = rule_file_parts(span.file, files);
287 let prefix = prefix.unwrap_or_default();
288 let values = [
289 ("$rule", TemplateValue::String(name.to_string())),
290 ("$prefix", TemplateValue::String(prefix.to_string())),
291 ("$file", TemplateValue::String(file.clone())),
292 ("$path", TemplateValue::String(path.clone())),
293 ("$isDelimiter", TemplateValue::Bool(delimiter)),
294 ("$prefixTitle", TemplateValue::String(title_case(prefix))),
295 ("$prefixUpper", TemplateValue::String(prefix.to_uppercase())),
296 ("$prefixLower", TemplateValue::String(prefix.to_lowercase())),
297 ("$fileTitle", TemplateValue::String(title_case(&file))),
298 ("$fileUpper", TemplateValue::String(file.to_uppercase())),
299 ("$fileLower", TemplateValue::String(file.to_lowercase())),
300 ("$pathTitle", TemplateValue::String(title_case(&path))),
301 ("$pathUpper", TemplateValue::String(path.to_uppercase())),
302 ("$pathLower", TemplateValue::String(path.to_lowercase())),
303 ];
304 evaluate_template(template, &values).map_err(|message| {
305 OpyError::at(
306 "rule-prefix-template-invalid",
307 format!("could not resolve rule prefix template: {message}"),
308 span,
309 )
310 })
311}
312
313fn rule_file_parts(file_id: u32, files: &[SourceFile]) -> (String, String) {
314 let path = files
315 .iter()
316 .find(|file| file.id == file_id)
317 .map(|file| file.path.replace('\\', "/"))
318 .unwrap_or_default();
319 let without_extension = path
320 .strip_suffix(".opy")
321 .or_else(|| path.strip_suffix(".OPY"))
322 .unwrap_or(&path)
323 .to_string();
324 let file = without_extension
325 .rsplit('/')
326 .next()
327 .unwrap_or_default()
328 .to_string();
329 (file, without_extension)
330}
331
332fn title_case(value: &str) -> String {
333 let mut result = String::with_capacity(value.len());
334 let mut capitalize = true;
335 for ch in value.chars() {
336 if ch == '_' {
337 result.push(' ');
338 capitalize = true;
339 } else if capitalize && ch.is_ascii_alphabetic() {
340 result.push(ch.to_ascii_uppercase());
341 capitalize = false;
342 } else {
343 result.push(ch);
344 if !ch.is_whitespace() && ch != '/' {
345 capitalize = false;
346 }
347 }
348 if ch == '/' || ch.is_whitespace() {
349 capitalize = true;
350 }
351 }
352 result
353}
354
355fn evaluate_template(template: &str, values: &[(&str, TemplateValue)]) -> Result<String, String> {
356 if let Some((then_value, condition, else_value)) = split_conditional(template) {
357 let branch = if evaluate_condition(condition, values)? {
358 then_value
359 } else {
360 else_value
361 };
362 return evaluate_string(branch, values);
363 }
364 evaluate_string(template, values)
365}
366
367fn split_conditional(value: &str) -> Option<(&str, &str, &str)> {
368 let mut quote = None;
369 let mut depth = 0usize;
370 let mut if_start = None;
371 let mut else_start = None;
372 for (index, ch) in value.char_indices() {
373 match (ch, quote) {
374 ('"' | '\'', None) => quote = Some(ch),
375 (ch, Some(current)) if ch == current => quote = None,
376 ('{', None) => depth += 1,
377 ('}', None) => depth = depth.saturating_sub(1),
378 _ => {}
379 }
380 if quote.is_none() && depth == 0 {
381 if value[index..].starts_with(" if ") && if_start.is_none() {
382 if_start = Some(index);
383 } else if value[index..].starts_with(" else ") && else_start.is_none() {
384 else_start = Some(index);
385 }
386 }
387 }
388 let (Some(if_start), Some(else_start)) = (if_start, else_start) else {
389 return None;
390 };
391 Some((
392 value[..if_start].trim(),
393 value[if_start + 4..else_start].trim(),
394 value[else_start + 6..].trim(),
395 ))
396}
397
398fn evaluate_condition(value: &str, values: &[(&str, TemplateValue)]) -> Result<bool, String> {
399 let value = value.trim();
400 if let Some(rest) = value.strip_prefix("not ") {
401 return Ok(!evaluate_condition(rest, values)?);
402 }
403 if let Some((left, right)) = value.split_once(" or ") {
404 return Ok(evaluate_condition(left, values)? || evaluate_condition(right, values)?);
405 }
406 if let Some((left, right)) = value.split_once(" and ") {
407 return Ok(evaluate_condition(left, values)? && evaluate_condition(right, values)?);
408 }
409 match lookup_template_value(value, values)? {
410 TemplateValue::Bool(value) => Ok(value),
411 TemplateValue::String(value) => Ok(!value.is_empty()),
412 }
413}
414
415fn evaluate_string(value: &str, values: &[(&str, TemplateValue)]) -> Result<String, String> {
416 let value = value.trim();
417 if let Some(body) = value
418 .strip_prefix("f\"")
419 .and_then(|body| body.strip_suffix('"'))
420 {
421 return interpolate_fstring(body, values);
422 }
423 if let Some(body) = value
424 .strip_prefix("f'")
425 .and_then(|body| body.strip_suffix('\''))
426 {
427 return interpolate_fstring(body, values);
428 }
429 if value.len() >= 2
430 && ((value.starts_with('"') && value.ends_with('"'))
431 || (value.starts_with('\'') && value.ends_with('\'')))
432 {
433 return Ok(value[1..value.len() - 1].to_string());
434 }
435 match lookup_template_value(value, values)? {
436 TemplateValue::String(value) => Ok(value),
437 TemplateValue::Bool(value) => Ok(value.to_string()),
438 }
439}
440
441fn interpolate_fstring(body: &str, values: &[(&str, TemplateValue)]) -> Result<String, String> {
442 let mut result = String::new();
443 let mut remaining = body;
444 while let Some(start) = remaining.find('{') {
445 result.push_str(&remaining[..start]);
446 let end = remaining[start + 1..]
447 .find('}')
448 .ok_or_else(|| "unterminated interpolation".to_string())?
449 + start
450 + 1;
451 result.push_str(&evaluate_string(&remaining[start + 1..end], values)?);
452 remaining = &remaining[end + 1..];
453 }
454 result.push_str(remaining);
455 Ok(result)
456}
457
458fn lookup_template_value(
459 value: &str,
460 values: &[(&str, TemplateValue)],
461) -> Result<TemplateValue, String> {
462 let value = value.trim();
463 let (base, mut methods) = value
464 .split_once('.')
465 .map_or((value, ""), |(base, methods)| (base, methods));
466 let mut result = values
467 .iter()
468 .find(|(name, _)| *name == base)
469 .map(|(_, value)| value.clone())
470 .ok_or_else(|| format!("unsupported expression '{value}'"))?;
471 while !methods.is_empty() {
472 let (method, rest) = methods
473 .split_once('.')
474 .map_or((methods, ""), |(method, rest)| (method, rest));
475 if method == "upper()" {
476 result = TemplateValue::String(as_string(&result).to_uppercase());
477 } else if method == "lower()" {
478 result = TemplateValue::String(as_string(&result).to_lowercase());
479 } else if let Some(args) = method
480 .strip_prefix("replace(")
481 .and_then(|v| v.strip_suffix(')'))
482 {
483 let (from, to) = args
484 .split_once(',')
485 .ok_or_else(|| "replace expects two arguments".to_string())?;
486 let from = unquote_template_arg(from.trim())?;
487 let to = unquote_template_arg(to.trim())?;
488 result = TemplateValue::String(as_string(&result).replace(&from, &to));
489 } else {
490 return Err(format!("unsupported method '{method}'"));
491 }
492 methods = rest;
493 }
494 Ok(result)
495}
496
497fn as_string(value: &TemplateValue) -> String {
498 match value {
499 TemplateValue::String(value) => value.clone(),
500 TemplateValue::Bool(value) => value.to_string(),
501 }
502}
503
504fn unquote_template_arg(value: &str) -> Result<String, String> {
505 if value.len() >= 2
506 && ((value.starts_with('"') && value.ends_with('"'))
507 || (value.starts_with('\'') && value.ends_with('\'')))
508 {
509 Ok(value[1..value.len() - 1].to_string())
510 } else {
511 Err(format!("expected a quoted string argument, got '{value}'"))
512 }
513}
514
515fn lower_annotations(annotations: &[cst::Annotation]) -> Vec<HirAnnotation> {
516 annotations
517 .iter()
518 .map(|annotation| HirAnnotation {
519 name: annotation.name.clone(),
520 args: annotation
521 .args
522 .iter()
523 .map(|arg| HirAnnotationArg {
524 text: arg.text.clone(),
525 span: Some(arg.span.into()),
526 })
527 .collect(),
528 span: Some(annotation.span.into()),
529 })
530 .collect()
531}
532
533fn lower_settings(settings: &cst::Settings) -> HirSettings {
535 HirSettings {
536 span: Some(settings.span.into()),
537 children: settings.children.iter().map(lower_settings_node).collect(),
538 }
539}
540
541fn lower_settings_node(node: &cst::SettingsNode) -> HirSettingsNode {
542 match node {
543 cst::SettingsNode::Group {
544 name,
545 children,
546 span,
547 } => HirSettingsNode::Group {
548 name: name.clone(),
549 children: children.iter().map(lower_settings_node).collect(),
550 span: Some((*span).into()),
551 },
552 cst::SettingsNode::Number { name, value, span } => HirSettingsNode::Number {
553 name: name.clone(),
554 value: *value,
555 span: Some((*span).into()),
556 },
557 cst::SettingsNode::Bool { name, value, span } => HirSettingsNode::Bool {
558 name: name.clone(),
559 value: *value,
560 span: Some((*span).into()),
561 },
562 cst::SettingsNode::String { name, value, span } => HirSettingsNode::String {
563 name: name.clone(),
564 value: value.clone(),
565 span: Some((*span).into()),
566 },
567 cst::SettingsNode::List {
568 name,
569 elements,
570 span,
571 } => HirSettingsNode::List {
572 name: name.clone(),
573 elements: elements
574 .iter()
575 .map(|element| crate::hir::types::SettingsListElement {
576 value: element.value.clone(),
577 span: Some(element.span.into()),
578 })
579 .collect(),
580 span: Some((*span).into()),
581 },
582 }
583}
584
585impl Lowerer {
586 fn collect_symbols(&mut self, program: &cst::Program) {
587 for decl in &program.declarations {
588 match decl {
589 Decl::GlobalVariable { name, .. } => {
590 self.globals.insert(name.clone());
591 }
592 Decl::PlayerVariable { name, .. } => {
593 self.players.insert(name.clone());
594 }
595 Decl::Subroutine { name, .. } => {
596 self.subroutines.insert(name.clone());
597 }
598 Decl::Enum { name, members, .. } => {
599 self.enums.insert(
600 name.clone(),
601 members.iter().map(|(member, _)| member.clone()).collect(),
602 );
603 }
604 Decl::Macro { name, .. } => {
605 self.macros.insert(name.clone());
606 }
607 }
608 }
609 }
610
611 fn initializer(&mut self, initializer: Option<&Expr>) -> Option<Box<HirExpr>> {
615 let initializer = initializer?;
616 let lowered = self.lower_expr(initializer, &[], CallPosition::Value);
617 match &lowered {
618 HirExpr::Number { text, .. } if text == "0" => None,
619 other => Some(Box::new(other.clone())),
620 }
621 }
622
623 fn lower_rule(
624 &mut self,
625 rule: &cst::Rule,
626 files: &[SourceFile],
627 preprocessing: &PreprocessingState,
628 ) -> OpyResult<Rule> {
629 let conditions = rule
630 .conditions
631 .iter()
632 .map(|condition| self.lower_expr(condition, &[], CallPosition::Value))
633 .collect();
634 let actions = self.lower_block(&rule.actions, &[], false, true);
635 Ok(Rule {
636 name: render_rule_name(
637 &rule.name,
638 rule.rule_prefix.as_deref(),
639 rule.delimiter,
640 rule.span,
641 files,
642 preprocessing,
643 )?,
644 span: Some(rule.span.into()),
645 name_span: Some(rule.name_span.into()),
646 disabled: rule.disabled,
647 delimiter: rule.delimiter,
648 new_page: rule.new_page.clone(),
649 annotations: lower_annotations(&rule.annotations),
650 event: Event {
651 name: rule.event.name.clone(),
652 args: rule
653 .event
654 .args
655 .iter()
656 .map(|arg| self.lower_expr(arg, &[], CallPosition::Value))
657 .collect(),
658 span: Some(rule.event.span.into()),
659 },
660 conditions,
661 actions,
662 })
663 }
664
665 fn lower_block(
667 &mut self,
668 stmts: &[Stmt],
669 macro_params: &[String],
670 breakable: bool,
671 allow_do_while: bool,
672 ) -> Vec<HirStmt> {
673 stmts
674 .iter()
675 .enumerate()
676 .map(|(index, stmt)| {
677 if matches!(stmt, Stmt::DoWhile { .. })
678 && (!allow_do_while
679 || stmts[..index]
680 .iter()
681 .any(|previous| !matches!(previous, Stmt::Pass { .. })))
682 {
683 self.error_at(
684 "do-while-placement",
685 "do-while must be at the beginning of a rule, subroutine, or do-while body; only pass statements may precede it".to_string(),
686 stmt.span(),
687 );
688 }
689 self.lower_stmt(stmt, macro_params, breakable)
690 })
691 .collect()
692 }
693
694 fn lower_stmt(&mut self, stmt: &Stmt, macro_params: &[String], breakable: bool) -> HirStmt {
695 match stmt {
696 Stmt::Expr { expr, span } => {
697 if let Expr::Call { name, args, .. } = expr {
700 if self.subroutines.contains(name) && args.is_empty() {
701 return HirStmt::CallSubroutine {
702 name: name.clone(),
703 span: Some(span.into()),
704 };
705 }
706 }
707 HirStmt::Expr {
710 expr: Box::new(self.lower_expr(expr, macro_params, CallPosition::Statement)),
711 span: Some(span.into()),
712 }
713 }
714 Stmt::Assign {
715 target,
716 value,
717 span,
718 } => HirStmt::Assign {
719 target: Box::new(self.lower_expr(target, macro_params, CallPosition::Value)),
720 value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)),
721 span: Some(span.into()),
722 },
723 Stmt::If {
724 branches,
725 r#else,
726 span,
727 } => HirStmt::If {
728 branches: branches
729 .iter()
730 .map(|branch| IfBranch {
731 condition: Box::new(self.lower_expr(
732 &branch.condition,
733 macro_params,
734 CallPosition::Value,
735 )),
736 body: self.lower_block(&branch.body, macro_params, breakable, false),
737 })
738 .collect(),
739 r#else: r#else
740 .as_ref()
741 .map(|body| self.lower_block(body, macro_params, breakable, false)),
742 span: Some(span.into()),
743 },
744 Stmt::For {
745 variable,
746 iterable,
747 body,
748 span,
749 } => {
750 let iterable_position = if matches!(iterable, Expr::Call { name, .. } if name == "range")
754 {
755 CallPosition::ForIterable
756 } else {
757 self.error_at(
758 "invalid-iterable",
759 "for-loop iterable must be a range(...) call".to_string(),
760 iterable.span(),
761 );
762 CallPosition::Value
763 };
764 HirStmt::For {
765 variable: Box::new(self.lower_expr(
766 variable,
767 macro_params,
768 CallPosition::Value,
769 )),
770 iterable: Box::new(self.lower_expr(iterable, macro_params, iterable_position)),
771 body: self.lower_block(body, macro_params, true, false),
772 span: Some(span.into()),
773 }
774 }
775 Stmt::While {
776 condition,
777 body,
778 span,
779 } => HirStmt::While {
780 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
781 body: self.lower_block(body, macro_params, true, false),
782 span: Some(span.into()),
783 },
784 Stmt::DoWhile {
785 condition,
786 body,
787 span,
788 } => HirStmt::DoWhile {
789 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
790 body: self.lower_block(body, macro_params, true, true),
791 span: Some(span.into()),
792 },
793 Stmt::Switch { value, arms, span } => HirStmt::Switch {
794 value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)),
795 arms: arms
796 .iter()
797 .map(|arm| match arm {
798 cst::SwitchArm::Case { value, body, span } => HirSwitchArm::Case {
799 value: Box::new(self.lower_expr(
800 value,
801 macro_params,
802 CallPosition::Value,
803 )),
804 body: self.lower_block(body, macro_params, true, false),
805 span: Some((*span).into()),
806 },
807 cst::SwitchArm::Default { body, span } => HirSwitchArm::Default {
808 body: self.lower_block(body, macro_params, true, false),
809 span: Some((*span).into()),
810 },
811 })
812 .collect(),
813 span: Some(span.into()),
814 },
815 Stmt::Break { span } => {
816 if !breakable {
817 self.error_at(
818 "break-context",
819 "break is only valid inside a switch or loop".to_string(),
820 *span,
821 );
822 }
823 HirStmt::Break {
824 span: Some(span.into()),
825 }
826 }
827 Stmt::Pass { span } => HirStmt::Pass {
828 span: Some(span.into()),
829 },
830 }
831 }
832
833 fn lower_macro_body(&mut self, body: &[Stmt], params: &[String]) -> Vec<HirStmt> {
834 self.lower_block(body, params, false, false)
835 }
836
837 fn lower_expr(
838 &mut self,
839 expr: &Expr,
840 macro_params: &[String],
841 position: CallPosition,
842 ) -> HirExpr {
843 match expr {
844 Expr::Number { value, text, span } => HirExpr::Number {
845 value: *value,
846 text: text.clone(),
847 span: Some(span.into()),
848 },
849 Expr::String { value, span } => HirExpr::String {
850 value: value.clone(),
851 span: Some(span.into()),
852 },
853 Expr::Bool { value, span } => HirExpr::Bool {
854 value: *value,
855 span: Some(span.into()),
856 },
857 Expr::Null { span } => HirExpr::Null {
858 span: Some(span.into()),
859 },
860 Expr::Array { elements, span } => HirExpr::Array {
861 elements: elements
862 .iter()
863 .map(|element| self.lower_expr(element, macro_params, CallPosition::Value))
864 .collect(),
865 span: Some(span.into()),
866 },
867 Expr::Dict { entries, span } => {
868 if !self.allow_dict_literal {
869 self.error_at(
870 "dict-access",
871 "dictionary literals must be accessed by a key".to_string(),
872 *span,
873 );
874 return HirExpr::Null { span: None };
875 }
876 HirExpr::Dict {
877 entries: entries
878 .iter()
879 .map(|entry| HirDictEntry {
880 key: Box::new(self.lower_expr(
881 &entry.key,
882 macro_params,
883 CallPosition::Value,
884 )),
885 value: Box::new(self.lower_expr(
886 &entry.value,
887 macro_params,
888 CallPosition::Value,
889 )),
890 span: Some(entry.span.into()),
891 })
892 .collect(),
893 span: Some(span.into()),
894 }
895 }
896 Expr::Comprehension {
897 element,
898 variable,
899 variable_span,
900 index,
901 iterable,
902 condition,
903 span,
904 } => {
905 let iterable = self.lower_expr(iterable, macro_params, CallPosition::Value);
906 let previous = std::mem::take(&mut self.locals);
907 self.locals.push(variable.clone());
908 if let Some((index, _)) = index {
909 self.locals.push(index.clone());
910 }
911 let element = self.lower_expr(element, macro_params, CallPosition::Value);
912 let condition = condition.as_ref().map(|condition| {
913 Box::new(self.lower_expr(condition, macro_params, CallPosition::Value))
914 });
915 self.locals = previous;
916 HirExpr::Comprehension {
917 element: Box::new(element),
918 variable: variable.clone(),
919 variable_span: Some(variable_span.into()),
920 index: index.as_ref().map(|(name, _)| name.clone()),
921 index_span: index.as_ref().map(|(_, span)| (*span).into()),
922 iterable: Box::new(iterable),
923 condition,
924 span: Some(span.into()),
925 }
926 }
927 Expr::Lambda { params, body, span } => {
928 if position != CallPosition::LambdaArgument {
929 self.error_at(
930 "lambda-context",
931 "lambda expressions are only valid as array operation arguments"
932 .to_string(),
933 *span,
934 );
935 return HirExpr::Null { span: None };
936 }
937 let previous = std::mem::take(&mut self.locals);
938 self.locals = params.iter().map(|(name, _)| name.clone()).collect();
939 let body = self.lower_expr(body, macro_params, CallPosition::Value);
940 self.locals = previous;
941 HirExpr::Lambda {
942 params: params.iter().map(|(name, _)| name.clone()).collect(),
943 param_spans: params
944 .iter()
945 .map(|(_, span)| Some((*span).into()))
946 .collect(),
947 body: Box::new(body),
948 span: Some(span.into()),
949 }
950 }
951 Expr::StringModifier {
952 modifier,
953 value,
954 format_text,
955 interpolations,
956 span,
957 } => {
958 if *modifier == 'f' {
959 if let Some(format_text) = format_text {
960 if !interpolations.is_empty() {
961 return HirExpr::Format {
962 text: format_text.clone(),
963 args: interpolations
964 .iter()
965 .map(|expr| {
966 self.lower_expr(expr, macro_params, CallPosition::Value)
967 })
968 .collect(),
969 span: Some(span.into()),
970 };
971 }
972 return HirExpr::String {
973 value: format_text.clone(),
974 span: Some(span.into()),
975 };
976 }
977 }
978 HirExpr::StringModifier {
979 modifier: modifier.to_string(),
980 value: value.clone(),
981 span: Some(span.into()),
982 }
983 }
984 Expr::Name { name, span } => self.lower_name(name, *span, macro_params),
985 Expr::Member {
986 receiver,
987 member,
988 member_span,
989 span,
990 } => self.lower_member(receiver, member, *member_span, *span, macro_params),
991 Expr::Index { array, index, span } => {
992 let previous = self.allow_dict_literal;
993 self.allow_dict_literal = true;
994 let array = self.lower_expr(array, macro_params, CallPosition::Value);
995 self.allow_dict_literal = previous;
996 HirExpr::Index {
997 array: Box::new(array),
998 index: Box::new(self.lower_expr(index, macro_params, CallPosition::Value)),
999 span: Some(span.into()),
1000 }
1001 }
1002 Expr::Call { name, args, span } => {
1003 self.lower_call(name, args, *span, macro_params, position)
1004 }
1005 Expr::ReceiverCall {
1006 receiver,
1007 name,
1008 args,
1009 span,
1010 } => self.lower_receiver_call(receiver, name, args, *span, macro_params, position),
1011 Expr::Binary {
1012 op,
1013 left,
1014 right,
1015 span,
1016 } => HirExpr::Binary {
1017 op: op.clone(),
1018 left: Box::new(self.lower_expr(left, macro_params, CallPosition::Value)),
1019 right: Box::new(self.lower_expr(right, macro_params, CallPosition::Value)),
1020 span: Some(span.into()),
1021 },
1022 Expr::Unary { op, operand, span } => HirExpr::Unary {
1023 op: op.clone(),
1024 operand: Box::new(self.lower_expr(operand, macro_params, CallPosition::Value)),
1025 span: Some(span.into()),
1026 },
1027 }
1028 }
1029
1030 fn lower_name(&mut self, name: &str, span: Span, macro_params: &[String]) -> HirExpr {
1031 if macro_params.iter().any(|param| param == name) {
1032 return HirExpr::MacroParam {
1033 name: name.to_string(),
1034 span: Some(span.into()),
1035 };
1036 }
1037 if self.locals.iter().any(|local| local == name) {
1038 return HirExpr::Local {
1039 name: name.to_string(),
1040 span: Some(span.into()),
1041 };
1042 }
1043 match name {
1044 "eventPlayer" => HirExpr::EventPlayer {
1045 span: Some(span.into()),
1046 },
1047 _ if self.globals.contains(name) => HirExpr::GlobalVar {
1048 name: name.to_string(),
1049 span: Some(span.into()),
1050 },
1051 _ if self.players.contains(name) => HirExpr::PlayerVar {
1052 player: Box::new(HirExpr::EventPlayer { span: None }),
1053 name: name.to_string(),
1054 span: Some(span.into()),
1055 },
1056 _ if self.enums.contains_key(name) => {
1057 self.error_at(
1058 "enum-type-without-member",
1059 format!("enum type '{name}' must be used with a member (e.g. {name}.MEMBER)"),
1060 span,
1061 );
1062 HirExpr::Null { span: None }
1063 }
1064 _ if default_var_index(name).is_some() => HirExpr::GlobalVar {
1071 name: name.to_string(),
1072 span: Some(span.into()),
1073 },
1074 _ => {
1075 self.error_at(
1076 "unknown-identifier",
1077 format!("unknown identifier '{name}'"),
1078 span,
1079 );
1080 HirExpr::Null { span: None }
1081 }
1082 }
1083 }
1084
1085 fn lower_member(
1086 &mut self,
1087 receiver: &Expr,
1088 member: &str,
1089 member_span: Span,
1090 span: Span,
1091 _macro_params: &[String],
1092 ) -> HirExpr {
1093 if let Expr::Name { name, .. } = receiver {
1094 if let Some(members) = self.enums.get(name) {
1096 return match members.iter().position(|candidate| candidate == member) {
1097 Some(index) => HirExpr::Number {
1098 value: index as f64,
1099 text: index.to_string(),
1100 span: Some(span.into()),
1101 },
1102 None => {
1103 self.error_at(
1104 "unknown-enum-member",
1105 format!("enum '{name}' has no member '{member}'"),
1106 span,
1107 );
1108 HirExpr::Null { span: None }
1109 }
1110 };
1111 }
1112 if self.manifest.domain_identity(name) {
1118 if let Some(domain) = self.catalog.enum_domain(name)
1119 && !domain
1120 .members
1121 .iter()
1122 .any(|candidate| candidate.member == *member)
1123 {
1124 self.error_at(
1125 "unknown-enum-member",
1126 format!("enum '{name}' has no member '{member}'"),
1127 span,
1128 );
1129 return HirExpr::Null { span: None };
1130 }
1131 return HirExpr::Enum {
1132 value_type: name.clone(),
1133 value: member.to_string(),
1134 span: Some(span.into()),
1135 };
1136 }
1137 if name == "eventPlayer" {
1139 return HirExpr::PlayerVar {
1140 player: Box::new(HirExpr::EventPlayer { span: None }),
1141 name: member.to_string(),
1142 span: Some(span.into()),
1143 };
1144 }
1145 if name == "random" {
1147 self.error_at(
1148 "unsupported-member",
1149 format!("module member '{name}.{member}' must be called"),
1150 span,
1151 );
1152 return HirExpr::Null { span: None };
1153 }
1154 if self.globals.contains(name)
1159 || self.players.contains(name)
1160 || default_var_index(name).is_some()
1161 {
1162 return HirExpr::Member {
1163 receiver: Box::new(self.lower_name(name, receiver.span(), &[])),
1164 member: member.to_string(),
1165 member_span: Some(member_span.into()),
1166 span: Some(span.into()),
1167 };
1168 }
1169 }
1170 self.error_at(
1171 "unsupported-member",
1172 "unsupported member access on this expression".to_string(),
1173 span,
1174 );
1175 HirExpr::Null { span: None }
1176 }
1177
1178 fn lower_call(
1179 &mut self,
1180 name: &str,
1181 args: &[cst::CallArg],
1182 span: Span,
1183 macro_params: &[String],
1184 position: CallPosition,
1185 ) -> HirExpr {
1186 if !self.macros.contains(name) && !self.subroutines.contains(name) && name != "sorted" {
1189 match self.manifest.resolve_function(name) {
1190 Some(entry) => self.check_call_position(name, entry, position, span),
1191 None => {
1192 let (code, message) = match position {
1193 CallPosition::Statement => {
1194 ("unknown-action", format!("unknown action '{name}'"))
1195 }
1196 CallPosition::Value => ("unknown-value", format!("unknown value '{name}'")),
1197 CallPosition::ForIterable => (
1198 "invalid-iterable",
1199 format!("for-loop iterable '{name}' must be a range(...) call"),
1200 ),
1201 CallPosition::LambdaArgument => {
1202 ("unknown-value", format!("unknown value '{name}'"))
1203 }
1204 };
1205 self.error_at(code, message, span);
1206 }
1207 }
1208 }
1209 match name {
1210 "sorted" => HirExpr::Call {
1211 name: name.to_string(),
1212 args: self.lower_arg_values_with_lambda(args, macro_params, |index, arg| {
1213 index == 1 || arg.keyword.as_ref().is_some_and(|(name, _)| name == "key")
1214 }),
1215 span: Some(span.into()),
1216 },
1217 "vect" => {
1218 let (bound, _) = match self.manifest.resolve_function(name) {
1223 Some(entry) => self.bind_args(entry, args, macro_params),
1224 None => (self.lower_arg_values(args, macro_params), None),
1225 };
1226 if bound.len() < 3 {
1227 self.error_at(
1228 "vect-arity",
1229 format!(
1230 "vect() expects 3 arguments (x, y, z) but got {}",
1231 args.len()
1232 ),
1233 span,
1234 );
1235 return HirExpr::Null { span: None };
1236 }
1237 HirExpr::Vector {
1238 x: Box::new(bound[0].clone()),
1239 y: Box::new(bound[1].clone()),
1240 z: Box::new(bound[2].clone()),
1241 span: Some(span.into()),
1242 }
1243 }
1244 _ => {
1245 if self.macros.contains(name) {
1246 for arg in args {
1250 if let Some((keyword, span)) = &arg.keyword {
1251 self.error_at(
1252 "keyword-unsupported",
1253 format!(
1254 "macro '{name}' does not accept keyword \
1255 arguments ('{keyword}')"
1256 ),
1257 *span,
1258 );
1259 }
1260 }
1261 return HirExpr::MacroCall {
1262 name: name.to_string(),
1263 args: self.lower_arg_values(args, macro_params),
1264 span: Some(span.into()),
1265 };
1266 }
1267 match self.manifest.resolve_function(name) {
1268 Some(entry) => {
1269 if self.subroutines.contains(name) {
1273 return HirExpr::Call {
1274 name: name.to_string(),
1275 args: self.lower_arg_values(args, macro_params),
1276 span: Some(span.into()),
1277 };
1278 }
1279 let (bound, selector) = self.bind_args(entry, args, macro_params);
1280 let (call_name, bound) =
1281 self.resolve_contextual_domain(entry, bound, selector.as_deref());
1282 HirExpr::Call {
1283 name: call_name,
1284 args: bound,
1285 span: Some(span.into()),
1286 }
1287 }
1288 None => HirExpr::Call {
1289 name: name.to_string(),
1290 args: self.lower_arg_values(args, macro_params),
1291 span: Some(span.into()),
1292 },
1293 }
1294 }
1295 }
1296 }
1297
1298 fn lower_arg_values(&mut self, args: &[cst::CallArg], macro_params: &[String]) -> Vec<HirExpr> {
1301 self.lower_arg_values_with_lambda(args, macro_params, |_, _| false)
1302 }
1303
1304 fn lower_arg_values_with_lambda(
1305 &mut self,
1306 args: &[cst::CallArg],
1307 macro_params: &[String],
1308 allows_lambda: impl Fn(usize, &cst::CallArg) -> bool,
1309 ) -> Vec<HirExpr> {
1310 args.iter()
1311 .enumerate()
1312 .map(|(index, arg)| {
1313 let position = if allows_lambda(index, arg) {
1314 CallPosition::LambdaArgument
1315 } else {
1316 CallPosition::Value
1317 };
1318 self.lower_expr(&arg.value, macro_params, position)
1319 })
1320 .collect()
1321 }
1322
1323 fn bind_args(
1335 &mut self,
1336 entry: &Function,
1337 args: &[cst::CallArg],
1338 macro_params: &[String],
1339 ) -> (Vec<HirExpr>, Option<String>) {
1340 let mut slots: Vec<Option<HirExpr>> = vec![None; entry.params.len()];
1341 let mut selector = None;
1342 let mut has_keyword = false;
1343 let mut binding_error = false;
1344 let contextual = entry.contextual_domain.as_ref();
1345
1346 let mut by_spelling: HashMap<&str, usize> = HashMap::new();
1350 for (index, param) in entry.params.iter().enumerate() {
1351 by_spelling.insert(param.name.as_str(), index);
1352 for alternate in ¶m.alternate_names {
1353 by_spelling.insert(alternate.as_str(), index);
1354 }
1355 }
1356
1357 for (arg_index, arg) in args.iter().enumerate() {
1358 match &arg.keyword {
1359 Some((keyword, name_span)) => {
1360 if !entry.keyword_args {
1361 binding_error = true;
1362 self.error_at(
1363 "keyword-unsupported",
1364 format!(
1365 "function '{}' does not accept keyword arguments ('{keyword}')",
1366 entry.id
1367 ),
1368 *name_span,
1369 );
1370 continue;
1371 }
1372 has_keyword = true;
1373 match by_spelling.get(keyword.as_str()) {
1374 None => {
1375 binding_error = true;
1376 self.error_at(
1377 "unknown-keyword",
1378 format!(
1379 "unknown keyword argument '{keyword}' for function '{}'",
1380 entry.id
1381 ),
1382 *name_span,
1383 );
1384 }
1385 Some(&index) => {
1386 let param = &entry.params[index];
1387 if param.positional_only {
1388 binding_error = true;
1389 self.error_at(
1390 "unknown-keyword",
1391 format!(
1392 "parameter '{}' of '{}' cannot be bound by keyword",
1393 param.name, entry.id
1394 ),
1395 *name_span,
1396 );
1397 } else if slots[index].is_some() {
1398 binding_error = true;
1399 self.error_at(
1400 "duplicate-argument",
1401 format!(
1402 "argument '{}' of function '{}' is defined twice",
1403 keyword, entry.id
1404 ),
1405 *name_span,
1406 );
1407 } else {
1408 slots[index] = Some(self.lower_call_arg_value(
1409 entry,
1410 index,
1411 arg,
1412 macro_params,
1413 ));
1414 if contextual.is_some_and(|c| c.by == param.name) {
1415 selector = Some(keyword.clone());
1416 }
1417 }
1418 }
1419 }
1420 }
1421 None => {
1422 if has_keyword && entry.contextual_domain.is_none() {
1428 binding_error = true;
1429 self.error_at(
1430 "positional-after-keyword",
1431 format!(
1432 "cannot use positional arguments after keyword \
1433 arguments in call to '{}'",
1434 entry.id
1435 ),
1436 arg.value.span(),
1437 );
1438 }
1439 let index = arg_index;
1443 if index < entry.params.len() {
1444 let param = &entry.params[index];
1445 if param.keyword_only {
1446 binding_error = true;
1447 self.error_at(
1448 "keyword-required",
1449 format!(
1450 "argument {} of '{}' must be passed as a keyword \
1451 (name = value; accepted names: {})",
1452 index + 1,
1453 entry.id,
1454 keyword_spellings(param).join(", ")
1455 ),
1456 arg.value.span(),
1457 );
1458 }
1459 if slots[index].is_none() {
1460 slots[index] =
1461 Some(self.lower_call_arg_value(entry, index, arg, macro_params));
1462 }
1463 } else {
1464 self.lower_expr(&arg.value, macro_params, CallPosition::Value);
1465 }
1466 }
1467 }
1468 }
1469
1470 if !binding_error && args.len() > entry.params.len() {
1476 self.check_arity(entry, args.len(), arg_span(args));
1477 }
1478
1479 let mut bound: Vec<HirExpr> = Vec::with_capacity(entry.params.len());
1483 for (index, param) in entry.params.iter().enumerate() {
1484 match &slots[index] {
1485 Some(value) => bound.push(value.clone()),
1486 None => match ¶m.default {
1487 Some(ParamDefault::EnumMember(member)) => {
1488 let domain = param.domain.clone().unwrap_or_default();
1489 bound.push(HirExpr::Enum {
1490 value_type: domain,
1491 value: member.clone(),
1492 span: None,
1493 });
1494 }
1495 Some(ParamDefault::Number(number)) => {
1496 bound.push(HirExpr::Number {
1497 value: *number,
1498 text: format!("{number}"),
1499 span: None,
1500 });
1501 }
1502 None if param.optional => {
1503 }
1506 None => {
1507 self.error_at(
1508 "missing-argument",
1509 format!(
1510 "missing argument '{}' for function '{}'",
1511 param.name, entry.id
1512 ),
1513 arg_span(args),
1514 );
1515 bound.push(HirExpr::Null { span: None });
1516 }
1517 },
1518 }
1519 }
1520
1521 for (index, param) in entry.params.iter().enumerate() {
1524 if !param.variable {
1525 continue;
1526 }
1527 if let Some(Some(value)) = slots.get(index) {
1528 if !matches!(value, HirExpr::GlobalVar { .. } | HirExpr::PlayerVar { .. }) {
1529 self.error_at(
1530 "invalid-argument",
1531 format!(
1532 "argument {} of '{}' must be a variable (globalvar or \
1533 playervar)",
1534 index + 1,
1535 entry.id
1536 ),
1537 arg_span(args),
1538 );
1539 }
1540 }
1541 }
1542
1543 (bound, selector)
1544 }
1545
1546 fn lower_call_arg_value(
1553 &mut self,
1554 entry: &Function,
1555 param_index: usize,
1556 arg: &cst::CallArg,
1557 macro_params: &[String],
1558 ) -> HirExpr {
1559 if let Some(contextual) = &entry.contextual_domain {
1560 let is_contextual = entry.params[param_index]
1561 .domain
1562 .as_deref()
1563 .is_some_and(|domain| domain == contextual.domain);
1564 if is_contextual {
1565 if let Expr::Member {
1566 receiver,
1567 member,
1568 span,
1569 ..
1570 } = &arg.value
1571 {
1572 if let Expr::Name { name, .. } = receiver.as_ref() {
1573 if name == &contextual.domain {
1574 return HirExpr::Enum {
1575 value_type: contextual.domain.clone(),
1576 value: member.clone(),
1577 span: Some((*span).into()),
1578 };
1579 }
1580 }
1581 }
1582 }
1583 }
1584 self.lower_expr(&arg.value, macro_params, CallPosition::Value)
1585 }
1586
1587 fn resolve_contextual_domain(
1597 &mut self,
1598 entry: &Function,
1599 mut bound: Vec<HirExpr>,
1600 selector: Option<&str>,
1601 ) -> (String, Vec<HirExpr>) {
1602 let Some(contextual) = &entry.contextual_domain else {
1603 return (entry.id.clone(), bound);
1604 };
1605 let Some(contextual_param) = entry
1606 .params
1607 .iter()
1608 .position(|param| param.domain.as_deref() == Some(contextual.domain.as_str()))
1609 else {
1610 return (entry.id.clone(), bound);
1611 };
1612 let HirExpr::Enum {
1616 value_type,
1617 value,
1618 span: value_span,
1619 } = &bound[contextual_param]
1620 else {
1621 return (entry.id.clone(), bound);
1622 };
1623 if value_type != &contextual.domain {
1624 return (entry.id.clone(), bound);
1625 }
1626 let Some(keyword) = selector else {
1627 return (entry.id.clone(), bound);
1628 };
1629 let Some(option) = contextual.options.get(keyword) else {
1630 return (entry.id.clone(), bound);
1631 };
1632 bound[contextual_param] = HirExpr::Enum {
1633 value_type: option.domain.clone(),
1634 value: value.clone(),
1635 span: *value_span,
1636 };
1637 (option.target.clone(), bound)
1638 }
1639
1640 fn lower_receiver_call(
1641 &mut self,
1642 receiver: &Expr,
1643 name: &str,
1644 args: &[cst::CallArg],
1645 span: Span,
1646 macro_params: &[String],
1647 position: CallPosition,
1648 ) -> HirExpr {
1649 if matches!(name, "map" | "filter" | "all" | "any") {
1650 let lowered = HirExpr::ReceiverCall {
1651 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1652 name: name.to_string(),
1653 args: self.lower_arg_values_with_lambda(args, macro_params, |index, _| index == 0),
1654 span: Some(span.into()),
1655 };
1656 return lowered;
1657 }
1658 if let Expr::Name { name: root, .. } = receiver {
1660 if root == "random" {
1661 return self.lower_call(
1662 &format!("random.{name}"),
1663 args,
1664 span,
1665 macro_params,
1666 position,
1667 );
1668 }
1669 }
1670 if let Expr::String { value, .. } = receiver {
1674 if name == "format" {
1675 if args.iter().any(|arg| arg.keyword.is_some()) {
1676 for arg in args {
1677 if let Some((keyword, span)) = &arg.keyword {
1678 self.error_at(
1679 "keyword-unsupported",
1680 format!(
1681 "function 'format' does not accept keyword \
1682 arguments ('{keyword}')"
1683 ),
1684 *span,
1685 );
1686 }
1687 }
1688 }
1689 let lowered: Vec<HirExpr> = self.lower_arg_values(args, macro_params);
1690 if let Some(entry) = self.manifest.resolve_member("format") {
1691 self.check_call_position("format", entry, position, span);
1692 }
1693 return HirExpr::Format {
1694 text: value.clone(),
1695 args: lowered,
1696 span: Some(span.into()),
1697 };
1698 }
1699 }
1700 let (member_name, lowered) = match self.manifest.resolve_member(name) {
1703 Some(entry) => {
1704 self.check_call_position(name, entry, position, span);
1705 if let Some(category) = entry.receiver {
1706 self.check_receiver(receiver, category, entry, span);
1707 }
1708 let (bound, _) = self.bind_args(entry, args, macro_params);
1709 (entry.id.clone(), bound)
1710 }
1711 None => {
1712 self.error_at("unknown-member", format!("unknown member '{name}'"), span);
1713 (name.to_string(), self.lower_arg_values(args, macro_params))
1714 }
1715 };
1716 if let Expr::Name { name: root, .. } = receiver {
1718 if root == "eventPlayer" {
1719 return HirExpr::ReceiverCall {
1720 receiver: Box::new(HirExpr::EventPlayer { span: None }),
1721 name: member_name,
1722 args: lowered,
1723 span: Some(span.into()),
1724 };
1725 }
1726 }
1727 HirExpr::ReceiverCall {
1729 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1730 name: member_name,
1731 args: lowered,
1732 span: Some(span.into()),
1733 }
1734 }
1735
1736 fn check_call_position(
1739 &mut self,
1740 name: &str,
1741 entry: &Function,
1742 position: CallPosition,
1743 span: Span,
1744 ) {
1745 match position {
1746 CallPosition::Statement => {
1747 if entry.context == Some(FunctionContext::ForIterable) {
1748 self.error_at(
1749 "invalid-call-context",
1750 format!("'{name}' is only valid as a for-loop iterable"),
1751 span,
1752 );
1753 } else if entry.kind.is_value() {
1754 self.error_at(
1755 "value-in-action-position",
1756 format!("value function '{name}' cannot be used as an action"),
1757 span,
1758 );
1759 }
1760 }
1761 CallPosition::Value => {
1762 if entry.kind.is_action() {
1763 self.error_at(
1764 "action-in-value-position",
1765 format!("action function '{name}' cannot be used as a value"),
1766 span,
1767 );
1768 } else if entry.context == Some(FunctionContext::ForIterable) {
1769 self.error_at(
1770 "invalid-call-context",
1771 format!("'{name}' is only valid as a for-loop iterable"),
1772 span,
1773 );
1774 }
1775 }
1776 CallPosition::ForIterable => {
1777 if entry.context != Some(FunctionContext::ForIterable) {
1778 self.error_at(
1779 "invalid-iterable",
1780 format!("for-loop iterable '{name}' must be a range(...) call"),
1781 span,
1782 );
1783 }
1784 }
1785 CallPosition::LambdaArgument => {
1786 if entry.kind.is_action() {
1787 self.error_at(
1788 "action-in-value-position",
1789 format!("action function '{name}' cannot be used as a value"),
1790 span,
1791 );
1792 } else if entry.context == Some(FunctionContext::ForIterable) {
1793 self.error_at(
1794 "invalid-call-context",
1795 format!("'{name}' is only valid as a for-loop iterable"),
1796 span,
1797 );
1798 }
1799 }
1800 }
1801 }
1802
1803 fn check_receiver(
1809 &mut self,
1810 receiver: &Expr,
1811 category: ReceiverCategory,
1812 entry: &Function,
1813 span: Span,
1814 ) {
1815 let mismatch = match category {
1816 ReceiverCategory::String => !matches!(receiver, Expr::String { .. }),
1817 ReceiverCategory::Variable => !assignable_receiver(receiver),
1818 ReceiverCategory::Player | ReceiverCategory::Any => false,
1819 };
1820 if mismatch {
1821 self.error_at(
1822 "invalid-receiver",
1823 format!(
1824 "member '{}' requires {} as its receiver",
1825 entry.id,
1826 category.describe()
1827 ),
1828 span,
1829 );
1830 }
1831 }
1832
1833 fn check_arity(&mut self, entry: &Function, got: usize, span: Span) {
1835 let (min, max) = entry.arity_bounds();
1836 let valid = got >= min && max.is_none_or(|max| got <= max);
1837 if !valid {
1838 let expects = match max {
1839 Some(max) if min == max => format!("exactly {min}"),
1840 Some(max) => format!("{min} to {max}"),
1841 None => format!("at least {min}"),
1842 };
1843 let role = match entry.kind {
1844 FunctionKind::Action => "action",
1845 FunctionKind::Value => "value",
1846 FunctionKind::MemberAction => "member action",
1847 FunctionKind::MemberValue => "member value",
1848 };
1849 self.error_at(
1850 "invalid-arity",
1851 format!(
1852 "{role} '{}' expects {expects} arguments but got {got}",
1853 entry.id
1854 ),
1855 span,
1856 );
1857 }
1858 }
1859
1860 fn error_at(&mut self, code: &str, message: String, span: Span) {
1861 self.errors.push(OpyError::at(code, message, span));
1862 }
1863}
1864
1865fn keyword_spellings(param: &Param) -> Vec<String> {
1867 let mut spellings = vec![param.name.clone()];
1868 spellings.extend(param.alternate_names.iter().cloned());
1869 spellings
1870}
1871
1872fn arg_span(args: &[CallArg]) -> Span {
1875 args.first().map(CallArg::span).unwrap_or_else(|| {
1876 Span::new(
1877 0,
1878 crate::diag::Position::new(1, 1),
1879 crate::diag::Position::new(1, 1),
1880 )
1881 })
1882}
1883
1884fn assignable_receiver(receiver: &Expr) -> bool {
1889 match receiver {
1890 Expr::Name { name, .. } => name != "eventPlayer",
1891 Expr::Array { .. } | Expr::Index { .. } => true,
1892 _ => false,
1893 }
1894}
1895
1896impl From<Span> for HirSpan {
1897 fn from(span: Span) -> HirSpan {
1898 HirSpan {
1899 file: span.file,
1900 start: Position {
1901 line: span.start.line,
1902 col: span.start.col,
1903 },
1904 end: Position {
1905 line: span.end.line,
1906 col: span.end.col,
1907 },
1908 }
1909 }
1910}
1911
1912impl From<&Span> for HirSpan {
1913 fn from(span: &Span) -> HirSpan {
1914 (*span).into()
1915 }
1916}
1917
1918#[cfg(test)]
1919mod tests {
1920 use super::*;
1921 use crate::hir::types::{Expr as HirExpr, RuleEntry as HirRuleEntry, Stmt as HirStmt};
1922 use crate::lexer::{LexInput, lex};
1923 use crate::parser::parse;
1924
1925 fn lower_ok(text: &str) -> HirProgram {
1926 let tokens = lex(LexInput { file_id: 0, text }).expect("lexes");
1927 let output = parse(&tokens);
1928 assert!(
1929 output.errors.is_empty(),
1930 "unexpected parse errors: {:?}",
1931 output.errors
1932 );
1933 let program = output.program.expect("parse produces a program");
1934 lower(&program, vec![], vec![]).expect("lowers without errors")
1935 }
1936
1937 fn rule_conditions_and_actions(hir: &HirProgram) -> (&Vec<HirExpr>, &Vec<HirStmt>) {
1938 let HirRuleEntry::Rule(rule) = &hir.rules[0] else {
1939 panic!("expected a rule");
1940 };
1941 (&rule.conditions, &rule.actions)
1942 }
1943
1944 #[test]
1945 fn producer_emits_the_v2_ordered_switch_contract() {
1946 let hir = lower_ok(
1947 "globalvar value\nrule \"r\":\n @Event global\n switch value:\n default:\n value = 1\n case 2:\n value = 2\n",
1948 );
1949 assert_eq!(hir.protocol.name, "wright/opy-hir");
1950 assert_eq!(hir.protocol.version, "2.0.0");
1951 let value = serde_json::to_value(&hir).expect("HIR must serialize");
1952 let switch = &value["rules"][0]["actions"][0];
1953 assert!(switch.get("arms").is_some());
1954 assert!(switch.get("cases").is_none());
1955 assert!(switch.get("default").is_none());
1956 }
1957
1958 #[test]
1959 fn receiver_calls_lower_to_receiver_call_hir() {
1960 let hir = lower_ok(
1964 "globalvar target\nrule \"r\":\n @Event eachPlayer\n eventPlayer.setMoveSpeed(100)\n target.setMoveSpeed(50)\n",
1965 );
1966 let (_, actions) = rule_conditions_and_actions(&hir);
1967 assert_eq!(actions.len(), 2);
1968
1969 let HirStmt::Expr { expr, .. } = &actions[0] else {
1970 panic!("expected expression statement");
1971 };
1972 let HirExpr::ReceiverCall {
1973 receiver,
1974 name,
1975 args,
1976 ..
1977 } = expr.as_ref()
1978 else {
1979 panic!("expected receiver call, got {expr:?}");
1980 };
1981 assert_eq!(name, "setMoveSpeed");
1982 assert!(matches!(receiver.as_ref(), HirExpr::EventPlayer { .. }));
1983 assert_eq!(args.len(), 1);
1984 assert!(matches!(&args[0], HirExpr::Number { .. }));
1985
1986 let HirStmt::Expr { expr, .. } = &actions[1] else {
1987 panic!("expected expression statement");
1988 };
1989 let HirExpr::ReceiverCall { receiver, name, .. } = expr.as_ref() else {
1990 panic!("expected receiver call, got {expr:?}");
1991 };
1992 assert_eq!(name, "setMoveSpeed");
1993 assert!(
1994 matches!(receiver.as_ref(), HirExpr::GlobalVar { name, .. } if name == "target"),
1995 "globalvar receiver must resolve to a GlobalVar"
1996 );
1997 }
1998
1999 #[test]
2000 fn bare_variable_member_expression_preserves_receiver_and_member() {
2001 let hir = lower_ok(
2002 "globalvar A\nplayervar B\nrule \"receiver\":\n @Event eachPlayer\n A = B.C\n",
2003 );
2004 let HirStmt::Assign { value, .. } = &hir
2005 .rules
2006 .iter()
2007 .find_map(|entry| {
2008 let RuleEntry::Rule(rule) = entry else {
2009 return None;
2010 };
2011 rule.actions.first()
2012 })
2013 .expect("assignment")
2014 else {
2015 panic!("expected assignment");
2016 };
2017 let HirExpr::Member {
2018 receiver, member, ..
2019 } = value.as_ref()
2020 else {
2021 panic!("expected opaque member expression, got {value:?}");
2022 };
2023 assert_eq!(member, "C");
2024 assert!(matches!(receiver.as_ref(), HirExpr::PlayerVar { name, .. } if name == "B"));
2025 }
2026
2027 #[test]
2028 fn rule_prefix_template_is_global_and_subroutine_identity_is_preserved() {
2029 let text = "rule \"before\":\n pass\ndef source_name():\n @Name \"Friendly\"\n pass\nrule \"after\":\n pass\n";
2030 let tokens = lex(LexInput { file_id: 0, text }).expect("lexes");
2031 let output = parse(&tokens);
2032 assert!(
2033 output.errors.is_empty(),
2034 "unexpected parse errors: {:?}",
2035 output.errors
2036 );
2037 let program = output.program.expect("program");
2038 let preprocessing = PreprocessingState {
2039 rule_prefix_template: Some(crate::hir::types::DirectiveValue {
2040 value: "f\"[{$pathTitle.replace('_', ' ')}] {$rule}\" if $rule and not $isDelimiter else $rule".to_string(),
2041 span: None,
2042 }),
2043 ..PreprocessingState::default()
2044 };
2045 let hir = lower_with_preprocessing(
2046 &program,
2047 vec![SourceFile {
2048 id: 0,
2049 path: "main.opy".to_string(),
2050 }],
2051 vec![],
2052 &preprocessing,
2053 )
2054 .expect("lowers");
2055 let names: Vec<_> = hir
2056 .rules
2057 .iter()
2058 .map(|entry| match entry {
2059 HirRuleEntry::Rule(rule) => rule.name.clone(),
2060 HirRuleEntry::SubroutineDef { name, .. } => name.clone(),
2061 })
2062 .collect();
2063 assert_eq!(
2064 names,
2065 vec!["[Main] before", "[Main] Friendly", "[Main] after"]
2066 );
2067 let HirRuleEntry::SubroutineDef {
2068 name, source_name, ..
2069 } = &hir.rules[1]
2070 else {
2071 panic!("expected subroutine definition");
2072 };
2073 assert_eq!(name, "[Main] Friendly");
2074 assert_eq!(source_name, "source_name");
2075 }
2076
2077 #[test]
2078 fn receiver_call_values_lower_in_conditions() {
2079 let hir = lower_ok(
2083 "rule \"r\":\n @Event eachPlayer\n @Condition eventPlayer.isAlive()\n eventPlayer.teleport(eventPlayer.getPosition())\n",
2084 );
2085 let (conditions, actions) = rule_conditions_and_actions(&hir);
2086 assert_eq!(conditions.len(), 1);
2087 let HirExpr::ReceiverCall { name, args, .. } = &conditions[0] else {
2088 panic!("expected receiver call condition, got {:?}", conditions[0]);
2089 };
2090 assert_eq!(name, "isAlive");
2091 assert_eq!(args.len(), 0);
2092
2093 let HirStmt::Expr { expr, .. } = &actions[0] else {
2094 panic!("expected expression statement");
2095 };
2096 let HirExpr::ReceiverCall {
2097 name,
2098 args,
2099 receiver,
2100 ..
2101 } = expr.as_ref()
2102 else {
2103 panic!("expected receiver call, got {expr:?}");
2104 };
2105 assert_eq!(name, "teleport");
2106 assert!(matches!(receiver.as_ref(), HirExpr::EventPlayer { .. }));
2107 assert_eq!(args.len(), 1);
2108 assert!(matches!(
2109 &args[0],
2110 HirExpr::ReceiverCall { name, .. } if name == "getPosition"
2111 ));
2112 }
2113
2114 #[test]
2115 fn format_string_receiver_stays_a_format_node() {
2116 let hir = lower_ok(
2119 "rule \"r\":\n @Event global\n print(\"{} points\".format(len([1, 2])))\n",
2120 );
2121 let (_, actions) = rule_conditions_and_actions(&hir);
2122 let HirStmt::Expr { expr, .. } = &actions[0] else {
2123 panic!("expected expression statement");
2124 };
2125 assert!(
2126 has_format(expr),
2127 "string `.format()` must lower to a Format node"
2128 );
2129 }
2130
2131 fn has_format(expr: &HirExpr) -> bool {
2132 match expr {
2133 HirExpr::Format { .. } => true,
2134 HirExpr::Call { args, .. } => args.iter().any(has_format),
2135 HirExpr::ReceiverCall { args, .. } => args.iter().any(has_format),
2136 _ => false,
2137 }
2138 }
2139
2140 fn lowered_value(source: &str) -> HirExpr {
2142 let program = crate::compile(source, "test.opy", std::path::Path::new(""))
2143 .unwrap_or_else(|error| panic!("compile failed: {error}"));
2144 let RuleEntry::Rule(rule) = &program.rules[0] else {
2145 panic!("expected a rule");
2146 };
2147 let HirStmt::Assign { value, .. } = &rule.actions[0] else {
2148 panic!("expected an assign statement");
2149 };
2150 (**value).clone()
2151 }
2152
2153 #[test]
2154 fn chase_time_reeval_none_lowers_to_the_catalog_enum() {
2155 let value = lowered_value(
2156 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NONE\n",
2157 );
2158 assert_enum(&value, "ChaseTimeReeval", "NONE");
2159 }
2160
2161 #[test]
2162 fn chase_time_reeval_destination_and_duration_lowers_to_the_catalog_enum() {
2163 let value = lowered_value(
2164 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.DESTINATION_AND_DURATION\n",
2165 );
2166 assert_enum(&value, "ChaseTimeReeval", "DESTINATION_AND_DURATION");
2167 }
2168
2169 #[test]
2170 fn chase_rate_reeval_members_lower_to_the_catalog_enum() {
2171 for member in ["NONE", "DESTINATION_AND_RATE"] {
2172 let source = format!(
2173 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseRateReeval.{member}\n"
2174 );
2175 assert_enum(&lowered_value(&source), "ChaseRateReeval", member);
2176 }
2177 }
2178
2179 fn assert_enum(value: &HirExpr, domain: &str, member: &str) {
2182 match value {
2183 HirExpr::Enum {
2184 value_type, value, ..
2185 } => {
2186 assert_eq!(value_type, domain);
2187 assert_eq!(value, member);
2188 }
2189 other => panic!("expected enum {domain}.{member}, got {other:?}"),
2190 }
2191 }
2192
2193 #[test]
2194 fn unknown_chase_time_reeval_member_is_rejected_by_the_catalog() {
2195 let error = crate::compile(
2196 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NOPE\n",
2197 "test.opy",
2198 std::path::Path::new(""),
2199 )
2200 .expect_err("unknown catalog member must be rejected");
2201 assert_eq!(error.code, "unknown-enum-member");
2202 }
2203
2204 #[test]
2205 fn unknown_enum_receiver_is_an_unsupported_member_error() {
2206 let error = crate::compile(
2207 "globalvar g\nrule \"r\":\n @Event global\n g = NotARealEnum.MEMBER\n",
2208 "test.opy",
2209 std::path::Path::new(""),
2210 )
2211 .expect_err("an unknown enum type must fail");
2212 assert_eq!(error.code, "unsupported-member");
2213 let span = error.span.expect("the error is source-located");
2214 assert_eq!(span.start.line, 4);
2215 }
2216
2217 fn compile_error(source: &str, line: u32) -> OpyError {
2221 let error = crate::compile(source, "test.opy", std::path::Path::new(""))
2222 .expect_err("expected a compile failure");
2223 let span = error.span.expect("the error is source-located");
2224 assert_eq!(span.start.line, line, "code '{}'", error.code);
2225 error
2226 }
2227
2228 fn action_source(statement: &str) -> String {
2229 format!("globalvar g\nrule \"r\":\n @Event global\n {statement}\n")
2230 }
2231
2232 #[test]
2233 fn chase_over_time_resolves_and_compiles_with_reference_signatures() {
2234 let hir = crate::compile(
2236 &action_source("chaseOverTime(g, 10, 3, ChaseTimeReeval.NONE)"),
2237 "test.opy",
2238 std::path::Path::new(""),
2239 )
2240 .expect("reference-supported chaseOverTime compiles");
2241 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2242 panic!("expected a rule");
2243 };
2244 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2245 panic!("expected expression statement");
2246 };
2247 let HirExpr::Call { name, args, .. } = expr.as_ref() else {
2248 panic!("expected a call, got {expr:?}");
2249 };
2250 assert_eq!(name, "chaseOverTime");
2251 assert_eq!(args.len(), 4);
2252 assert!(matches!(
2253 &args[3],
2254 HirExpr::Enum { value_type, value, .. }
2255 if value_type == "ChaseTimeReeval" && value == "NONE"
2256 ));
2257
2258 let hir = crate::compile(
2260 &action_source("chaseOverTime(g, 10, 3)"),
2261 "test.opy",
2262 std::path::Path::new(""),
2263 )
2264 .expect("default-reevaluation chaseOverTime compiles");
2265 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2266 panic!("expected a rule");
2267 };
2268 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2269 panic!("expected expression statement");
2270 };
2271 let HirExpr::Call { args, .. } = expr.as_ref() else {
2272 panic!("expected a call");
2273 };
2274 assert_eq!(args.len(), 4);
2275 assert!(matches!(
2276 &args[3],
2277 HirExpr::Enum { value_type, value, .. }
2278 if value_type == "ChaseTimeReeval" && value == "DESTINATION_AND_DURATION"
2279 ));
2280 }
2281
2282 #[test]
2283 fn is_game_in_progress_resolves_as_a_builtin_value() {
2284 let hir = crate::compile(
2286 &action_source("@Condition isGameInProgress() == true"),
2287 "test.opy",
2288 std::path::Path::new(""),
2289 )
2290 .expect("reference-supported isGameInProgress compiles");
2291 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2292 panic!("expected a rule");
2293 };
2294 assert!(matches!(&rule.conditions[0], HirExpr::Binary { .. }));
2295 }
2296
2297 #[test]
2298 fn enum_gated_members_resolve_through_the_manifest() {
2299 let source = "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2303 @Condition eventPlayer.getThrottle() != vect(0, 0, 0)\n \
2304 @Condition worldVector(vect(1, 2, 3), eventPlayer, Transform.ROTATION) != vect(0, 0, 0)\n \
2305 eventPlayer.setInvisibility(Invis.ALL)\n \
2306 eventPlayer.setStatusEffect(eventPlayer, Status.ROOTED, 2)\n";
2307 let hir = crate::compile(source, "test.opy", std::path::Path::new(""))
2308 .expect("enum-gated members compile");
2309 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2310 panic!("expected a rule");
2311 };
2312 assert_eq!(rule.actions.len(), 2);
2313 }
2314
2315 #[test]
2316 fn get_players_in_radius_fills_reference_enum_defaults() {
2317 let hir = crate::compile(
2320 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2321 @Condition len(getPlayersInRadius(eventPlayer.getPosition(), 10)) > 0\n \
2322 disableInspector()\n",
2323 "test.opy",
2324 std::path::Path::new(""),
2325 )
2326 .expect("getPlayersInRadius with defaults compiles");
2327 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2328 panic!("expected a rule");
2329 };
2330 let HirExpr::Binary { left, .. } = &rule.conditions[0] else {
2331 panic!("expected a comparison");
2332 };
2333 let HirExpr::Call { name, args, .. } = left.as_ref() else {
2334 panic!("expected len call");
2335 };
2336 assert_eq!(name, "len");
2337 let HirExpr::Call { name, args, .. } = &args[0] else {
2338 panic!("expected getPlayersInRadius call");
2339 };
2340 assert_eq!(name, "getPlayersInRadius");
2341 assert_eq!(args.len(), 4);
2342 assert!(matches!(
2343 &args[2],
2344 HirExpr::Enum { value_type, value, .. }
2345 if value_type == "Team" && value == "ALL"
2346 ));
2347 assert!(matches!(
2348 &args[3],
2349 HirExpr::Enum { value_type, value, .. }
2350 if value_type == "LosCheck" && value == "OFF"
2351 ));
2352 }
2353
2354 #[test]
2355 fn value_call_in_action_position_is_rejected() {
2356 let error = compile_error(&action_source("isGameInProgress()"), 4);
2357 assert_eq!(error.code, "value-in-action-position");
2358 }
2359
2360 #[test]
2361 fn value_member_in_action_position_is_rejected() {
2362 let error = compile_error(
2365 "globalvar g\nrule \"r\":\n @Event eachPlayer\n eventPlayer.isAlive()\n",
2366 4,
2367 );
2368 assert_eq!(error.code, "value-in-action-position");
2369 }
2370
2371 fn first_action_expr(source: &str) -> HirExpr {
2376 let program = crate::compile(source, "test.opy", std::path::Path::new(""))
2377 .unwrap_or_else(|error| panic!("compile failed: {error}"));
2378 let RuleEntry::Rule(rule) = &program.rules[0] else {
2379 panic!("expected a rule");
2380 };
2381 match &rule.actions[0] {
2382 HirStmt::Expr { expr, .. } => (**expr).clone(),
2383 HirStmt::Assign { value, .. } => (**value).clone(),
2384 other => panic!("expected an expression or assignment, got {other:?}"),
2385 }
2386 }
2387
2388 fn strip_spans(value: &mut serde_json::Value) {
2391 match value {
2392 serde_json::Value::Object(map) => {
2393 map.remove("span");
2394 map.remove("name_span");
2395 for nested in map.values_mut() {
2396 strip_spans(nested);
2397 }
2398 }
2399 serde_json::Value::Array(items) => {
2400 for item in items {
2401 strip_spans(item);
2402 }
2403 }
2404 _ => {}
2405 }
2406 }
2407
2408 #[test]
2409 fn chase_keyword_forms_dispatch_to_the_concrete_chase_functions() {
2410 let expr = first_action_expr(&action_source("chase(g, 10, rate=2, ChaseReeval.NONE)"));
2415 let HirExpr::Call { name, args, .. } = &expr else {
2416 panic!("expected a call, got {expr:?}");
2417 };
2418 assert_eq!(name, "chaseAtRate");
2419 assert!(matches!(
2420 &args[3],
2421 HirExpr::Enum { value_type, value, .. }
2422 if value_type == "ChaseRateReeval" && value == "NONE"
2423 ));
2424
2425 let expr = first_action_expr(&action_source(
2426 "chase(g, 10, duration=3, ChaseReeval.DESTINATION_AND_DURATION)",
2427 ));
2428 let HirExpr::Call { name, args, .. } = &expr else {
2429 panic!("expected a call, got {expr:?}");
2430 };
2431 assert_eq!(name, "chaseOverTime");
2432 assert!(matches!(
2433 &args[3],
2434 HirExpr::Enum { value_type, value, .. }
2435 if value_type == "ChaseTimeReeval" && value == "DESTINATION_AND_DURATION"
2436 ));
2437
2438 let expr = first_action_expr(
2441 "playervar P\nrule \"r\":\n @Event eachPlayer\n \
2442 chase(eventPlayer.P, 0, rate=1, ChaseReeval.NONE)\n",
2443 );
2444 let HirExpr::Call { name, args, .. } = &expr else {
2445 panic!("expected a call, got {expr:?}");
2446 };
2447 assert_eq!(name, "chaseAtRate");
2448 assert!(matches!(&args[0], HirExpr::PlayerVar { .. }));
2449 }
2450
2451 #[test]
2452 fn chase_reeval_is_only_a_standalone_identity_inside_the_chase_context() {
2453 let error = compile_error(&action_source("g = ChaseReeval.NONE"), 4);
2457 assert_eq!(error.code, "unsupported-member");
2458
2459 let expr = first_action_expr(&action_source(
2464 "chase(g, 10, rate=2, ChaseReeval.DESTINATION_AND_DURATION)",
2465 ));
2466 let HirExpr::Call { name, args, .. } = &expr else {
2467 panic!("expected a call, got {expr:?}");
2468 };
2469 assert_eq!(name, "chaseAtRate");
2470 assert!(matches!(
2471 &args[3],
2472 HirExpr::Enum { value_type, value, .. }
2473 if value_type == "ChaseRateReeval" && value == "DESTINATION_AND_DURATION"
2474 ));
2475
2476 let expr = first_action_expr(&action_source("chase(g, 10, rate=2, 5)"));
2479 let HirExpr::Call { name, args, .. } = &expr else {
2480 panic!("expected a call, got {expr:?}");
2481 };
2482 assert_eq!(name, "chase");
2483 assert!(matches!(&args[3], HirExpr::Number { .. }));
2484 }
2485
2486 #[test]
2487 fn chase_requires_the_keyword_rate_or_duration_third_argument() {
2488 let error = compile_error(&action_source("chase(g, 10, 2, ChaseReeval.NONE)"), 4);
2489 assert_eq!(error.code, "keyword-required");
2490 assert!(error.message.contains("rate"));
2491 }
2492
2493 #[test]
2494 fn chase_family_requires_a_variable_first_argument() {
2495 let error = compile_error(&action_source("chase(10, 10, rate=2, ChaseReeval.NONE)"), 4);
2500 assert_eq!(error.code, "invalid-argument");
2501
2502 let error = compile_error(
2503 &action_source("chaseOverTime(10, 0, 30, ChaseTimeReeval.NONE)"),
2504 4,
2505 );
2506 assert_eq!(error.code, "invalid-argument");
2507 }
2508
2509 #[test]
2510 fn keyword_binding_matches_positional_binding_in_hir() {
2511 fn without_spans(expr: &HirExpr) -> serde_json::Value {
2515 let mut value = serde_json::to_value(expr).unwrap();
2516 strip_spans(&mut value);
2517 value
2518 }
2519 let keyword = without_spans(&first_action_expr(&action_source(
2520 "chaseOverTime(g, 10, duration=3)",
2521 )));
2522 let positional = without_spans(&first_action_expr(&action_source(
2523 "chaseOverTime(g, 10, 3)",
2524 )));
2525 assert_eq!(keyword, positional);
2526
2527 let keyword = without_spans(&first_action_expr(&action_source("wait(time=1)")));
2528 let positional = without_spans(&first_action_expr(&action_source("wait(1)")));
2529 assert_eq!(keyword, positional);
2530
2531 let keyword = without_spans(&first_action_expr(&action_source(
2533 "wait(waitBehavior=Wait.IGNORE_CONDITION, time=2)",
2534 )));
2535 let positional = without_spans(&first_action_expr(&action_source("wait(2)")));
2536 assert_eq!(keyword, positional);
2537
2538 let keyword = without_spans(&first_action_expr(&action_source(
2539 "g = vect(x=1, y=2, z=3)",
2540 )));
2541 let positional = without_spans(&first_action_expr(&action_source("g = vect(1, 2, 3)")));
2542 assert_eq!(keyword, positional);
2543 }
2544
2545 #[test]
2546 fn keyword_binding_diagnostics_are_structured_and_source_located() {
2547 let error = compile_error(&action_source("chaseOverTime(g, 10, bogus=1)"), 4);
2549 assert_eq!(error.code, "unknown-keyword");
2550 assert!(error.message.contains("bogus"));
2551
2552 let error = compile_error(
2554 &action_source(
2555 "chaseOverTime(g, 10, 3, ChaseTimeReeval.NONE, \
2556 reevaluation=ChaseTimeReeval.NONE)",
2557 ),
2558 4,
2559 );
2560 assert_eq!(error.code, "duplicate-argument");
2561
2562 let error = compile_error(&action_source("chaseOverTime(g, duration=3, 5)"), 4);
2564 assert_eq!(error.code, "positional-after-keyword");
2565
2566 let error = compile_error(&action_source("chaseOverTime(g, 10)"), 4);
2568 assert_eq!(error.code, "missing-argument");
2569
2570 let error = compile_error(
2573 &action_source("chase(variable=g, destination=10, rate=2, ChaseReeval.NONE)"),
2574 4,
2575 );
2576 assert_eq!(error.code, "unknown-keyword");
2577 }
2578
2579 #[test]
2580 fn keyword_arguments_are_rejected_for_reference_special_cases() {
2581 let error = compile_error(
2584 "globalvar g\nrule \"r\":\n @Event global\n \
2585 for I in range(start=0, stop=3):\n debug(I)\n",
2586 4,
2587 );
2588 assert_eq!(error.code, "keyword-unsupported");
2589
2590 let error = compile_error(&action_source("g = random.uniform(min=1, max=2)"), 4);
2591 assert_eq!(error.code, "keyword-unsupported");
2592
2593 let error = compile_error(&action_source("print(\"{} points\".format(value=1))"), 4);
2594 assert_eq!(error.code, "keyword-unsupported");
2595 }
2596
2597 #[test]
2598 fn wait_uses_the_reference_keyword_names() {
2599 let error = compile_error(&action_source("wait(duration=1)"), 4);
2603 assert_eq!(error.code, "unknown-keyword");
2604 assert!(error.message.contains("duration"));
2605 }
2606
2607 #[test]
2608 fn action_call_in_value_position_is_rejected() {
2609 let error = compile_error(&action_source("g = wait(1)"), 4);
2610 assert_eq!(error.code, "action-in-value-position");
2611 }
2612
2613 #[test]
2614 fn missing_required_argument_is_a_source_located_diagnostic() {
2615 let error = compile_error(&action_source("chaseOverTime(g, 10)"), 4);
2619 assert_eq!(error.code, "missing-argument");
2620 assert!(error.message.contains("duration"));
2621
2622 let error = compile_error(&action_source("chaseOverTime(g, 10, 3, 4, 5)"), 4);
2623 assert_eq!(error.code, "invalid-arity");
2624 }
2625
2626 #[test]
2627 fn missing_member_argument_is_a_source_located_diagnostic() {
2628 let error = compile_error(
2633 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2634 getPlayersInRadius(eventPlayer.getPosition(), 10).setStatusEffect(eventPlayer, 30)\n",
2635 4,
2636 );
2637 assert_eq!(error.code, "missing-argument");
2638 assert!(error.message.contains("duration"));
2639 }
2640
2641 #[test]
2642 fn invalid_receiver_categories_are_rejected() {
2643 let error = compile_error(&action_source("3.append(1)"), 4);
2646 assert_eq!(error.code, "invalid-receiver");
2647 assert!(error.message.contains("append"));
2648
2649 let error = compile_error(&action_source("print(3.format(\"{}\"))"), 4);
2650 assert_eq!(error.code, "invalid-receiver");
2651 assert!(error.message.contains("format"));
2652 }
2653
2654 #[test]
2655 fn cross_domain_enum_arguments_resolve_as_opaque_identities() {
2656 let expr = first_action_expr(&action_source("chaseOverTime(g, 10, 3, Invis.ALL)"));
2660 let HirExpr::Call { args, .. } = &expr else {
2661 panic!("expected a call, got {expr:?}");
2662 };
2663 assert!(matches!(
2664 &args[3],
2665 HirExpr::Enum { value_type, value, .. }
2666 if value_type == "Invis" && value == "ALL"
2667 ));
2668
2669 let expr = first_action_expr(&action_source(
2670 "eventPlayer.setInvisibility(ChaseTimeReeval.NONE)",
2671 ));
2672 let HirExpr::ReceiverCall { args, .. } = &expr else {
2673 panic!("expected a receiver call, got {expr:?}");
2674 };
2675 assert!(matches!(
2676 &args[0],
2677 HirExpr::Enum { value_type, value, .. }
2678 if value_type == "ChaseTimeReeval" && value == "NONE"
2679 ));
2680 }
2681
2682 #[test]
2683 fn non_enum_arguments_for_enum_parameters_are_carried_structurally() {
2684 let expr = first_action_expr(&action_source("eventPlayer.setInvisibility(g)"));
2688 let HirExpr::ReceiverCall { args, .. } = &expr else {
2689 panic!("expected a receiver call, got {expr:?}");
2690 };
2691 assert!(matches!(
2692 &args[0],
2693 HirExpr::GlobalVar { name, .. } if name == "g"
2694 ));
2695
2696 let expr = first_action_expr(&action_source("eventPlayer.setInvisibility(3)"));
2697 let HirExpr::ReceiverCall { args, .. } = &expr else {
2698 panic!("expected a receiver call, got {expr:?}");
2699 };
2700 assert!(matches!(&args[0], HirExpr::Number { .. }));
2701 }
2702
2703 #[test]
2704 fn unknown_builtins_fail_at_resolution_not_emission() {
2705 let error = compile_error(&action_source("frobnicate()"), 4);
2706 assert_eq!(error.code, "unknown-action");
2707
2708 let error = compile_error(&action_source("g = frobnicate()"), 4);
2709 assert_eq!(error.code, "unknown-value");
2710
2711 let error = compile_error(
2712 "globalvar g\nrule \"r\":\n @Event eachPlayer\n eventPlayer.frobnicate()\n",
2713 4,
2714 );
2715 assert_eq!(error.code, "unknown-member");
2716 }
2717
2718 #[test]
2719 fn wright_only_catalog_names_are_rejected() {
2720 let error = compile_error(&action_source("createHudText(1)"), 4);
2724 assert_eq!(error.code, "unknown-action");
2725
2726 let error = compile_error(&action_source("g = squareRoot(9)"), 4);
2727 assert_eq!(error.code, "unknown-value");
2728 }
2729
2730 #[test]
2731 fn generic_member_only_actions_are_rejected() {
2732 let error = compile_error(&action_source("setMoveSpeed(eventPlayer, 100)"), 4);
2735 assert_eq!(error.code, "unknown-action");
2736 }
2737
2738 #[test]
2739 fn range_is_for_iterables_only() {
2740 let error = compile_error(&action_source("@Condition len(range(1, 5, 1)) > 0"), 4);
2743 assert_eq!(error.code, "invalid-call-context");
2744
2745 let error = compile_error(&action_source("for g in [1, 2]:\n debug(g)"), 4);
2746 assert_eq!(error.code, "invalid-iterable");
2747
2748 crate::compile(
2749 &action_source("for g in range(3):\n debug(g)"),
2750 "test.opy",
2751 std::path::Path::new(""),
2752 )
2753 .expect("the for-header range form compiles");
2754 }
2755
2756 #[test]
2757 fn source_aliases_resolve_to_canonical_names() {
2758 let hir = crate::compile(
2761 &action_source("stopChasingVariable(g)"),
2762 "test.opy",
2763 std::path::Path::new(""),
2764 )
2765 .expect("the alias target compiles");
2766 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2767 panic!("expected a rule");
2768 };
2769 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2770 panic!("expected expression statement");
2771 };
2772 let HirExpr::Call { name, .. } = expr.as_ref() else {
2773 panic!("expected a call");
2774 };
2775 assert_eq!(name, "stopChasing");
2776
2777 let hir = crate::compile(
2778 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2779 @Condition eventPlayer.getCurrentHero() != null\n \
2780 @Condition eventPlayer.hasStatusEffect(Status.BURNING) == false\n \
2781 disableInspector()\n",
2782 "test.opy",
2783 std::path::Path::new(""),
2784 )
2785 .expect("member aliases compile");
2786 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2787 panic!("expected a rule");
2788 };
2789 let HirExpr::Binary { left, .. } = &rule.conditions[0] else {
2790 panic!("expected a comparison");
2791 };
2792 let HirExpr::ReceiverCall { name, .. } = left.as_ref() else {
2793 panic!("expected a receiver call");
2794 };
2795 assert_eq!(name, "getHero");
2796 }
2797
2798 #[test]
2799 fn unknown_catalog_enum_members_are_rejected() {
2800 for source in [
2801 "globalvar g\nrule \"r\":\n @Event global\n g = Color.CYAN\n",
2802 "globalvar g\nrule \"r\":\n @Event global\n g = DynamicEffect.SPARKLES\n",
2803 ] {
2804 let error = crate::compile(source, "test.opy", std::path::Path::new(""))
2805 .expect_err("unknown catalog member must be rejected");
2806 assert_eq!(error.code, "unknown-enum-member");
2807 }
2808 }
2809
2810 #[test]
2811 fn default_var_for_binder_resolves_at_all_range_arities() {
2812 for (binder, iterable) in [
2817 ("I", "range(0, 10)"),
2818 ("I", "range(3)"),
2819 ("I", "range(1, 5, 2)"),
2820 ] {
2821 let hir = lower_ok(&format!(
2822 "globalvar total\nrule \"r\":\n @Event global\n for {binder} in {iterable}:\n total += {binder}\n"
2823 ));
2824 let (_, actions) = rule_conditions_and_actions(&hir);
2825 let HirStmt::For { variable, body, .. } = &actions[0] else {
2826 panic!("expected a for statement");
2827 };
2828 assert!(
2829 matches!(variable.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
2830 "the binder resolves to the implicit global 'I', got {variable:?}"
2831 );
2832 assert!(!body.is_empty(), "the loop body lowers");
2833 let HirStmt::Assign { value, .. } = &body[0] else {
2836 panic!("expected an assignment in the body");
2837 };
2838 let HirExpr::Binary { right, .. } = value.as_ref() else {
2839 panic!("expected a binary expression");
2840 };
2841 assert!(
2842 matches!(right.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
2843 "the binder use inside the body resolves to the implicit global"
2844 );
2845 }
2846 }
2847
2848 #[test]
2849 fn default_var_names_resolve_as_implicit_globals() {
2850 let hir = lower_ok("rule \"r\":\n @Event global\n I = 5\n debug(I)\n");
2853 let (_, actions) = rule_conditions_and_actions(&hir);
2854 let HirStmt::Assign { target, .. } = &actions[0] else {
2855 panic!("expected an assignment");
2856 };
2857 assert!(
2858 matches!(target.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
2859 "the implicit global resolves, got {target:?}"
2860 );
2861 assert_eq!(default_var_index("I"), Some(8));
2863 assert_eq!(default_var_index("AA"), Some(26));
2864 assert_eq!(default_var_index("Z"), Some(25));
2865 assert_eq!(default_var_index("DX"), Some(127));
2866 assert_eq!(default_var_index("DY"), None);
2867 assert_eq!(default_var_index("i"), None);
2868 }
2869
2870 #[test]
2871 fn nested_same_name_for_binders_reuse_the_implicit_global() {
2872 let hir = lower_ok(
2876 "rule \"r\":\n @Event global\n for I in range(3):\n for I in range(2):\n debug(I)\n",
2877 );
2878 let (_, actions) = rule_conditions_and_actions(&hir);
2879 let HirStmt::For {
2880 variable: outer,
2881 body,
2882 ..
2883 } = &actions[0]
2884 else {
2885 panic!("expected an outer for statement");
2886 };
2887 let HirStmt::For {
2888 variable: inner, ..
2889 } = &body[0]
2890 else {
2891 panic!("expected an inner for statement");
2892 };
2893 assert!(
2894 matches!(outer.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I")
2895 && matches!(inner.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
2896 "both loops bind the same implicit global (spans differ per binder site)"
2897 );
2898 }
2899
2900 #[test]
2901 fn undeclared_lowercase_binder_is_still_an_unknown_identifier() {
2902 let error = compile_error(
2907 "rule \"r\":\n @Event global\n for i in range(3):\n debug(i)\n",
2908 3,
2909 );
2910 assert_eq!(error.code, "unknown-identifier");
2911 let span = error.span.expect("the error is source-located");
2912 assert_eq!(span.start.line, 3);
2913 }
2914
2915 #[test]
2916 fn issue_28_constructs_lower_to_provenance_preserving_hir() {
2917 let hir = lower_ok(
2918 "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",
2919 );
2920 let (_, actions) = rule_conditions_and_actions(&hir);
2921 let HirStmt::DoWhile { condition, .. } = &actions[0] else {
2922 panic!("expected do-while");
2923 };
2924 assert!(matches!(condition.as_ref(), HirExpr::Binary { op, .. } if op == "not in"));
2925 let HirStmt::Switch { arms, .. } = &actions[1] else {
2926 panic!("expected switch");
2927 };
2928 assert_eq!(arms.len(), 2);
2929 let HirSwitchArm::Case {
2930 value: case_value,
2931 body,
2932 ..
2933 } = &arms[0]
2934 else {
2935 panic!("expected case arm");
2936 };
2937 assert!(
2938 matches!(case_value.as_ref(), HirExpr::Number { value, .. } if *value == 0x10 as f64)
2939 );
2940 let HirStmt::Assign { value, .. } = &body[0] else {
2941 panic!("expected case assignment");
2942 };
2943 assert!(matches!(value.as_ref(), HirExpr::Binary { op, .. } if op == "in"));
2944 let HirSwitchArm::Default { body, .. } = &arms[1] else {
2945 panic!("expected default arm");
2946 };
2947 assert!(matches!(body[0], HirStmt::Assign { .. }));
2948 let HirStmt::Assign { value, .. } = &actions[2] else {
2949 panic!("expected comprehension assignment");
2950 };
2951 assert!(matches!(value.as_ref(), HirExpr::Comprehension { .. }));
2952 let HirStmt::Assign { value, .. } = &actions[3] else {
2953 panic!("expected sorted assignment");
2954 };
2955 assert!(
2956 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")))
2957 );
2958 let HirStmt::Assign { value, .. } = &actions[4] else {
2959 panic!("expected string assignment");
2960 };
2961 assert!(
2962 matches!(value.as_ref(), HirExpr::StringModifier { modifier, .. } if modifier == "w")
2963 );
2964 }
2965
2966 #[test]
2967 fn issue_28_rejects_reference_invalid_bare_dict_and_lambda() {
2968 let dict_error = compile_error(
2969 "globalvar x\nrule \"r\":\n @Event global\n x = {\"x\": 1}\n",
2970 4,
2971 );
2972 assert_eq!(dict_error.code, "dict-access");
2973 let lambda_error = compile_error(
2974 "globalvar x\nrule \"r\":\n @Event global\n x = lambda value: value\n",
2975 4,
2976 );
2977 assert_eq!(lambda_error.code, "lambda-context");
2978 }
2979
2980 #[test]
2981 fn do_while_requires_rule_or_definition_prefix_position() {
2982 let error = compile_error(
2983 "globalvar value\nrule \"r\":\n @Event global\n value = 1\n do:\n value += 1\n while value < 2\n",
2984 5,
2985 );
2986 assert_eq!(error.code, "do-while-placement");
2987 assert_eq!(
2988 error.message,
2989 "do-while must be at the beginning of a rule, subroutine, or do-while body; only pass statements may precede it"
2990 );
2991 }
2992}