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