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