1use std::collections::HashMap;
11use std::fmt;
12
13use crate::catalog::{Catalog, Kind};
14use crate::core::source::Span;
15use crate::wir::{self, Action, ActionId, Program, Value, ValueId};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ElementNodeKind {
20 Rule,
21 Condition,
22 Action,
23 Value,
24}
25
26#[derive(Debug, Clone)]
28#[non_exhaustive]
29pub struct ElementCountNode {
30 pub kind: ElementNodeKind,
31 pub id: usize,
34 pub name: String,
36 pub span: Option<Span>,
38 pub base_count: usize,
40 pub adjustment: isize,
43 pub count: usize,
45 pub children: Vec<ElementCountNode>,
47}
48
49#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub struct ElementCountReport {
53 pub total: usize,
55 pub rules: Vec<ElementCountNode>,
57}
58
59impl ElementCountNode {
60 pub fn height(&self) -> usize {
67 1 + self
68 .children
69 .iter()
70 .map(Self::height)
71 .max()
72 .unwrap_or_default()
73 }
74
75 pub fn node_count(&self) -> usize {
80 1 + self.children.iter().map(Self::node_count).sum::<usize>()
81 }
82
83 pub fn statement_value_nodes(&self) -> usize {
98 if self.kind == ElementNodeKind::Value {
99 return self.node_count();
100 }
101 self.children
102 .iter()
103 .map(|child| match child.kind {
104 ElementNodeKind::Value | ElementNodeKind::Condition => {
105 child.statement_value_nodes()
106 }
107 ElementNodeKind::Rule | ElementNodeKind::Action => 0,
108 })
109 .sum()
110 }
111}
112
113impl ElementCountReport {
114 pub fn rule_counts(&self) -> impl Iterator<Item = (&str, usize)> {
116 self.rules
117 .iter()
118 .map(|rule| (rule.name.as_str(), rule.count))
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124#[non_exhaustive]
125pub enum ElementCountError {
126 InvalidProgram { message: String },
129 Unsupported {
132 kind: ElementNodeKind,
133 name: String,
134 span: Option<Span>,
135 reason: String,
136 },
137 Cycle {
139 kind: ElementNodeKind,
140 id: usize,
142 },
143}
144
145impl fmt::Display for ElementCountError {
146 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147 match self {
148 Self::InvalidProgram { message } => write!(formatter, "invalid program: {message}"),
149 Self::Unsupported {
150 kind,
151 name,
152 span,
153 reason,
154 } => write!(
155 formatter,
156 "unsupported {kind:?} '{name}'{}: {reason}",
157 span.map_or_else(String::new, |span| format!(" at {span:?}"))
158 ),
159 Self::Cycle { kind, id } => write!(formatter, "cyclic {kind:?} reference at {id}"),
160 }
161 }
162}
163
164impl std::error::Error for ElementCountError {}
165
166impl Program {
167 pub(crate) fn element_count(
173 &self,
174 catalog: &Catalog,
175 ) -> Result<ElementCountReport, ElementCountError> {
176 self.validate()
177 .map_err(|error| ElementCountError::InvalidProgram {
178 message: error.to_string(),
179 })?;
180 crate::rules::validate::validate_wir(self, catalog, false).map_err(|error| {
181 ElementCountError::InvalidProgram {
182 message: error.to_string(),
183 }
184 })?;
185
186 let mut counter = Counter {
187 program: self,
188 catalog,
189 values: HashMap::new(),
190 actions: HashMap::new(),
191 next_node_id: 0,
192 name_slot: false,
193 };
194 let mut rules = Vec::with_capacity(self.rules.len());
195 for rule in self.rules.iter() {
196 rules.push(counter.rule(rule)?);
197 }
198 let total = rules.iter().map(|rule| rule.count).sum();
199 Ok(ElementCountReport { total, rules })
200 }
201}
202
203impl crate::program::Program {
204 pub fn element_count(
206 &self,
207 catalog: &Catalog,
208 ) -> Result<ElementCountReport, ElementCountError> {
209 let storage = self
210 .to_wir()
211 .map_err(|error| ElementCountError::InvalidProgram {
212 message: error.to_string(),
213 })?;
214 storage.element_count(catalog)
215 }
216}
217
218struct Counted {
219 node: ElementCountNode,
220 heroes: usize,
221}
222
223impl Counted {
224 #[allow(clippy::too_many_arguments)]
225 fn finish(
226 kind: ElementNodeKind,
227 id: usize,
228 name: impl Into<String>,
229 span: Option<Span>,
230 base_count: usize,
231 adjustment: isize,
232 children: Vec<ElementCountNode>,
233 heroes: usize,
234 ) -> Self {
235 let children_count: usize = children.iter().map(|child| child.count).sum();
236 let count = (base_count as isize + children_count as isize + adjustment).max(0) as usize;
237 Self {
238 node: ElementCountNode {
239 kind,
240 id,
241 name: name.into(),
242 span,
243 base_count,
244 adjustment,
245 count,
246 children,
247 },
248 heroes,
249 }
250 }
251}
252
253struct Counter<'a> {
254 program: &'a Program,
255 catalog: &'a Catalog,
256 values: HashMap<usize, usize>,
257 actions: HashMap<usize, usize>,
258 next_node_id: usize,
259 name_slot: bool,
261}
262
263impl Counter<'_> {
264 fn next_node_id(&mut self) -> usize {
265 let id = self.next_node_id;
266 self.next_node_id += 1;
267 id
268 }
269
270 fn rule(&mut self, rule: &wir::Rule) -> Result<ElementCountNode, ElementCountError> {
271 let node_id = self.next_node_id();
272 let mut children = Vec::with_capacity(rule.conditions.len() + rule.actions.len());
273 for condition in &rule.conditions {
274 children.push(self.condition(condition.value)?.node);
275 }
276 for (index, action) in rule.actions.iter().enumerate() {
277 let rule_final = index + 1 == rule.actions.len();
278 children.push(self.action(*action, rule_final)?.node);
279 }
280 Ok(Counted::finish(
281 ElementNodeKind::Rule,
282 node_id,
283 &rule.name,
284 rule.span,
285 1,
286 0,
287 children,
288 0,
289 )
290 .node)
291 }
292
293 fn condition(&mut self, id: ValueId) -> Result<Counted, ElementCountError> {
294 let node_id = self.next_node_id();
295 let Some(value) = self.program.values.get(id) else {
296 return Err(ElementCountError::InvalidProgram {
297 message: format!("dangling condition value {}", id.index()),
298 });
299 };
300 let (children, heroes) = match &value.value {
301 Value::Call { name, args } if wir::is_comparison_operator(name) => {
302 let mut children = Vec::with_capacity(args.len());
303 let mut heroes = 0;
304 for argument in args {
305 let counted = self.value(*argument, true)?;
306 heroes += counted.heroes / 2 * 2;
307 children.push(counted.node);
308 }
309 (children, heroes)
310 }
311 _ => {
312 let counted = self.value(id, true)?;
313 (vec![counted.node], counted.heroes)
314 }
315 };
316 Ok(Counted::finish(
317 ElementNodeKind::Condition,
318 node_id,
319 "condition",
320 value.span,
321 1,
322 pair_surcharge(heroes),
323 children,
324 heroes,
325 ))
326 }
327
328 fn action(&mut self, id: ActionId, rule_final: bool) -> Result<Counted, ElementCountError> {
329 let node_id = self.next_node_id();
330 if let Some(&active_id) = self.actions.get(&id.index()) {
331 return Err(ElementCountError::Cycle {
332 kind: ElementNodeKind::Action,
333 id: active_id,
334 });
335 }
336 self.actions.insert(id.index(), node_id);
337 let mut action = self.program.actions.get(id);
338 while let Some(Action::Disabled { action: inner, .. }) = action {
339 action = self.program.actions.get(*inner);
340 }
341 let Some(action) = action else {
342 return Err(ElementCountError::InvalidProgram {
343 message: format!("dangling action {}", id.index()),
344 });
345 };
346 let result = self.action_inner(action, node_id, rule_final);
347 self.actions.remove(&id.index());
348 result
349 }
350
351 fn action_inner(
352 &mut self,
353 action: &Action,
354 node_id: usize,
355 rule_final: bool,
356 ) -> Result<Counted, ElementCountError> {
357 let span = action.span();
358 let mut children = Vec::new();
359 let mut heroes = 0;
360 let mut base = 1;
361 let name;
362 match action {
363 Action::SetGlobalVariable { value, .. }
364 | Action::ModifyGlobalVariable { value, .. } => {
365 name = "variable action";
366 self.push_action_value(&mut children, &mut heroes, *value)?;
367 }
368 Action::SetPlayerVariable { player, value, .. }
369 | Action::ModifyPlayerVariable { player, value, .. } => {
370 name = "player variable action";
371 self.push_action_value(&mut children, &mut heroes, *player)?;
372 self.push_action_value(&mut children, &mut heroes, *value)?;
373 }
374 Action::AssignMember { target, value, .. } => {
375 name = "member assignment";
376 self.push_action_value(&mut children, &mut heroes, *target)?;
377 self.push_action_value(&mut children, &mut heroes, *value)?;
378 }
379 Action::CallSubroutine { .. } => {
380 name = "call subroutine";
381 }
382 Action::If {
383 branches,
384 else_body,
385 ..
386 } => {
387 name = "if";
388 base += branches.len().saturating_sub(1)
392 + usize::from(else_body.is_some())
393 + usize::from(!rule_final);
394 for branch in branches {
395 self.push_action_value(&mut children, &mut heroes, branch.condition)?;
396 for nested in &branch.body {
397 children.push(self.action(*nested, false)?.node);
398 }
399 }
400 if let Some(body) = else_body {
401 for nested in body {
402 children.push(self.action(*nested, false)?.node);
403 }
404 }
405 }
406 Action::While {
407 condition, body, ..
408 } => {
409 name = "while";
410 base += 1;
411 self.push_action_value(&mut children, &mut heroes, *condition)?;
412 for nested in body {
413 children.push(self.action(*nested, false)?.node);
414 }
415 }
416 Action::ForGlobalVariable {
417 start,
418 stop,
419 step,
420 body,
421 ..
422 } => {
423 name = "for global variable";
424 base += 1;
425 for value in [start, stop, step] {
426 self.push_action_value(&mut children, &mut heroes, *value)?;
427 }
428 for nested in body {
429 children.push(self.action(*nested, false)?.node);
430 }
431 }
432 Action::ForPlayerVariable {
433 player,
434 start,
435 stop,
436 step,
437 body,
438 ..
439 } => {
440 name = "for player variable";
441 base += 1;
442 for value in [player, start, stop, step] {
443 self.push_action_value(&mut children, &mut heroes, *value)?;
444 }
445 for nested in body {
446 children.push(self.action(*nested, false)?.node);
447 }
448 }
449 Action::Disabled { .. } => {
450 return Err(ElementCountError::InvalidProgram {
451 message: "a disabled action wraps no enabled action".to_string(),
452 });
453 }
454 Action::Call {
455 name: action_name,
456 args,
457 ..
458 } => {
459 if self.catalog.entry(Kind::Action, action_name).is_none() {
460 return Err(ElementCountError::Unsupported {
461 kind: ElementNodeKind::Action,
462 name: action_name.clone(),
463 span,
464 reason: "the action is not a catalog identity".to_string(),
465 });
466 }
467 name = action_name.as_str();
468 for (index, argument) in args.iter().enumerate() {
469 self.name_slot = variable_slot(
470 self.program,
471 self.catalog,
472 Kind::Action,
473 action_name,
474 args,
475 index,
476 ) || (index == 0
477 && takes_variable(self.catalog, Kind::Action, action_name));
478 self.push_action_value(&mut children, &mut heroes, *argument)?;
479 }
480 base = (base as isize
483 + omitted_default_cost(
484 self.program,
485 self.catalog,
486 Kind::Action,
487 action_name,
488 args,
489 true,
490 ))
491 .max(0) as usize;
492 }
493 }
494 Ok(Counted::finish(
495 ElementNodeKind::Action,
496 node_id,
497 name,
498 span,
499 base,
500 pair_surcharge(heroes),
501 children,
502 heroes,
503 ))
504 }
505
506 fn push_action_value(
507 &mut self,
508 children: &mut Vec<ElementCountNode>,
509 heroes: &mut usize,
510 id: ValueId,
511 ) -> Result<(), ElementCountError> {
512 let counted = self.value(id, true)?;
513 *heroes += counted.heroes / 2 * 2;
515 children.push(counted.node);
516 Ok(())
517 }
518
519 fn value(&mut self, id: ValueId, top_level: bool) -> Result<Counted, ElementCountError> {
520 let name_slot = std::mem::take(&mut self.name_slot);
521 let node_id = self.next_node_id();
522 if let Some(&active_id) = self.values.get(&id.index()) {
523 return Err(ElementCountError::Cycle {
524 kind: ElementNodeKind::Value,
525 id: active_id,
526 });
527 }
528 self.values.insert(id.index(), node_id);
529 let Some(value) = self.program.values.get(id) else {
530 return Err(ElementCountError::InvalidProgram {
531 message: format!("dangling value {}", id.index()),
532 });
533 };
534 let span = value.span;
535 let result = match &value.value {
536 Value::Number { .. } => self.value_node(node_id, "number", span, 2, vec![], 0),
537 Value::String(_) => self.value_node(node_id, "string", span, 1, vec![], 0),
538 Value::LocalizedString(_) => {
539 self.value_node(node_id, "localized string", span, 2, vec![], 0)
540 }
541 Value::Bool(_) => self.value_node(node_id, "boolean", span, 1, vec![], 0),
542 Value::Null => self.value_node(node_id, "null", span, 1, vec![], 0),
543 Value::Array(elements) => {
544 self.value_children(node_id, "array", span, 2, elements, None)
545 }
546 Value::Vector { x, y, z } => {
547 self.value_children(node_id, "vector", span, 1, &[*x, *y, *z], None)
548 }
549 Value::Enum { value_type, .. } => {
550 let heroes = usize::from(value_type == "Hero");
551 let base = literal_enum_cost(value_type);
552 self.value_node(node_id, value_type, span, base, vec![], heroes)
553 }
554 Value::GlobalVariable(_) => {
555 let base = if name_slot { 1 } else { 2 };
556 self.value_node(node_id, "global variable", span, base, vec![], 0)
557 }
558 Value::PlayerVariable { player, .. } => {
559 let base = if name_slot { 0 } else { 2 };
561 self.value_children(node_id, "player variable", span, base, &[*player], None)
562 }
563 Value::Subroutine(_) => self.value_node(node_id, "subroutine", span, 1, vec![], 0),
564 Value::EventPlayer => self.value_node(node_id, "event player", span, 1, vec![], 0),
565 Value::Call { name, args } => {
566 if name == crate::wir::AMBIGUOUS_ENUM_CALL
567 && crate::wir::ambiguous_enum_parts(self.program, id).is_some()
568 {
569 self.value_node(node_id, "ambiguous enum", span, 1, vec![], 0)
570 } else {
571 if name != "memberAccess"
572 && self.catalog.entry(Kind::Value, name).is_none()
573 && self.catalog.entry(Kind::Operator, name).is_none()
574 && !crate::wir::is_canonical_helper(name)
575 {
576 return Err(ElementCountError::Unsupported {
577 kind: ElementNodeKind::Value,
578 name: name.clone(),
579 span,
580 reason: "the value is not a catalog identity".to_string(),
581 });
582 }
583 let child_ids: Vec<ValueId> = if name == "memberAccess" {
584 args.first()
585 .copied()
586 .into_iter()
587 .chain(args.iter().copied().skip(2))
588 .collect()
589 } else {
590 args.clone()
591 };
592 let base = if wir::is_comparison_operator(name) {
593 2
595 } else if name == "array" || name == "evaluateOnce" {
596 2
597 } else {
598 1
599 };
600 let omitted = omitted_default_cost(
601 self.program,
602 self.catalog,
603 Kind::Value,
604 name,
605 args,
606 false,
607 );
608 let mut counted = self.value_children(
609 node_id,
610 name,
611 span,
612 (base as isize + omitted) as usize,
613 &child_ids,
614 Some((Kind::Value, name)),
615 )?;
616 let setting = workshop_setting_adjustment(name);
617 counted.node.adjustment += setting;
618 counted.node.count = (counted.node.count as isize + setting).max(0) as usize;
619 Ok(counted)
620 }
621 }
622 }?;
623 self.values.remove(&id.index());
624 let mut result = result;
625 if top_level {
626 result.node.adjustment -= 1;
627 result.node.count = (result.node.count as isize - 1).max(0) as usize;
628 }
629 Ok(result)
630 }
631
632 fn value_node(
633 &self,
634 id: usize,
635 name: impl Into<String>,
636 span: Option<Span>,
637 base: usize,
638 children: Vec<ElementCountNode>,
639 heroes: usize,
640 ) -> Result<Counted, ElementCountError> {
641 Ok(Counted::finish(
642 ElementNodeKind::Value,
643 id,
644 name,
645 span,
646 base,
647 0,
648 children,
649 heroes,
650 ))
651 }
652
653 fn value_children(
654 &mut self,
655 id: usize,
656 name: impl Into<String>,
657 span: Option<Span>,
658 base: usize,
659 ids: &[ValueId],
660 owner: Option<(Kind, &str)>,
661 ) -> Result<Counted, ElementCountError> {
662 let mut children = Vec::with_capacity(ids.len());
663 let mut heroes = 0;
664 for (index, child) in ids.iter().enumerate() {
665 self.name_slot = owner.is_some_and(|(kind, name)| {
666 variable_slot(self.program, self.catalog, kind, name, ids, index)
667 });
668 let counted = self.value(*child, false)?;
669 heroes += counted.heroes;
670 children.push(counted.node);
671 }
672 self.value_node(id, name, span, base, children, heroes)
673 }
674}
675
676fn pair_surcharge(heroes: usize) -> isize {
677 (heroes / 2) as isize
678}
679
680fn omitted_default_cost(
682 program: &wir::Program,
683 catalog: &Catalog,
684 kind: Kind,
685 name: &str,
686 args: &[ValueId],
687 direct: bool,
688) -> isize {
689 let Some(entry) = catalog.entry(kind, name) else {
690 return 0;
691 };
692 let given = args.len() + usize::from(entry.wir_args_folded(program, args));
695 (given..entry.param_count())
696 .filter_map(|index| entry.param_default(index))
697 .map(|default| {
698 let cost = if default.parse::<f64>().is_ok() {
699 2
700 } else {
701 default.split('.').next().map_or(1, literal_enum_cost) as isize
702 };
703 cost - isize::from(direct)
704 })
705 .sum()
706}
707
708fn literal_enum_cost(value_type: &str) -> usize {
711 if matches!(value_type, "Team" | "Hero" | "Color" | "Button" | "Map") {
712 2
713 } else {
714 1
715 }
716}
717
718fn variable_slot(
721 program: &wir::Program,
722 catalog: &Catalog,
723 kind: Kind,
724 name: &str,
725 args: &[ValueId],
726 index: usize,
727) -> bool {
728 catalog.entry(kind, name).is_some_and(|entry| {
729 let index = entry.wir_arg_param_index(program, args, index);
730 entry.param_type(index) == Some("Variable")
731 || entry
732 .params()
733 .get(index)
734 .is_some_and(|param| param == "Variable")
735 })
736}
737
738fn takes_variable(catalog: &Catalog, kind: Kind, name: &str) -> bool {
741 catalog.entry(kind, name).is_some_and(|entry| {
742 (0..entry.param_count()).any(|index| {
743 entry.param_type(index) == Some("Variable")
744 || entry
745 .params()
746 .get(index)
747 .is_some_and(|param| param == "Variable")
748 })
749 })
750}
751
752fn workshop_setting_adjustment(name: &str) -> isize {
755 match name {
756 "workshopSettingInteger"
757 | "workshopSettingFloat"
758 | "createWorkshopSettingInt"
759 | "createWorkshopSettingFloat" => -3,
760 "workshopSettingCombo" | "createWorkshopSettingEnum" => -2,
761 _ => 0,
762 }
763}