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