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, Locale};
44
45const PROTOCOL_NAME: &str = "wright/opy-hir";
47
48#[derive(Clone, Copy, PartialEq, Eq)]
51enum CallPosition {
52 Statement,
54 Value,
56 ForIterable,
58 LambdaArgument,
60}
61
62struct Lowerer {
64 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_for_binder(variable, macro_params)),
766 iterable: Box::new(self.lower_expr(iterable, macro_params, iterable_position)),
767 body: self.lower_block(body, macro_params, true, false),
768 span: Some(span.into()),
769 }
770 }
771 Stmt::While {
772 condition,
773 body,
774 span,
775 } => HirStmt::While {
776 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
777 body: self.lower_block(body, macro_params, true, false),
778 span: Some(span.into()),
779 },
780 Stmt::DoWhile {
781 condition,
782 body,
783 span,
784 } => HirStmt::DoWhile {
785 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
786 body: self.lower_block(body, macro_params, true, true),
787 span: Some(span.into()),
788 },
789 Stmt::Switch { value, arms, span } => HirStmt::Switch {
790 value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)),
791 arms: arms
792 .iter()
793 .map(|arm| match arm {
794 cst::SwitchArm::Case { value, body, span } => HirSwitchArm::Case {
795 value: Box::new(self.lower_expr(
796 value,
797 macro_params,
798 CallPosition::Value,
799 )),
800 body: self.lower_block(body, macro_params, true, false),
801 span: Some((*span).into()),
802 },
803 cst::SwitchArm::Default { body, span } => HirSwitchArm::Default {
804 body: self.lower_block(body, macro_params, true, false),
805 span: Some((*span).into()),
806 },
807 })
808 .collect(),
809 span: Some(span.into()),
810 },
811 Stmt::Break { span } => {
812 if !breakable {
813 self.error_at(
814 "break-context",
815 "break is only valid inside a switch or loop".to_string(),
816 *span,
817 );
818 }
819 HirStmt::Break {
820 span: Some(span.into()),
821 }
822 }
823 Stmt::Pass { span } => HirStmt::Pass {
824 span: Some(span.into()),
825 },
826 }
827 }
828
829 fn lower_for_binder(&mut self, variable: &Expr, macro_params: &[String]) -> HirExpr {
830 if let Expr::Member {
831 receiver,
832 member,
833 member_span,
834 span,
835 } = variable
836 {
837 return HirExpr::PlayerVar {
838 player: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
839 name: member.clone(),
840 member_span: Some((*member_span).into()),
841 span: Some((*span).into()),
842 };
843 }
844 self.lower_expr(variable, macro_params, CallPosition::Value)
845 }
846
847 fn lower_macro_body(&mut self, body: &[Stmt], params: &[String]) -> Vec<HirStmt> {
848 self.lower_block(body, params, false, false)
849 }
850
851 fn lower_expr(
852 &mut self,
853 expr: &Expr,
854 macro_params: &[String],
855 position: CallPosition,
856 ) -> HirExpr {
857 match expr {
858 Expr::Number { value, text, span } => HirExpr::Number {
859 value: *value,
860 text: text.clone(),
861 span: Some(span.into()),
862 },
863 Expr::String { value, span } => HirExpr::String {
864 value: value.clone(),
865 span: Some(span.into()),
866 },
867 Expr::Bool { value, span } => HirExpr::Bool {
868 value: *value,
869 span: Some(span.into()),
870 },
871 Expr::Null { span } => HirExpr::Null {
872 span: Some(span.into()),
873 },
874 Expr::Array { elements, span } => HirExpr::Array {
875 elements: elements
876 .iter()
877 .map(|element| self.lower_expr(element, macro_params, CallPosition::Value))
878 .collect(),
879 span: Some(span.into()),
880 },
881 Expr::Dict { entries, span } => {
882 if !self.allow_dict_literal {
883 self.error_at(
884 "dict-access",
885 "dictionary literals must be accessed by a key".to_string(),
886 *span,
887 );
888 return HirExpr::Null { span: None };
889 }
890 HirExpr::Dict {
891 entries: entries
892 .iter()
893 .map(|entry| HirDictEntry {
894 key: Box::new(self.lower_expr(
895 &entry.key,
896 macro_params,
897 CallPosition::Value,
898 )),
899 value: Box::new(self.lower_expr(
900 &entry.value,
901 macro_params,
902 CallPosition::Value,
903 )),
904 span: Some(entry.span.into()),
905 })
906 .collect(),
907 span: Some(span.into()),
908 }
909 }
910 Expr::Comprehension {
911 element,
912 variable,
913 variable_span,
914 index,
915 iterable,
916 condition,
917 span,
918 } => {
919 let iterable = self.lower_expr(iterable, macro_params, CallPosition::Value);
920 let previous = std::mem::take(&mut self.locals);
921 self.locals.push(variable.clone());
922 if let Some((index, _)) = index {
923 self.locals.push(index.clone());
924 }
925 let element = self.lower_expr(element, macro_params, CallPosition::Value);
926 let condition = condition.as_ref().map(|condition| {
927 Box::new(self.lower_expr(condition, macro_params, CallPosition::Value))
928 });
929 self.locals = previous;
930 HirExpr::Comprehension {
931 element: Box::new(element),
932 variable: variable.clone(),
933 variable_span: Some(variable_span.into()),
934 index: index.as_ref().map(|(name, _)| name.clone()),
935 index_span: index.as_ref().map(|(_, span)| (*span).into()),
936 iterable: Box::new(iterable),
937 condition,
938 span: Some(span.into()),
939 }
940 }
941 Expr::Lambda { params, body, span } => {
942 if position != CallPosition::LambdaArgument {
943 self.error_at(
944 "lambda-context",
945 "lambda expressions are only valid as array operation arguments"
946 .to_string(),
947 *span,
948 );
949 return HirExpr::Null { span: None };
950 }
951 let previous = std::mem::take(&mut self.locals);
952 self.locals = params.iter().map(|(name, _)| name.clone()).collect();
953 let body = self.lower_expr(body, macro_params, CallPosition::Value);
954 self.locals = previous;
955 HirExpr::Lambda {
956 params: params.iter().map(|(name, _)| name.clone()).collect(),
957 param_spans: params
958 .iter()
959 .map(|(_, span)| Some((*span).into()))
960 .collect(),
961 body: Box::new(body),
962 span: Some(span.into()),
963 }
964 }
965 Expr::StringModifier {
966 modifier,
967 value,
968 format_text,
969 interpolations,
970 span,
971 } => {
972 if *modifier == 'f' {
973 if let Some(format_text) = format_text {
974 if !interpolations.is_empty() {
975 return HirExpr::Format {
976 text: format_text.clone(),
977 args: interpolations
978 .iter()
979 .map(|expr| {
980 self.lower_expr(expr, macro_params, CallPosition::Value)
981 })
982 .collect(),
983 span: Some(span.into()),
984 };
985 }
986 return HirExpr::String {
987 value: format_text.clone(),
988 span: Some(span.into()),
989 };
990 }
991 }
992 HirExpr::StringModifier {
993 modifier: modifier.to_string(),
994 value: value.clone(),
995 span: Some(span.into()),
996 }
997 }
998 Expr::Name { name, span } => self.lower_name(name, *span, macro_params),
999 Expr::Type { name, args, span } => HirExpr::Type {
1000 name: name.clone(),
1001 args: args
1002 .iter()
1003 .map(|arg| self.lower_expr(arg, macro_params, CallPosition::Value))
1004 .collect(),
1005 span: Some(span.into()),
1006 },
1007 Expr::Member {
1008 receiver,
1009 member,
1010 member_span,
1011 span,
1012 } => self.lower_member(receiver, member, *member_span, *span, macro_params),
1013 Expr::Index { array, index, span } => {
1014 let previous = self.allow_dict_literal;
1015 self.allow_dict_literal = true;
1016 let array = self.lower_expr(array, macro_params, CallPosition::Value);
1017 self.allow_dict_literal = previous;
1018 HirExpr::Index {
1019 array: Box::new(array),
1020 index: Box::new(self.lower_expr(index, macro_params, CallPosition::Value)),
1021 span: Some(span.into()),
1022 }
1023 }
1024 Expr::Call { name, args, span } => {
1025 self.lower_call(name, args, *span, macro_params, position)
1026 }
1027 Expr::ReceiverCall {
1028 receiver,
1029 name,
1030 args,
1031 span,
1032 } => self.lower_receiver_call(receiver, name, args, *span, macro_params, position),
1033 Expr::Binary {
1034 op,
1035 left,
1036 right,
1037 span,
1038 } => HirExpr::Binary {
1039 op: op.clone(),
1040 left: Box::new(self.lower_expr(left, macro_params, CallPosition::Value)),
1041 right: Box::new(self.lower_expr(right, macro_params, CallPosition::Value)),
1042 span: Some(span.into()),
1043 },
1044 Expr::Conditional {
1045 then_value,
1046 condition,
1047 else_value,
1048 span,
1049 } => HirExpr::Conditional {
1050 then_value: Box::new(self.lower_expr(
1051 then_value,
1052 macro_params,
1053 CallPosition::Value,
1054 )),
1055 condition: Box::new(self.lower_expr(condition, macro_params, CallPosition::Value)),
1056 else_value: Box::new(self.lower_expr(
1057 else_value,
1058 macro_params,
1059 CallPosition::Value,
1060 )),
1061 span: Some((*span).into()),
1062 },
1063 Expr::Unary { op, operand, span } => HirExpr::Unary {
1064 op: op.clone(),
1065 operand: Box::new(self.lower_expr(operand, macro_params, CallPosition::Value)),
1066 span: Some(span.into()),
1067 },
1068 }
1069 }
1070
1071 fn lower_name(&mut self, name: &str, span: Span, macro_params: &[String]) -> HirExpr {
1072 if macro_params.iter().any(|param| param == name) {
1073 return HirExpr::MacroParam {
1074 name: name.to_string(),
1075 span: Some(span.into()),
1076 };
1077 }
1078 if self.locals.iter().any(|local| local == name) {
1079 return HirExpr::Local {
1080 name: name.to_string(),
1081 span: Some(span.into()),
1082 };
1083 }
1084 match name {
1085 "eventPlayer" => HirExpr::EventPlayer {
1086 span: Some(span.into()),
1087 },
1088 "hostPlayer" => HirExpr::HostPlayer {
1089 span: Some(span.into()),
1090 },
1091 _ if self.globals.contains(name) => HirExpr::GlobalVar {
1092 name: name.to_string(),
1093 span: Some(span.into()),
1094 },
1095 _ if self.players.contains(name) => HirExpr::PlayerVar {
1096 player: Box::new(HirExpr::EventPlayer { span: None }),
1097 name: name.to_string(),
1098 member_span: None,
1099 span: Some(span.into()),
1100 },
1101 _ if self.enums.contains_key(name) => {
1102 self.error_at(
1103 "enum-type-without-member",
1104 format!("enum type '{name}' must be used with a member (e.g. {name}.MEMBER)"),
1105 span,
1106 );
1107 HirExpr::Null { span: None }
1108 }
1109 _ if default_var_index(name).is_some() => HirExpr::GlobalVar {
1116 name: name.to_string(),
1117 span: Some(span.into()),
1118 },
1119 _ => {
1120 self.error_at(
1121 "unknown-identifier",
1122 format!("unknown identifier '{name}'"),
1123 span,
1124 );
1125 HirExpr::Null { span: None }
1126 }
1127 }
1128 }
1129
1130 fn lower_member(
1131 &mut self,
1132 receiver: &Expr,
1133 member: &str,
1134 member_span: Span,
1135 span: Span,
1136 _macro_params: &[String],
1137 ) -> HirExpr {
1138 if let Expr::Name { name, .. } = receiver {
1139 if let Some(members) = self.enums.get(name) {
1141 return match members.iter().position(|candidate| candidate == member) {
1142 Some(index) => HirExpr::Number {
1143 value: index as f64,
1144 text: index.to_string(),
1145 span: Some(span.into()),
1146 },
1147 None => {
1148 self.error_at(
1149 "unknown-enum-member",
1150 format!("enum '{name}' has no member '{member}'"),
1151 span,
1152 );
1153 HirExpr::Null { span: None }
1154 }
1155 };
1156 }
1157 if self.manifest.domain_identity(name) {
1163 let locale = Locale::new("en-US");
1164 let catalog_member = match (name.as_str(), member) {
1165 ("SpecVisibility", "NEVER") => "VISIBLE_NEVER",
1166 _ => member,
1167 };
1168 let canonical_member = self
1169 .catalog
1170 .enum_domain(name)
1171 .and_then(|domain| {
1172 domain
1173 .members
1174 .iter()
1175 .find(|candidate| candidate.member == catalog_member)
1176 .map(|candidate| candidate.member.clone())
1177 })
1178 .or_else(|| {
1179 if name == "Team" && member.parse::<u32>().is_ok() {
1180 self.catalog
1181 .resolve_enum_member(name, &locale, &format!("{name} {member}"))
1182 .map(|(_, member)| member)
1183 } else {
1184 None
1185 }
1186 });
1187 let Some(canonical_member) = canonical_member else {
1188 self.error_at(
1189 "unknown-enum-member",
1190 format!("enum '{name}' has no member '{member}'"),
1191 span,
1192 );
1193 return HirExpr::Null { span: None };
1194 };
1195 return HirExpr::Enum {
1196 value_type: name.clone(),
1197 value: canonical_member,
1198 span: Some(span.into()),
1199 };
1200 }
1201 if name == "eventPlayer" {
1203 return HirExpr::PlayerVar {
1204 player: Box::new(HirExpr::EventPlayer { span: None }),
1205 name: member.to_string(),
1206 member_span: Some(member_span.into()),
1207 span: Some(span.into()),
1208 };
1209 }
1210 if name == "hostPlayer" {
1211 return HirExpr::PlayerVar {
1212 player: Box::new(HirExpr::HostPlayer { span: None }),
1213 name: member.to_string(),
1214 member_span: Some(member_span.into()),
1215 span: Some(span.into()),
1216 };
1217 }
1218 if name == "random" {
1220 self.error_at(
1221 "unsupported-member",
1222 format!("module member '{name}.{member}' must be called"),
1223 span,
1224 );
1225 return HirExpr::Null { span: None };
1226 }
1227 if self.globals.contains(name)
1232 || self.players.contains(name)
1233 || default_var_index(name).is_some()
1234 {
1235 let receiver = if default_var_index(name).is_some() {
1236 HirExpr::GlobalVar {
1237 name: name.to_string(),
1238 span: Some(receiver.span().into()),
1239 }
1240 } else {
1241 self.lower_name(name, receiver.span(), &[])
1242 };
1243 return HirExpr::Member {
1244 receiver: Box::new(receiver),
1245 member: member.to_string(),
1246 member_span: Some(member_span.into()),
1247 span: Some(span.into()),
1248 };
1249 }
1250 }
1251 self.error_at(
1252 "unsupported-member",
1253 "unsupported member access on this expression".to_string(),
1254 span,
1255 );
1256 HirExpr::Null { span: None }
1257 }
1258
1259 fn lower_call(
1260 &mut self,
1261 name: &str,
1262 args: &[cst::CallArg],
1263 span: Span,
1264 macro_params: &[String],
1265 position: CallPosition,
1266 ) -> HirExpr {
1267 if name == "createWorkshopSetting" {
1268 return self.lower_workshop_setting(args, span, macro_params);
1269 }
1270 if !self.macros.contains(name) && !self.subroutines.contains(name) && name != "sorted" {
1273 match self.manifest.resolve_function(name) {
1274 Some(entry) => self.check_call_position(name, entry, position, span),
1275 None => {
1276 let (code, message) = match position {
1277 CallPosition::Statement => {
1278 ("unknown-action", format!("unknown action '{name}'"))
1279 }
1280 CallPosition::Value => ("unknown-value", format!("unknown value '{name}'")),
1281 CallPosition::ForIterable => (
1282 "invalid-iterable",
1283 format!("for-loop iterable '{name}' must be a range(...) call"),
1284 ),
1285 CallPosition::LambdaArgument => {
1286 ("unknown-value", format!("unknown value '{name}'"))
1287 }
1288 };
1289 self.error_at(code, message, span);
1290 }
1291 }
1292 }
1293 match name {
1294 "sorted" => HirExpr::Call {
1295 name: name.to_string(),
1296 args: self.lower_arg_values_with_lambda(args, macro_params, |index, arg| {
1297 index == 1 || arg.keyword.as_ref().is_some_and(|(name, _)| name == "key")
1298 }),
1299 span: Some(span.into()),
1300 },
1301 "vect" => {
1302 let (bound, _) = match self.manifest.resolve_function(name) {
1307 Some(entry) => self.bind_args(entry, args, macro_params),
1308 None => (self.lower_arg_values(args, macro_params), None),
1309 };
1310 if bound.len() < 3 {
1311 self.error_at(
1312 "vect-arity",
1313 format!(
1314 "vect() expects 3 arguments (x, y, z) but got {}",
1315 args.len()
1316 ),
1317 span,
1318 );
1319 return HirExpr::Null { span: None };
1320 }
1321 HirExpr::Vector {
1322 x: Box::new(bound[0].clone()),
1323 y: Box::new(bound[1].clone()),
1324 z: Box::new(bound[2].clone()),
1325 span: Some(span.into()),
1326 }
1327 }
1328 _ => {
1329 if self.macros.contains(name) {
1330 for arg in args {
1334 if let Some((keyword, span)) = &arg.keyword {
1335 self.error_at(
1336 "keyword-unsupported",
1337 format!(
1338 "macro '{name}' does not accept keyword \
1339 arguments ('{keyword}')"
1340 ),
1341 *span,
1342 );
1343 }
1344 }
1345 return HirExpr::MacroCall {
1346 name: name.to_string(),
1347 args: self.lower_arg_values(args, macro_params),
1348 span: Some(span.into()),
1349 };
1350 }
1351 match self.manifest.resolve_function(name) {
1352 Some(entry) => {
1353 if self.subroutines.contains(name) {
1357 return HirExpr::Call {
1358 name: name.to_string(),
1359 args: self.lower_arg_values(args, macro_params),
1360 span: Some(span.into()),
1361 };
1362 }
1363 let (bound, selector) = self.bind_args(entry, args, macro_params);
1364 let (call_name, bound) =
1365 self.resolve_contextual_domain(entry, bound, selector.as_deref());
1366 HirExpr::Call {
1367 name: call_name,
1368 args: bound,
1369 span: Some(span.into()),
1370 }
1371 }
1372 None => HirExpr::Call {
1373 name: name.to_string(),
1374 args: self.lower_arg_values(args, macro_params),
1375 span: Some(span.into()),
1376 },
1377 }
1378 }
1379 }
1380 }
1381
1382 fn lower_workshop_setting(
1383 &mut self,
1384 args: &[cst::CallArg],
1385 span: Span,
1386 macro_params: &[String],
1387 ) -> HirExpr {
1388 if !(4..=5).contains(&args.len()) {
1389 self.error_at(
1390 "invalid-arity",
1391 format!(
1392 "function 'createWorkshopSetting' takes 4 or 5 arguments, received {}",
1393 args.len()
1394 ),
1395 span,
1396 );
1397 return HirExpr::Null { span: None };
1398 }
1399 for arg in args {
1400 if let Some((keyword, keyword_span)) = &arg.keyword {
1401 self.error_at(
1402 "keyword-unsupported",
1403 format!(
1404 "function 'createWorkshopSetting' does not accept keyword arguments ('{keyword}')"
1405 ),
1406 *keyword_span,
1407 );
1408 }
1409 }
1410
1411 let setting_type = match &args[0].value {
1412 Expr::Type {
1413 name,
1414 args: type_args,
1415 span: type_span,
1416 } => HirExpr::Type {
1417 name: name.clone(),
1418 args: type_args
1419 .iter()
1420 .map(|arg| self.lower_expr(arg, macro_params, CallPosition::Value))
1421 .collect(),
1422 span: Some((*type_span).into()),
1423 },
1424 Expr::Name {
1425 name,
1426 span: type_span,
1427 } if matches!(name.as_str(), "bool" | "int" | "float") => HirExpr::Type {
1428 name: name.clone(),
1429 args: Vec::new(),
1430 span: Some((*type_span).into()),
1431 },
1432 other => {
1433 self.error_at(
1434 "invalid-argument",
1435 "argument 1 of 'createWorkshopSetting' must be a setting type".to_string(),
1436 other.span(),
1437 );
1438 HirExpr::Null { span: None }
1439 }
1440 };
1441 let mut lowered = Vec::with_capacity(5);
1442 lowered.push(setting_type);
1443 lowered.extend(
1444 args[1..]
1445 .iter()
1446 .map(|arg| self.lower_expr(&arg.value, macro_params, CallPosition::Value)),
1447 );
1448 if args.len() == 4 {
1449 lowered.push(HirExpr::Number {
1450 value: 0.0,
1451 text: "0".to_string(),
1452 span: None,
1453 });
1454 }
1455 HirExpr::Call {
1456 name: "createWorkshopSetting".to_string(),
1457 args: lowered,
1458 span: Some(span.into()),
1459 }
1460 }
1461
1462 fn lower_arg_values(&mut self, args: &[cst::CallArg], macro_params: &[String]) -> Vec<HirExpr> {
1465 self.lower_arg_values_with_lambda(args, macro_params, |_, _| false)
1466 }
1467
1468 fn lower_arg_values_with_lambda(
1469 &mut self,
1470 args: &[cst::CallArg],
1471 macro_params: &[String],
1472 allows_lambda: impl Fn(usize, &cst::CallArg) -> bool,
1473 ) -> Vec<HirExpr> {
1474 args.iter()
1475 .enumerate()
1476 .map(|(index, arg)| {
1477 let position = if allows_lambda(index, arg) {
1478 CallPosition::LambdaArgument
1479 } else {
1480 CallPosition::Value
1481 };
1482 self.lower_expr(&arg.value, macro_params, position)
1483 })
1484 .collect()
1485 }
1486
1487 fn bind_args(
1499 &mut self,
1500 entry: &Function,
1501 args: &[cst::CallArg],
1502 macro_params: &[String],
1503 ) -> (Vec<HirExpr>, Option<String>) {
1504 let mut slots: Vec<Option<HirExpr>> = vec![None; entry.params.len()];
1505 let mut selector = None;
1506 let mut has_keyword = false;
1507 let mut binding_error = false;
1508 let contextual = entry.contextual_domain.as_ref();
1509
1510 let mut by_spelling: HashMap<&str, usize> = HashMap::new();
1514 for (index, param) in entry.params.iter().enumerate() {
1515 by_spelling.insert(param.name.as_str(), index);
1516 for alternate in ¶m.alternate_names {
1517 by_spelling.insert(alternate.as_str(), index);
1518 }
1519 }
1520
1521 for (arg_index, arg) in args.iter().enumerate() {
1522 match &arg.keyword {
1523 Some((keyword, name_span)) => {
1524 if !entry.keyword_args {
1525 binding_error = true;
1526 self.error_at(
1527 "keyword-unsupported",
1528 format!(
1529 "function '{}' does not accept keyword arguments ('{keyword}')",
1530 entry.id
1531 ),
1532 *name_span,
1533 );
1534 continue;
1535 }
1536 has_keyword = true;
1537 match by_spelling.get(keyword.as_str()) {
1538 None => {
1539 binding_error = true;
1540 self.error_at(
1541 "unknown-keyword",
1542 format!(
1543 "unknown keyword argument '{keyword}' for function '{}'",
1544 entry.id
1545 ),
1546 *name_span,
1547 );
1548 }
1549 Some(&index) => {
1550 let param = &entry.params[index];
1551 if param.positional_only {
1552 binding_error = true;
1553 self.error_at(
1554 "unknown-keyword",
1555 format!(
1556 "parameter '{}' of '{}' cannot be bound by keyword",
1557 param.name, entry.id
1558 ),
1559 *name_span,
1560 );
1561 } else if slots[index].is_some() {
1562 binding_error = true;
1563 self.error_at(
1564 "duplicate-argument",
1565 format!(
1566 "argument '{}' of function '{}' is defined twice",
1567 keyword, entry.id
1568 ),
1569 *name_span,
1570 );
1571 } else {
1572 slots[index] = Some(self.lower_call_arg_value(
1573 entry,
1574 index,
1575 arg,
1576 macro_params,
1577 ));
1578 if contextual.is_some_and(|c| c.by == param.name) {
1579 selector = Some(keyword.clone());
1580 }
1581 }
1582 }
1583 }
1584 }
1585 None => {
1586 if has_keyword && entry.contextual_domain.is_none() {
1592 binding_error = true;
1593 self.error_at(
1594 "positional-after-keyword",
1595 format!(
1596 "cannot use positional arguments after keyword \
1597 arguments in call to '{}'",
1598 entry.id
1599 ),
1600 arg.value.span(),
1601 );
1602 }
1603 let index = arg_index;
1607 if index < entry.params.len() {
1608 let param = &entry.params[index];
1609 if param.keyword_only {
1610 binding_error = true;
1611 self.error_at(
1612 "keyword-required",
1613 format!(
1614 "argument {} of '{}' must be passed as a keyword \
1615 (name = value; accepted names: {})",
1616 index + 1,
1617 entry.id,
1618 keyword_spellings(param).join(", ")
1619 ),
1620 arg.value.span(),
1621 );
1622 }
1623 if slots[index].is_none() {
1624 slots[index] =
1625 Some(self.lower_call_arg_value(entry, index, arg, macro_params));
1626 }
1627 } else {
1628 self.lower_expr(&arg.value, macro_params, CallPosition::Value);
1629 }
1630 }
1631 }
1632 }
1633
1634 if !binding_error && args.len() > entry.params.len() {
1640 self.check_arity(entry, args.len(), arg_span(args));
1641 }
1642
1643 let mut bound: Vec<HirExpr> = Vec::with_capacity(entry.params.len());
1647 for (index, param) in entry.params.iter().enumerate() {
1648 match &slots[index] {
1649 Some(value) => bound.push(value.clone()),
1650 None => match ¶m.default {
1651 Some(ParamDefault::EnumMember(member)) => {
1652 let domain = param.domain.clone().unwrap_or_default();
1653 bound.push(HirExpr::Enum {
1654 value_type: domain,
1655 value: member.clone(),
1656 span: None,
1657 });
1658 }
1659 Some(ParamDefault::Number(number)) => {
1660 bound.push(HirExpr::Number {
1661 value: *number,
1662 text: format!("{number}"),
1663 span: None,
1664 });
1665 }
1666 None if param.optional => {
1667 }
1670 None => {
1671 self.error_at(
1672 "missing-argument",
1673 format!(
1674 "missing argument '{}' for function '{}'",
1675 param.name, entry.id
1676 ),
1677 arg_span(args),
1678 );
1679 bound.push(HirExpr::Null { span: None });
1680 }
1681 },
1682 }
1683 }
1684
1685 for (index, param) in entry.params.iter().enumerate() {
1688 if !param.variable {
1689 continue;
1690 }
1691 if let Some(Some(value)) = slots.get(index) {
1692 if !matches!(value, HirExpr::GlobalVar { .. } | HirExpr::PlayerVar { .. }) {
1693 self.error_at(
1694 "invalid-argument",
1695 format!(
1696 "argument {} of '{}' must be a variable (globalvar or \
1697 playervar)",
1698 index + 1,
1699 entry.id
1700 ),
1701 arg_span(args),
1702 );
1703 }
1704 }
1705 }
1706
1707 (bound, selector)
1708 }
1709
1710 fn lower_call_arg_value(
1717 &mut self,
1718 entry: &Function,
1719 param_index: usize,
1720 arg: &cst::CallArg,
1721 macro_params: &[String],
1722 ) -> HirExpr {
1723 if let Some(contextual) = &entry.contextual_domain {
1724 let is_contextual = entry.params[param_index]
1725 .domain
1726 .as_deref()
1727 .is_some_and(|domain| domain == contextual.domain);
1728 if is_contextual {
1729 if let Expr::Member {
1730 receiver,
1731 member,
1732 span,
1733 ..
1734 } = &arg.value
1735 {
1736 if let Expr::Name { name, .. } = receiver.as_ref() {
1737 if name == &contextual.domain {
1738 return HirExpr::Enum {
1739 value_type: contextual.domain.clone(),
1740 value: member.clone(),
1741 span: Some((*span).into()),
1742 };
1743 }
1744 }
1745 }
1746 }
1747 }
1748 self.lower_expr(&arg.value, macro_params, CallPosition::Value)
1749 }
1750
1751 fn resolve_contextual_domain(
1761 &mut self,
1762 entry: &Function,
1763 mut bound: Vec<HirExpr>,
1764 selector: Option<&str>,
1765 ) -> (String, Vec<HirExpr>) {
1766 let Some(contextual) = &entry.contextual_domain else {
1767 return (entry.id.clone(), bound);
1768 };
1769 let Some(contextual_param) = entry
1770 .params
1771 .iter()
1772 .position(|param| param.domain.as_deref() == Some(contextual.domain.as_str()))
1773 else {
1774 return (entry.id.clone(), bound);
1775 };
1776 let HirExpr::Enum {
1780 value_type,
1781 value,
1782 span: value_span,
1783 } = &bound[contextual_param]
1784 else {
1785 return (entry.id.clone(), bound);
1786 };
1787 if value_type != &contextual.domain {
1788 return (entry.id.clone(), bound);
1789 }
1790 let Some(keyword) = selector else {
1791 return (entry.id.clone(), bound);
1792 };
1793 let Some(option) = contextual.options.get(keyword) else {
1794 return (entry.id.clone(), bound);
1795 };
1796 bound[contextual_param] = HirExpr::Enum {
1797 value_type: option.domain.clone(),
1798 value: value.clone(),
1799 span: *value_span,
1800 };
1801 (option.target.clone(), bound)
1802 }
1803
1804 fn lower_receiver_call(
1805 &mut self,
1806 receiver: &Expr,
1807 name: &str,
1808 args: &[cst::CallArg],
1809 span: Span,
1810 macro_params: &[String],
1811 position: CallPosition,
1812 ) -> HirExpr {
1813 if matches!(name, "map" | "filter" | "all" | "any") {
1814 let lowered = HirExpr::ReceiverCall {
1815 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1816 name: name.to_string(),
1817 args: self.lower_arg_values_with_lambda(args, macro_params, |index, _| index == 0),
1818 span: Some(span.into()),
1819 };
1820 return lowered;
1821 }
1822 if let Expr::Name { name: root, .. } = receiver {
1824 if root == "random" {
1825 return self.lower_call(
1826 &format!("random.{name}"),
1827 args,
1828 span,
1829 macro_params,
1830 position,
1831 );
1832 }
1833 }
1834 if let Expr::String { value, .. } = receiver {
1838 if name == "format" {
1839 if args.iter().any(|arg| arg.keyword.is_some()) {
1840 for arg in args {
1841 if let Some((keyword, span)) = &arg.keyword {
1842 self.error_at(
1843 "keyword-unsupported",
1844 format!(
1845 "function 'format' does not accept keyword \
1846 arguments ('{keyword}')"
1847 ),
1848 *span,
1849 );
1850 }
1851 }
1852 }
1853 let lowered: Vec<HirExpr> = self.lower_arg_values(args, macro_params);
1854 if let Some(entry) = self.manifest.resolve_member("format") {
1855 self.check_call_position("format", entry, position, span);
1856 }
1857 return HirExpr::Format {
1858 text: value.clone(),
1859 args: lowered,
1860 span: Some(span.into()),
1861 };
1862 }
1863 }
1864 let (member_name, lowered) = match self.manifest.resolve_member(name) {
1867 Some(entry) => {
1868 self.check_call_position(name, entry, position, span);
1869 if let Some(category) = entry.receiver {
1870 self.check_receiver(receiver, category, entry, span);
1871 }
1872 let (bound, _) = self.bind_args(entry, args, macro_params);
1873 (entry.id.clone(), bound)
1874 }
1875 None => {
1876 self.error_at("unknown-member", format!("unknown member '{name}'"), span);
1877 (name.to_string(), self.lower_arg_values(args, macro_params))
1878 }
1879 };
1880 if let Expr::Name { name: root, .. } = receiver {
1882 if root == "eventPlayer" {
1883 return HirExpr::ReceiverCall {
1884 receiver: Box::new(HirExpr::EventPlayer { span: None }),
1885 name: member_name,
1886 args: lowered,
1887 span: Some(span.into()),
1888 };
1889 }
1890 }
1891 HirExpr::ReceiverCall {
1893 receiver: Box::new(self.lower_expr(receiver, macro_params, CallPosition::Value)),
1894 name: member_name,
1895 args: lowered,
1896 span: Some(span.into()),
1897 }
1898 }
1899
1900 fn check_call_position(
1903 &mut self,
1904 name: &str,
1905 entry: &Function,
1906 position: CallPosition,
1907 span: Span,
1908 ) {
1909 match position {
1910 CallPosition::Statement => {
1911 if entry.context == Some(FunctionContext::ForIterable) {
1912 self.error_at(
1913 "invalid-call-context",
1914 format!("'{name}' is only valid as a for-loop iterable"),
1915 span,
1916 );
1917 } else if entry.kind.is_value() {
1918 self.error_at(
1919 "value-in-action-position",
1920 format!("value function '{name}' cannot be used as an action"),
1921 span,
1922 );
1923 }
1924 }
1925 CallPosition::Value => {
1926 if entry.kind.is_action() {
1927 self.error_at(
1928 "action-in-value-position",
1929 format!("action function '{name}' cannot be used as a value"),
1930 span,
1931 );
1932 } else if entry.context == Some(FunctionContext::ForIterable) {
1933 self.error_at(
1934 "invalid-call-context",
1935 format!("'{name}' is only valid as a for-loop iterable"),
1936 span,
1937 );
1938 }
1939 }
1940 CallPosition::ForIterable => {
1941 if entry.context != Some(FunctionContext::ForIterable) {
1942 self.error_at(
1943 "invalid-iterable",
1944 format!("for-loop iterable '{name}' must be a range(...) call"),
1945 span,
1946 );
1947 }
1948 }
1949 CallPosition::LambdaArgument => {
1950 if entry.kind.is_action() {
1951 self.error_at(
1952 "action-in-value-position",
1953 format!("action function '{name}' cannot be used as a value"),
1954 span,
1955 );
1956 } else if entry.context == Some(FunctionContext::ForIterable) {
1957 self.error_at(
1958 "invalid-call-context",
1959 format!("'{name}' is only valid as a for-loop iterable"),
1960 span,
1961 );
1962 }
1963 }
1964 }
1965 }
1966
1967 fn check_receiver(
1973 &mut self,
1974 receiver: &Expr,
1975 category: ReceiverCategory,
1976 entry: &Function,
1977 span: Span,
1978 ) {
1979 let mismatch = match category {
1980 ReceiverCategory::String => !matches!(receiver, Expr::String { .. }),
1981 ReceiverCategory::Variable => !assignable_receiver(receiver),
1982 ReceiverCategory::Player | ReceiverCategory::Any => false,
1983 };
1984 if mismatch {
1985 self.error_at(
1986 "invalid-receiver",
1987 format!(
1988 "member '{}' requires {} as its receiver",
1989 entry.id,
1990 category.describe()
1991 ),
1992 span,
1993 );
1994 }
1995 }
1996
1997 fn check_arity(&mut self, entry: &Function, got: usize, span: Span) {
1999 let (min, max) = entry.arity_bounds();
2000 let valid = got >= min && max.is_none_or(|max| got <= max);
2001 if !valid {
2002 let expects = match max {
2003 Some(max) if min == max => format!("exactly {min}"),
2004 Some(max) => format!("{min} to {max}"),
2005 None => format!("at least {min}"),
2006 };
2007 let role = match entry.kind {
2008 FunctionKind::Action => "action",
2009 FunctionKind::Value => "value",
2010 FunctionKind::MemberAction => "member action",
2011 FunctionKind::MemberValue => "member value",
2012 };
2013 self.error_at(
2014 "invalid-arity",
2015 format!(
2016 "{role} '{}' expects {expects} arguments but got {got}",
2017 entry.id
2018 ),
2019 span,
2020 );
2021 }
2022 }
2023
2024 fn error_at(&mut self, code: &str, message: String, span: Span) {
2025 self.errors.push(OpyError::at(code, message, span));
2026 }
2027}
2028
2029fn keyword_spellings(param: &Param) -> Vec<String> {
2031 let mut spellings = vec![param.name.clone()];
2032 spellings.extend(param.alternate_names.iter().cloned());
2033 spellings
2034}
2035
2036fn arg_span(args: &[CallArg]) -> Span {
2039 args.first().map(CallArg::span).unwrap_or_else(|| {
2040 Span::new(
2041 0,
2042 crate::diag::Position::new(1, 1),
2043 crate::diag::Position::new(1, 1),
2044 )
2045 })
2046}
2047
2048fn assignable_receiver(receiver: &Expr) -> bool {
2053 match receiver {
2054 Expr::Name { name, .. } => name != "eventPlayer",
2055 Expr::Array { .. } | Expr::Index { .. } => true,
2056 _ => false,
2057 }
2058}
2059
2060impl From<Span> for HirSpan {
2061 fn from(span: Span) -> HirSpan {
2062 HirSpan {
2063 file: span.file,
2064 start: Position {
2065 line: span.start.line,
2066 col: span.start.col,
2067 },
2068 end: Position {
2069 line: span.end.line,
2070 col: span.end.col,
2071 },
2072 }
2073 }
2074}
2075
2076impl From<&Span> for HirSpan {
2077 fn from(span: &Span) -> HirSpan {
2078 (*span).into()
2079 }
2080}
2081
2082#[cfg(test)]
2083mod tests {
2084 use super::*;
2085 use crate::hir::types::{Expr as HirExpr, RuleEntry as HirRuleEntry, Stmt as HirStmt};
2086 use crate::lexer::{LexInput, lex};
2087 use crate::parser::parse;
2088
2089 fn lower_ok(text: &str) -> HirProgram {
2090 let tokens = lex(LexInput { file_id: 0, text }).expect("lexes");
2091 let output = parse(&tokens);
2092 assert!(
2093 output.errors.is_empty(),
2094 "unexpected parse errors: {:?}",
2095 output.errors
2096 );
2097 let program = output.program.expect("parse produces a program");
2098 lower(&program, vec![], vec![]).expect("lowers without errors")
2099 }
2100
2101 fn rule_conditions_and_actions(hir: &HirProgram) -> (&Vec<HirExpr>, &Vec<HirStmt>) {
2102 let HirRuleEntry::Rule(rule) = &hir.rules[0] else {
2103 panic!("expected a rule");
2104 };
2105 (&rule.conditions, &rule.actions)
2106 }
2107
2108 #[test]
2109 fn producer_emits_the_v2_ordered_switch_contract() {
2110 let hir = lower_ok(
2111 "globalvar value\nrule \"r\":\n @Event global\n switch value:\n default:\n value = 1\n case 2:\n value = 2\n",
2112 );
2113 assert_eq!(hir.protocol.name, "wright/opy-hir");
2114 assert_eq!(hir.protocol.version, "2.0.0");
2115 let value = serde_json::to_value(&hir).expect("HIR must serialize");
2116 let switch = &value["rules"][0]["actions"][0];
2117 assert!(switch.get("arms").is_some());
2118 assert!(switch.get("cases").is_none());
2119 assert!(switch.get("default").is_none());
2120 }
2121
2122 #[test]
2123 fn receiver_calls_lower_to_receiver_call_hir() {
2124 let hir = lower_ok(
2128 "globalvar target\nrule \"r\":\n @Event eachPlayer\n eventPlayer.setMoveSpeed(100)\n target.setMoveSpeed(50)\n",
2129 );
2130 let (_, actions) = rule_conditions_and_actions(&hir);
2131 assert_eq!(actions.len(), 2);
2132
2133 let HirStmt::Expr { expr, .. } = &actions[0] else {
2134 panic!("expected expression statement");
2135 };
2136 let HirExpr::ReceiverCall {
2137 receiver,
2138 name,
2139 args,
2140 ..
2141 } = expr.as_ref()
2142 else {
2143 panic!("expected receiver call, got {expr:?}");
2144 };
2145 assert_eq!(name, "setMoveSpeed");
2146 assert!(matches!(receiver.as_ref(), HirExpr::EventPlayer { .. }));
2147 assert_eq!(args.len(), 1);
2148 assert!(matches!(&args[0], HirExpr::Number { .. }));
2149
2150 let HirStmt::Expr { expr, .. } = &actions[1] else {
2151 panic!("expected expression statement");
2152 };
2153 let HirExpr::ReceiverCall { receiver, name, .. } = expr.as_ref() else {
2154 panic!("expected receiver call, got {expr:?}");
2155 };
2156 assert_eq!(name, "setMoveSpeed");
2157 assert!(
2158 matches!(receiver.as_ref(), HirExpr::GlobalVar { name, .. } if name == "target"),
2159 "globalvar receiver must resolve to a GlobalVar"
2160 );
2161 }
2162
2163 #[test]
2164 fn bare_variable_member_expression_preserves_receiver_and_member() {
2165 let hir = lower_ok(
2166 "globalvar A\nplayervar B\nrule \"receiver\":\n @Event eachPlayer\n A = B.C\n",
2167 );
2168 let HirStmt::Assign { value, .. } = &hir
2169 .rules
2170 .iter()
2171 .find_map(|entry| {
2172 let RuleEntry::Rule(rule) = entry else {
2173 return None;
2174 };
2175 rule.actions.first()
2176 })
2177 .expect("assignment")
2178 else {
2179 panic!("expected assignment");
2180 };
2181 let HirExpr::Member {
2182 receiver, member, ..
2183 } = value.as_ref()
2184 else {
2185 panic!("expected opaque member expression, got {value:?}");
2186 };
2187 assert_eq!(member, "C");
2188 assert!(matches!(receiver.as_ref(), HirExpr::GlobalVar { name, .. } if name == "B"));
2189 }
2190
2191 #[test]
2192 fn rule_prefix_template_is_global_and_subroutine_identity_is_preserved() {
2193 let text = "rule \"before\":\n pass\ndef source_name():\n @Name \"Friendly\"\n pass\nrule \"after\":\n pass\n";
2194 let tokens = lex(LexInput { file_id: 0, text }).expect("lexes");
2195 let output = parse(&tokens);
2196 assert!(
2197 output.errors.is_empty(),
2198 "unexpected parse errors: {:?}",
2199 output.errors
2200 );
2201 let program = output.program.expect("program");
2202 let preprocessing = PreprocessingState {
2203 rule_prefix_template: Some(crate::hir::types::DirectiveValue {
2204 value: "f\"[{$pathTitle.replace('_', ' ')}] {$rule}\" if $rule and not $isDelimiter else $rule".to_string(),
2205 span: None,
2206 }),
2207 ..PreprocessingState::default()
2208 };
2209 let hir = lower_with_preprocessing(
2210 &program,
2211 vec![SourceFile {
2212 id: 0,
2213 path: "main.opy".to_string(),
2214 }],
2215 vec![],
2216 &preprocessing,
2217 )
2218 .expect("lowers");
2219 let names: Vec<_> = hir
2220 .rules
2221 .iter()
2222 .map(|entry| match entry {
2223 HirRuleEntry::Rule(rule) => rule.name.clone(),
2224 HirRuleEntry::SubroutineDef { name, .. } => name.clone(),
2225 })
2226 .collect();
2227 assert_eq!(
2228 names,
2229 vec!["[Main] before", "[Main] Friendly", "[Main] after"]
2230 );
2231 let HirRuleEntry::SubroutineDef {
2232 name, source_name, ..
2233 } = &hir.rules[1]
2234 else {
2235 panic!("expected subroutine definition");
2236 };
2237 assert_eq!(name, "[Main] Friendly");
2238 assert_eq!(source_name, "source_name");
2239 }
2240
2241 #[test]
2242 fn receiver_call_values_lower_in_conditions() {
2243 let hir = lower_ok(
2247 "rule \"r\":\n @Event eachPlayer\n @Condition eventPlayer.isAlive()\n eventPlayer.teleport(eventPlayer.getPosition())\n",
2248 );
2249 let (conditions, actions) = rule_conditions_and_actions(&hir);
2250 assert_eq!(conditions.len(), 1);
2251 let HirExpr::ReceiverCall { name, args, .. } = &conditions[0] else {
2252 panic!("expected receiver call condition, got {:?}", conditions[0]);
2253 };
2254 assert_eq!(name, "isAlive");
2255 assert_eq!(args.len(), 0);
2256
2257 let HirStmt::Expr { expr, .. } = &actions[0] else {
2258 panic!("expected expression statement");
2259 };
2260 let HirExpr::ReceiverCall {
2261 name,
2262 args,
2263 receiver,
2264 ..
2265 } = expr.as_ref()
2266 else {
2267 panic!("expected receiver call, got {expr:?}");
2268 };
2269 assert_eq!(name, "teleport");
2270 assert!(matches!(receiver.as_ref(), HirExpr::EventPlayer { .. }));
2271 assert_eq!(args.len(), 1);
2272 assert!(matches!(
2273 &args[0],
2274 HirExpr::ReceiverCall { name, .. } if name == "getPosition"
2275 ));
2276 }
2277
2278 #[test]
2279 fn format_string_receiver_stays_a_format_node() {
2280 let hir = lower_ok(
2283 "rule \"r\":\n @Event global\n print(\"{} points\".format(len([1, 2])))\n",
2284 );
2285 let (_, actions) = rule_conditions_and_actions(&hir);
2286 let HirStmt::Expr { expr, .. } = &actions[0] else {
2287 panic!("expected expression statement");
2288 };
2289 assert!(
2290 has_format(expr),
2291 "string `.format()` must lower to a Format node"
2292 );
2293 }
2294
2295 fn has_format(expr: &HirExpr) -> bool {
2296 match expr {
2297 HirExpr::Format { .. } => true,
2298 HirExpr::Call { args, .. } => args.iter().any(has_format),
2299 HirExpr::ReceiverCall { args, .. } => args.iter().any(has_format),
2300 _ => false,
2301 }
2302 }
2303
2304 fn lowered_value(source: &str) -> HirExpr {
2306 let program = crate::compile(source, "test.opy", std::path::Path::new(""))
2307 .unwrap_or_else(|error| panic!("compile failed: {error}"));
2308 let RuleEntry::Rule(rule) = &program.rules[0] else {
2309 panic!("expected a rule");
2310 };
2311 let HirStmt::Assign { value, .. } = &rule.actions[0] else {
2312 panic!("expected an assign statement");
2313 };
2314 (**value).clone()
2315 }
2316
2317 #[test]
2318 fn chase_time_reeval_none_lowers_to_the_catalog_enum() {
2319 let value = lowered_value(
2320 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NONE\n",
2321 );
2322 assert_enum(&value, "ChaseTimeReeval", "NONE");
2323 }
2324
2325 #[test]
2326 fn chase_time_reeval_destination_and_duration_lowers_to_the_catalog_enum() {
2327 let value = lowered_value(
2328 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.DESTINATION_AND_DURATION\n",
2329 );
2330 assert_enum(&value, "ChaseTimeReeval", "DESTINATION_AND_DURATION");
2331 }
2332
2333 #[test]
2334 fn chase_rate_reeval_members_lower_to_the_catalog_enum() {
2335 for member in ["NONE", "DESTINATION_AND_RATE"] {
2336 let source = format!(
2337 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseRateReeval.{member}\n"
2338 );
2339 assert_enum(&lowered_value(&source), "ChaseRateReeval", member);
2340 }
2341 }
2342
2343 fn assert_enum(value: &HirExpr, domain: &str, member: &str) {
2346 match value {
2347 HirExpr::Enum {
2348 value_type, value, ..
2349 } => {
2350 assert_eq!(value_type, domain);
2351 assert_eq!(value, member);
2352 }
2353 other => panic!("expected enum {domain}.{member}, got {other:?}"),
2354 }
2355 }
2356
2357 #[test]
2358 fn unknown_chase_time_reeval_member_is_rejected_by_the_catalog() {
2359 let error = crate::compile(
2360 "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NOPE\n",
2361 "test.opy",
2362 std::path::Path::new(""),
2363 )
2364 .expect_err("unknown catalog member must be rejected");
2365 assert_eq!(error.code, "unknown-enum-member");
2366 }
2367
2368 #[test]
2369 fn unknown_enum_receiver_is_an_unsupported_member_error() {
2370 let error = crate::compile(
2371 "globalvar g\nrule \"r\":\n @Event global\n g = NotARealEnum.MEMBER\n",
2372 "test.opy",
2373 std::path::Path::new(""),
2374 )
2375 .expect_err("an unknown enum type must fail");
2376 assert_eq!(error.code, "unsupported-member");
2377 let span = error.span.expect("the error is source-located");
2378 assert_eq!(span.start.line, 4);
2379 }
2380
2381 fn compile_error(source: &str, line: u32) -> OpyError {
2385 let error = crate::compile(source, "test.opy", std::path::Path::new(""))
2386 .expect_err("expected a compile failure");
2387 let span = error.span.expect("the error is source-located");
2388 assert_eq!(span.start.line, line, "code '{}'", error.code);
2389 error
2390 }
2391
2392 fn action_source(statement: &str) -> String {
2393 format!("globalvar g\nrule \"r\":\n @Event global\n {statement}\n")
2394 }
2395
2396 #[test]
2397 fn chase_over_time_resolves_and_compiles_with_reference_signatures() {
2398 let hir = crate::compile(
2400 &action_source("chaseOverTime(g, 10, 3, ChaseTimeReeval.NONE)"),
2401 "test.opy",
2402 std::path::Path::new(""),
2403 )
2404 .expect("reference-supported chaseOverTime compiles");
2405 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2406 panic!("expected a rule");
2407 };
2408 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2409 panic!("expected expression statement");
2410 };
2411 let HirExpr::Call { name, args, .. } = expr.as_ref() else {
2412 panic!("expected a call, got {expr:?}");
2413 };
2414 assert_eq!(name, "chaseOverTime");
2415 assert_eq!(args.len(), 4);
2416 assert!(matches!(
2417 &args[3],
2418 HirExpr::Enum { value_type, value, .. }
2419 if value_type == "ChaseTimeReeval" && value == "NONE"
2420 ));
2421
2422 let hir = crate::compile(
2424 &action_source("chaseOverTime(g, 10, 3)"),
2425 "test.opy",
2426 std::path::Path::new(""),
2427 )
2428 .expect("default-reevaluation chaseOverTime compiles");
2429 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2430 panic!("expected a rule");
2431 };
2432 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2433 panic!("expected expression statement");
2434 };
2435 let HirExpr::Call { args, .. } = expr.as_ref() else {
2436 panic!("expected a call");
2437 };
2438 assert_eq!(args.len(), 4);
2439 assert!(matches!(
2440 &args[3],
2441 HirExpr::Enum { value_type, value, .. }
2442 if value_type == "ChaseTimeReeval" && value == "DESTINATION_AND_DURATION"
2443 ));
2444 }
2445
2446 #[test]
2447 fn is_game_in_progress_resolves_as_a_builtin_value() {
2448 let hir = crate::compile(
2450 &action_source("@Condition isGameInProgress() == true"),
2451 "test.opy",
2452 std::path::Path::new(""),
2453 )
2454 .expect("reference-supported isGameInProgress compiles");
2455 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2456 panic!("expected a rule");
2457 };
2458 assert!(matches!(&rule.conditions[0], HirExpr::Binary { .. }));
2459 }
2460
2461 #[test]
2462 fn enum_gated_members_resolve_through_the_manifest() {
2463 let source = "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2467 @Condition eventPlayer.getThrottle() != vect(0, 0, 0)\n \
2468 @Condition worldVector(vect(1, 2, 3), eventPlayer, Transform.ROTATION) != vect(0, 0, 0)\n \
2469 eventPlayer.setInvisibility(Invis.ALL)\n \
2470 eventPlayer.setStatusEffect(eventPlayer, Status.ROOTED, 2)\n";
2471 let hir = crate::compile(source, "test.opy", std::path::Path::new(""))
2472 .expect("enum-gated members compile");
2473 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2474 panic!("expected a rule");
2475 };
2476 assert_eq!(rule.actions.len(), 2);
2477 }
2478
2479 #[test]
2480 fn get_players_in_radius_fills_reference_enum_defaults() {
2481 let hir = crate::compile(
2484 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2485 @Condition len(getPlayersInRadius(eventPlayer.getPosition(), 10)) > 0\n \
2486 disableInspector()\n",
2487 "test.opy",
2488 std::path::Path::new(""),
2489 )
2490 .expect("getPlayersInRadius with defaults compiles");
2491 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2492 panic!("expected a rule");
2493 };
2494 let HirExpr::Binary { left, .. } = &rule.conditions[0] else {
2495 panic!("expected a comparison");
2496 };
2497 let HirExpr::Call { name, args, .. } = left.as_ref() else {
2498 panic!("expected len call");
2499 };
2500 assert_eq!(name, "len");
2501 let HirExpr::Call { name, args, .. } = &args[0] else {
2502 panic!("expected getPlayersInRadius call");
2503 };
2504 assert_eq!(name, "getPlayersInRadius");
2505 assert_eq!(args.len(), 4);
2506 assert!(matches!(
2507 &args[2],
2508 HirExpr::Enum { value_type, value, .. }
2509 if value_type == "Team" && value == "ALL"
2510 ));
2511 assert!(matches!(
2512 &args[3],
2513 HirExpr::Enum { value_type, value, .. }
2514 if value_type == "LosCheck" && value == "OFF"
2515 ));
2516 }
2517
2518 #[test]
2519 fn value_call_in_action_position_is_rejected() {
2520 let error = compile_error(&action_source("isGameInProgress()"), 4);
2521 assert_eq!(error.code, "value-in-action-position");
2522 }
2523
2524 #[test]
2525 fn value_member_in_action_position_is_rejected() {
2526 let error = compile_error(
2529 "globalvar g\nrule \"r\":\n @Event eachPlayer\n eventPlayer.isAlive()\n",
2530 4,
2531 );
2532 assert_eq!(error.code, "value-in-action-position");
2533 }
2534
2535 fn first_action_expr(source: &str) -> HirExpr {
2540 let program = crate::compile(source, "test.opy", std::path::Path::new(""))
2541 .unwrap_or_else(|error| panic!("compile failed: {error}"));
2542 let RuleEntry::Rule(rule) = &program.rules[0] else {
2543 panic!("expected a rule");
2544 };
2545 match &rule.actions[0] {
2546 HirStmt::Expr { expr, .. } => (**expr).clone(),
2547 HirStmt::Assign { value, .. } => (**value).clone(),
2548 other => panic!("expected an expression or assignment, got {other:?}"),
2549 }
2550 }
2551
2552 fn strip_spans(value: &mut serde_json::Value) {
2555 match value {
2556 serde_json::Value::Object(map) => {
2557 map.remove("span");
2558 map.remove("name_span");
2559 for nested in map.values_mut() {
2560 strip_spans(nested);
2561 }
2562 }
2563 serde_json::Value::Array(items) => {
2564 for item in items {
2565 strip_spans(item);
2566 }
2567 }
2568 _ => {}
2569 }
2570 }
2571
2572 #[test]
2573 fn chase_keyword_forms_dispatch_to_the_concrete_chase_functions() {
2574 let expr = first_action_expr(&action_source("chase(g, 10, rate=2, ChaseReeval.NONE)"));
2579 let HirExpr::Call { name, args, .. } = &expr else {
2580 panic!("expected a call, got {expr:?}");
2581 };
2582 assert_eq!(name, "chaseAtRate");
2583 assert!(matches!(
2584 &args[3],
2585 HirExpr::Enum { value_type, value, .. }
2586 if value_type == "ChaseRateReeval" && value == "NONE"
2587 ));
2588
2589 let expr = first_action_expr(&action_source(
2590 "chase(g, 10, duration=3, ChaseReeval.DESTINATION_AND_DURATION)",
2591 ));
2592 let HirExpr::Call { name, args, .. } = &expr else {
2593 panic!("expected a call, got {expr:?}");
2594 };
2595 assert_eq!(name, "chaseOverTime");
2596 assert!(matches!(
2597 &args[3],
2598 HirExpr::Enum { value_type, value, .. }
2599 if value_type == "ChaseTimeReeval" && value == "DESTINATION_AND_DURATION"
2600 ));
2601
2602 let expr = first_action_expr(
2605 "playervar P\nrule \"r\":\n @Event eachPlayer\n \
2606 chase(eventPlayer.P, 0, rate=1, ChaseReeval.NONE)\n",
2607 );
2608 let HirExpr::Call { name, args, .. } = &expr else {
2609 panic!("expected a call, got {expr:?}");
2610 };
2611 assert_eq!(name, "chaseAtRate");
2612 assert!(matches!(&args[0], HirExpr::PlayerVar { .. }));
2613 }
2614
2615 #[test]
2616 fn chase_reeval_is_only_a_standalone_identity_inside_the_chase_context() {
2617 let error = compile_error(&action_source("g = ChaseReeval.NONE"), 4);
2621 assert_eq!(error.code, "unsupported-member");
2622
2623 let expr = first_action_expr(&action_source(
2628 "chase(g, 10, rate=2, ChaseReeval.DESTINATION_AND_DURATION)",
2629 ));
2630 let HirExpr::Call { name, args, .. } = &expr else {
2631 panic!("expected a call, got {expr:?}");
2632 };
2633 assert_eq!(name, "chaseAtRate");
2634 assert!(matches!(
2635 &args[3],
2636 HirExpr::Enum { value_type, value, .. }
2637 if value_type == "ChaseRateReeval" && value == "DESTINATION_AND_DURATION"
2638 ));
2639
2640 let expr = first_action_expr(&action_source("chase(g, 10, rate=2, 5)"));
2643 let HirExpr::Call { name, args, .. } = &expr else {
2644 panic!("expected a call, got {expr:?}");
2645 };
2646 assert_eq!(name, "chase");
2647 assert!(matches!(&args[3], HirExpr::Number { .. }));
2648 }
2649
2650 #[test]
2651 fn chase_requires_the_keyword_rate_or_duration_third_argument() {
2652 let error = compile_error(&action_source("chase(g, 10, 2, ChaseReeval.NONE)"), 4);
2653 assert_eq!(error.code, "keyword-required");
2654 assert!(error.message.contains("rate"));
2655 }
2656
2657 #[test]
2658 fn chase_family_requires_a_variable_first_argument() {
2659 let error = compile_error(&action_source("chase(10, 10, rate=2, ChaseReeval.NONE)"), 4);
2664 assert_eq!(error.code, "invalid-argument");
2665
2666 let error = compile_error(
2667 &action_source("chaseOverTime(10, 0, 30, ChaseTimeReeval.NONE)"),
2668 4,
2669 );
2670 assert_eq!(error.code, "invalid-argument");
2671 }
2672
2673 #[test]
2674 fn keyword_binding_matches_positional_binding_in_hir() {
2675 fn without_spans(expr: &HirExpr) -> serde_json::Value {
2679 let mut value = serde_json::to_value(expr).unwrap();
2680 strip_spans(&mut value);
2681 value
2682 }
2683 let keyword = without_spans(&first_action_expr(&action_source(
2684 "chaseOverTime(g, 10, duration=3)",
2685 )));
2686 let positional = without_spans(&first_action_expr(&action_source(
2687 "chaseOverTime(g, 10, 3)",
2688 )));
2689 assert_eq!(keyword, positional);
2690
2691 let keyword = without_spans(&first_action_expr(&action_source("wait(time=1)")));
2692 let positional = without_spans(&first_action_expr(&action_source("wait(1)")));
2693 assert_eq!(keyword, positional);
2694
2695 let keyword = without_spans(&first_action_expr(&action_source(
2697 "wait(waitBehavior=Wait.IGNORE_CONDITION, time=2)",
2698 )));
2699 let positional = without_spans(&first_action_expr(&action_source("wait(2)")));
2700 assert_eq!(keyword, positional);
2701
2702 let keyword = without_spans(&first_action_expr(&action_source(
2703 "g = vect(x=1, y=2, z=3)",
2704 )));
2705 let positional = without_spans(&first_action_expr(&action_source("g = vect(1, 2, 3)")));
2706 assert_eq!(keyword, positional);
2707 }
2708
2709 #[test]
2710 fn keyword_binding_diagnostics_are_structured_and_source_located() {
2711 let error = compile_error(&action_source("chaseOverTime(g, 10, bogus=1)"), 4);
2713 assert_eq!(error.code, "unknown-keyword");
2714 assert!(error.message.contains("bogus"));
2715
2716 let error = compile_error(
2718 &action_source(
2719 "chaseOverTime(g, 10, 3, ChaseTimeReeval.NONE, \
2720 reevaluation=ChaseTimeReeval.NONE)",
2721 ),
2722 4,
2723 );
2724 assert_eq!(error.code, "duplicate-argument");
2725
2726 let error = compile_error(&action_source("chaseOverTime(g, duration=3, 5)"), 4);
2728 assert_eq!(error.code, "positional-after-keyword");
2729
2730 let error = compile_error(&action_source("chaseOverTime(g, 10)"), 4);
2732 assert_eq!(error.code, "missing-argument");
2733
2734 let error = compile_error(
2737 &action_source("chase(variable=g, destination=10, rate=2, ChaseReeval.NONE)"),
2738 4,
2739 );
2740 assert_eq!(error.code, "unknown-keyword");
2741 }
2742
2743 #[test]
2744 fn keyword_arguments_are_rejected_for_reference_special_cases() {
2745 let error = compile_error(
2748 "globalvar g\nrule \"r\":\n @Event global\n \
2749 for I in range(start=0, stop=3):\n debug(I)\n",
2750 4,
2751 );
2752 assert_eq!(error.code, "keyword-unsupported");
2753
2754 let error = compile_error(&action_source("g = random.uniform(min=1, max=2)"), 4);
2755 assert_eq!(error.code, "keyword-unsupported");
2756
2757 let error = compile_error(&action_source("print(\"{} points\".format(value=1))"), 4);
2758 assert_eq!(error.code, "keyword-unsupported");
2759 }
2760
2761 #[test]
2762 fn wait_uses_the_reference_keyword_names() {
2763 let error = compile_error(&action_source("wait(duration=1)"), 4);
2767 assert_eq!(error.code, "unknown-keyword");
2768 assert!(error.message.contains("duration"));
2769 }
2770
2771 #[test]
2772 fn action_call_in_value_position_is_rejected() {
2773 let error = compile_error(&action_source("g = wait(1)"), 4);
2774 assert_eq!(error.code, "action-in-value-position");
2775 }
2776
2777 #[test]
2778 fn missing_required_argument_is_a_source_located_diagnostic() {
2779 let error = compile_error(&action_source("chaseOverTime(g, 10)"), 4);
2783 assert_eq!(error.code, "missing-argument");
2784 assert!(error.message.contains("duration"));
2785
2786 let error = compile_error(&action_source("chaseOverTime(g, 10, 3, 4, 5)"), 4);
2787 assert_eq!(error.code, "invalid-arity");
2788 }
2789
2790 #[test]
2791 fn missing_member_argument_is_a_source_located_diagnostic() {
2792 let error = compile_error(
2797 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2798 getPlayersInRadius(eventPlayer.getPosition(), 10).setStatusEffect(eventPlayer, 30)\n",
2799 4,
2800 );
2801 assert_eq!(error.code, "missing-argument");
2802 assert!(error.message.contains("duration"));
2803 }
2804
2805 #[test]
2806 fn invalid_receiver_categories_are_rejected() {
2807 let error = compile_error(&action_source("3.append(1)"), 4);
2810 assert_eq!(error.code, "invalid-receiver");
2811 assert!(error.message.contains("append"));
2812
2813 let error = compile_error(&action_source("print(3.format(\"{}\"))"), 4);
2814 assert_eq!(error.code, "invalid-receiver");
2815 assert!(error.message.contains("format"));
2816 }
2817
2818 #[test]
2819 fn cross_domain_enum_arguments_resolve_as_opaque_identities() {
2820 let expr = first_action_expr(&action_source("chaseOverTime(g, 10, 3, Invis.ALL)"));
2824 let HirExpr::Call { args, .. } = &expr else {
2825 panic!("expected a call, got {expr:?}");
2826 };
2827 assert!(matches!(
2828 &args[3],
2829 HirExpr::Enum { value_type, value, .. }
2830 if value_type == "Invis" && value == "ALL"
2831 ));
2832
2833 let expr = first_action_expr(&action_source(
2834 "eventPlayer.setInvisibility(ChaseTimeReeval.NONE)",
2835 ));
2836 let HirExpr::ReceiverCall { args, .. } = &expr else {
2837 panic!("expected a receiver call, got {expr:?}");
2838 };
2839 assert!(matches!(
2840 &args[0],
2841 HirExpr::Enum { value_type, value, .. }
2842 if value_type == "ChaseTimeReeval" && value == "NONE"
2843 ));
2844 }
2845
2846 #[test]
2847 fn non_enum_arguments_for_enum_parameters_are_carried_structurally() {
2848 let expr = first_action_expr(&action_source("eventPlayer.setInvisibility(g)"));
2852 let HirExpr::ReceiverCall { args, .. } = &expr else {
2853 panic!("expected a receiver call, got {expr:?}");
2854 };
2855 assert!(matches!(
2856 &args[0],
2857 HirExpr::GlobalVar { name, .. } if name == "g"
2858 ));
2859
2860 let expr = first_action_expr(&action_source("eventPlayer.setInvisibility(3)"));
2861 let HirExpr::ReceiverCall { args, .. } = &expr else {
2862 panic!("expected a receiver call, got {expr:?}");
2863 };
2864 assert!(matches!(&args[0], HirExpr::Number { .. }));
2865 }
2866
2867 #[test]
2868 fn unknown_builtins_fail_at_resolution_not_emission() {
2869 let error = compile_error(&action_source("frobnicate()"), 4);
2870 assert_eq!(error.code, "unknown-action");
2871
2872 let error = compile_error(&action_source("g = frobnicate()"), 4);
2873 assert_eq!(error.code, "unknown-value");
2874
2875 let error = compile_error(
2876 "globalvar g\nrule \"r\":\n @Event eachPlayer\n eventPlayer.frobnicate()\n",
2877 4,
2878 );
2879 assert_eq!(error.code, "unknown-member");
2880 }
2881
2882 #[test]
2883 fn wright_only_catalog_names_are_rejected() {
2884 let error = compile_error(&action_source("createHudText(1)"), 4);
2888 assert_eq!(error.code, "unknown-action");
2889
2890 let error = compile_error(&action_source("g = squareRoot(9)"), 4);
2891 assert_eq!(error.code, "unknown-value");
2892 }
2893
2894 #[test]
2895 fn generic_member_only_actions_are_rejected() {
2896 let error = compile_error(&action_source("setMoveSpeed(eventPlayer, 100)"), 4);
2899 assert_eq!(error.code, "unknown-action");
2900 }
2901
2902 #[test]
2903 fn range_is_for_iterables_only() {
2904 let error = compile_error(&action_source("@Condition len(range(1, 5, 1)) > 0"), 4);
2907 assert_eq!(error.code, "invalid-call-context");
2908
2909 let error = compile_error(&action_source("for g in [1, 2]:\n debug(g)"), 4);
2910 assert_eq!(error.code, "invalid-iterable");
2911
2912 crate::compile(
2913 &action_source("for g in range(3):\n debug(g)"),
2914 "test.opy",
2915 std::path::Path::new(""),
2916 )
2917 .expect("the for-header range form compiles");
2918 }
2919
2920 #[test]
2921 fn source_aliases_resolve_to_canonical_names() {
2922 let hir = crate::compile(
2925 &action_source("stopChasingVariable(g)"),
2926 "test.opy",
2927 std::path::Path::new(""),
2928 )
2929 .expect("the alias target compiles");
2930 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2931 panic!("expected a rule");
2932 };
2933 let HirStmt::Expr { expr, .. } = &rule.actions[0] else {
2934 panic!("expected expression statement");
2935 };
2936 let HirExpr::Call { name, .. } = expr.as_ref() else {
2937 panic!("expected a call");
2938 };
2939 assert_eq!(name, "stopChasing");
2940
2941 let hir = crate::compile(
2942 "globalvar g\nrule \"r\":\n @Event eachPlayer\n \
2943 @Condition eventPlayer.getCurrentHero() != null\n \
2944 @Condition eventPlayer.hasStatusEffect(Status.BURNING) == false\n \
2945 disableInspector()\n",
2946 "test.opy",
2947 std::path::Path::new(""),
2948 )
2949 .expect("member aliases compile");
2950 let RuleEntry::Rule(rule) = &hir.rules[0] else {
2951 panic!("expected a rule");
2952 };
2953 let HirExpr::Binary { left, .. } = &rule.conditions[0] else {
2954 panic!("expected a comparison");
2955 };
2956 let HirExpr::ReceiverCall { name, .. } = left.as_ref() else {
2957 panic!("expected a receiver call");
2958 };
2959 assert_eq!(name, "getHero");
2960 }
2961
2962 #[test]
2963 fn unknown_catalog_enum_members_are_rejected() {
2964 for source in [
2965 "globalvar g\nrule \"r\":\n @Event global\n g = Color.CYAN\n",
2966 "globalvar g\nrule \"r\":\n @Event global\n g = DynamicEffect.SPARKLES\n",
2967 ] {
2968 let error = crate::compile(source, "test.opy", std::path::Path::new(""))
2969 .expect_err("unknown catalog member must be rejected");
2970 assert_eq!(error.code, "unknown-enum-member");
2971 }
2972 }
2973
2974 #[test]
2975 fn default_var_for_binder_resolves_at_all_range_arities() {
2976 for (binder, iterable) in [
2981 ("I", "range(0, 10)"),
2982 ("I", "range(3)"),
2983 ("I", "range(1, 5, 2)"),
2984 ] {
2985 let hir = lower_ok(&format!(
2986 "globalvar total\nrule \"r\":\n @Event global\n for {binder} in {iterable}:\n total += {binder}\n"
2987 ));
2988 let (_, actions) = rule_conditions_and_actions(&hir);
2989 let HirStmt::For { variable, body, .. } = &actions[0] else {
2990 panic!("expected a for statement");
2991 };
2992 assert!(
2993 matches!(variable.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
2994 "the binder resolves to the implicit global 'I', got {variable:?}"
2995 );
2996 assert!(!body.is_empty(), "the loop body lowers");
2997 let HirStmt::Assign { value, .. } = &body[0] else {
3000 panic!("expected an assignment in the body");
3001 };
3002 let HirExpr::Binary { right, .. } = value.as_ref() else {
3003 panic!("expected a binary expression");
3004 };
3005 assert!(
3006 matches!(right.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3007 "the binder use inside the body resolves to the implicit global"
3008 );
3009 }
3010 }
3011
3012 #[test]
3013 fn player_variable_range_binder_preserves_host_player_receiver() {
3014 let hir = lower_ok(
3015 "playervar I\nrule \"r\":\n @Event global\n for hostPlayer.I in range(3):\n hostPlayer.I = 1\n",
3016 );
3017 let (_, actions) = rule_conditions_and_actions(&hir);
3018 let HirStmt::For { variable, .. } = &actions[0] else {
3019 panic!("expected a for statement");
3020 };
3021 let HirExpr::PlayerVar {
3022 player,
3023 name,
3024 member_span,
3025 span,
3026 } = variable.as_ref()
3027 else {
3028 panic!("expected a player-variable binder, got {variable:?}");
3029 };
3030 assert_eq!(name, "I");
3031 assert!(matches!(player.as_ref(), HirExpr::HostPlayer { .. }));
3032 assert_eq!(span.unwrap().start.line, 4);
3033 assert_eq!(span.unwrap().start.col, 9);
3034 assert_eq!(span.unwrap().end.line, 4);
3035 assert_eq!(span.unwrap().end.col, 21);
3036 let member_span = member_span.expect("player binder member span");
3037 assert_eq!(member_span.start.line, 4);
3038 assert_eq!(member_span.start.col, 20);
3039 assert_eq!(member_span.end.line, 4);
3040 assert_eq!(member_span.end.col, 21);
3041 }
3042
3043 #[test]
3044 fn default_var_names_resolve_as_implicit_globals() {
3045 let hir = lower_ok("rule \"r\":\n @Event global\n I = 5\n debug(I)\n");
3048 let (_, actions) = rule_conditions_and_actions(&hir);
3049 let HirStmt::Assign { target, .. } = &actions[0] else {
3050 panic!("expected an assignment");
3051 };
3052 assert!(
3053 matches!(target.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3054 "the implicit global resolves, got {target:?}"
3055 );
3056 assert_eq!(default_var_index("I"), Some(8));
3058 assert_eq!(default_var_index("AA"), Some(26));
3059 assert_eq!(default_var_index("Z"), Some(25));
3060 assert_eq!(default_var_index("DX"), Some(127));
3061 assert_eq!(default_var_index("DY"), None);
3062 assert_eq!(default_var_index("i"), None);
3063 }
3064
3065 #[test]
3066 fn nested_same_name_for_binders_reuse_the_implicit_global() {
3067 let hir = lower_ok(
3071 "rule \"r\":\n @Event global\n for I in range(3):\n for I in range(2):\n debug(I)\n",
3072 );
3073 let (_, actions) = rule_conditions_and_actions(&hir);
3074 let HirStmt::For {
3075 variable: outer,
3076 body,
3077 ..
3078 } = &actions[0]
3079 else {
3080 panic!("expected an outer for statement");
3081 };
3082 let HirStmt::For {
3083 variable: inner, ..
3084 } = &body[0]
3085 else {
3086 panic!("expected an inner for statement");
3087 };
3088 assert!(
3089 matches!(outer.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I")
3090 && matches!(inner.as_ref(), HirExpr::GlobalVar { name, .. } if name == "I"),
3091 "both loops bind the same implicit global (spans differ per binder site)"
3092 );
3093 }
3094
3095 #[test]
3096 fn undeclared_lowercase_binder_is_still_an_unknown_identifier() {
3097 let error = compile_error(
3102 "rule \"r\":\n @Event global\n for i in range(3):\n debug(i)\n",
3103 3,
3104 );
3105 assert_eq!(error.code, "unknown-identifier");
3106 let span = error.span.expect("the error is source-located");
3107 assert_eq!(span.start.line, 3);
3108 }
3109
3110 #[test]
3111 fn issue_28_constructs_lower_to_provenance_preserving_hir() {
3112 let hir = lower_ok(
3113 "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",
3114 );
3115 let (_, actions) = rule_conditions_and_actions(&hir);
3116 let HirStmt::DoWhile { condition, .. } = &actions[0] else {
3117 panic!("expected do-while");
3118 };
3119 assert!(matches!(condition.as_ref(), HirExpr::Binary { op, .. } if op == "not in"));
3120 let HirStmt::Switch { arms, .. } = &actions[1] else {
3121 panic!("expected switch");
3122 };
3123 assert_eq!(arms.len(), 2);
3124 let HirSwitchArm::Case {
3125 value: case_value,
3126 body,
3127 ..
3128 } = &arms[0]
3129 else {
3130 panic!("expected case arm");
3131 };
3132 assert!(
3133 matches!(case_value.as_ref(), HirExpr::Number { value, .. } if *value == 0x10 as f64)
3134 );
3135 let HirStmt::Assign { value, .. } = &body[0] else {
3136 panic!("expected case assignment");
3137 };
3138 assert!(matches!(value.as_ref(), HirExpr::Binary { op, .. } if op == "in"));
3139 let HirSwitchArm::Default { body, .. } = &arms[1] else {
3140 panic!("expected default arm");
3141 };
3142 assert!(matches!(body[0], HirStmt::Assign { .. }));
3143 let HirStmt::Assign { value, .. } = &actions[2] else {
3144 panic!("expected comprehension assignment");
3145 };
3146 assert!(matches!(value.as_ref(), HirExpr::Comprehension { .. }));
3147 let HirStmt::Assign { value, .. } = &actions[3] else {
3148 panic!("expected sorted assignment");
3149 };
3150 assert!(
3151 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")))
3152 );
3153 let HirStmt::Assign { value, .. } = &actions[4] else {
3154 panic!("expected string assignment");
3155 };
3156 assert!(
3157 matches!(value.as_ref(), HirExpr::StringModifier { modifier, .. } if modifier == "w")
3158 );
3159 }
3160
3161 #[test]
3162 fn issue_28_rejects_reference_invalid_bare_dict_and_lambda() {
3163 let dict_error = compile_error(
3164 "globalvar x\nrule \"r\":\n @Event global\n x = {\"x\": 1}\n",
3165 4,
3166 );
3167 assert_eq!(dict_error.code, "dict-access");
3168 let lambda_error = compile_error(
3169 "globalvar x\nrule \"r\":\n @Event global\n x = lambda value: value\n",
3170 4,
3171 );
3172 assert_eq!(lambda_error.code, "lambda-context");
3173 }
3174
3175 #[test]
3176 fn do_while_requires_rule_or_definition_prefix_position() {
3177 let error = compile_error(
3178 "globalvar value\nrule \"r\":\n @Event global\n value = 1\n do:\n value += 1\n while value < 2\n",
3179 5,
3180 );
3181 assert_eq!(error.code, "do-while-placement");
3182 assert_eq!(
3183 error.message,
3184 "do-while must be at the beginning of a rule, subroutine, or do-while body; only pass statements may precede it"
3185 );
3186 }
3187}