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