1use std::collections::HashSet;
12use std::fmt;
13
14use crate::catalog::{Catalog, Kind};
15use crate::source::Span;
16use crate::wir::{self, Action, ActionId, Program, Value, ValueId};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ElementNodeKind {
21 Rule,
22 Condition,
23 Action,
24 Value,
25}
26
27#[derive(Debug, Clone)]
29pub struct ElementCountNode {
30 pub kind: ElementNodeKind,
31 pub id: usize,
35 pub name: String,
36 pub span: Option<Span>,
37 pub base_count: usize,
39 pub adjustment: isize,
42 pub count: usize,
44 pub children: Vec<ElementCountNode>,
45}
46
47#[derive(Debug, Clone)]
49pub struct ElementCountReport {
50 pub total: usize,
51 pub rules: Vec<ElementCountNode>,
52}
53
54impl ElementCountReport {
55 pub fn rule_counts(&self) -> impl Iterator<Item = (&str, usize)> {
57 self.rules
58 .iter()
59 .map(|rule| (rule.name.as_str(), rule.count))
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum ElementCountError {
66 InvalidProgram {
67 message: String,
68 },
69 Unsupported {
70 kind: ElementNodeKind,
71 name: String,
72 span: Option<Span>,
73 reason: String,
74 },
75 Cycle {
76 kind: ElementNodeKind,
77 id: usize,
78 },
79}
80
81impl fmt::Display for ElementCountError {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::InvalidProgram { message } => write!(formatter, "invalid WIR: {message}"),
85 Self::Unsupported {
86 kind,
87 name,
88 span,
89 reason,
90 } => write!(
91 formatter,
92 "unsupported {kind:?} '{name}'{}: {reason}",
93 span.map_or_else(String::new, |span| format!(" at {span:?}"))
94 ),
95 Self::Cycle { kind, id } => write!(formatter, "cyclic {kind:?} reference at {id}"),
96 }
97 }
98}
99
100impl std::error::Error for ElementCountError {}
101
102impl Program {
103 pub fn element_count(
110 &self,
111 catalog: &Catalog,
112 ) -> Result<ElementCountReport, ElementCountError> {
113 self.validate()
114 .map_err(|error| ElementCountError::InvalidProgram {
115 message: error.to_string(),
116 })?;
117 crate::validate::validate_canonical_ids(self, catalog).map_err(|error| {
118 ElementCountError::InvalidProgram {
119 message: error.to_string(),
120 }
121 })?;
122
123 let mut counter = Counter {
124 program: self,
125 catalog,
126 values: HashSet::new(),
127 actions: HashSet::new(),
128 };
129 let mut rules = Vec::with_capacity(self.rules.len());
130 for (index, rule) in self.rules.iter().enumerate() {
131 rules.push(counter.rule(index, rule)?);
132 }
133 let total = rules.iter().map(|rule| rule.count).sum();
134 Ok(ElementCountReport { total, rules })
135 }
136}
137
138struct Counted {
139 node: ElementCountNode,
140 heroes: usize,
141}
142
143impl Counted {
144 #[allow(clippy::too_many_arguments)]
145 fn finish(
146 kind: ElementNodeKind,
147 id: usize,
148 name: impl Into<String>,
149 span: Option<Span>,
150 base_count: usize,
151 adjustment: isize,
152 children: Vec<ElementCountNode>,
153 heroes: usize,
154 ) -> Self {
155 let children_count: usize = children.iter().map(|child| child.count).sum();
156 let count = (base_count as isize + children_count as isize + adjustment).max(0) as usize;
157 Self {
158 node: ElementCountNode {
159 kind,
160 id,
161 name: name.into(),
162 span,
163 base_count,
164 adjustment,
165 count,
166 children,
167 },
168 heroes,
169 }
170 }
171}
172
173struct Counter<'a> {
174 program: &'a Program,
175 catalog: &'a Catalog,
176 values: HashSet<usize>,
177 actions: HashSet<usize>,
178}
179
180impl Counter<'_> {
181 fn rule(
182 &mut self,
183 index: usize,
184 rule: &wir::Rule,
185 ) -> Result<ElementCountNode, ElementCountError> {
186 let mut children = Vec::with_capacity(rule.conditions.len() + rule.actions.len());
187 for condition in &rule.conditions {
188 children.push(self.condition(*condition)?.node);
189 }
190 for action in &rule.actions {
191 children.push(self.action(*action)?.node);
192 }
193 Ok(Counted::finish(
194 ElementNodeKind::Rule,
195 index,
196 &rule.name,
197 rule.span,
198 1,
199 0,
200 children,
201 0,
202 )
203 .node)
204 }
205
206 fn condition(&mut self, id: ValueId) -> Result<Counted, ElementCountError> {
207 let Some(value) = self.program.values.get(id) else {
208 return Err(ElementCountError::InvalidProgram {
209 message: format!("dangling condition value {}", id.index()),
210 });
211 };
212 let (children, heroes) = match &value.value {
213 Value::Call { name, args } if is_comparison(name) => {
214 let mut children = Vec::with_capacity(args.len());
215 let mut heroes = 0;
216 for argument in args {
217 let counted = self.value(*argument, true)?;
218 heroes += counted.heroes;
219 children.push(counted.node);
220 }
221 (children, heroes)
222 }
223 _ => {
224 let counted = self.value(id, true)?;
225 (vec![counted.node], counted.heroes)
226 }
227 };
228 Ok(Counted::finish(
229 ElementNodeKind::Condition,
230 id.index(),
231 "condition",
232 value.span,
233 1,
234 pair_surcharge(heroes),
235 children,
236 heroes,
237 ))
238 }
239
240 fn action(&mut self, id: ActionId) -> Result<Counted, ElementCountError> {
241 if !self.actions.insert(id.index()) {
242 return Err(ElementCountError::Cycle {
243 kind: ElementNodeKind::Action,
244 id: id.index(),
245 });
246 }
247 let Some(action) = self.program.actions.get(id) else {
248 return Err(ElementCountError::InvalidProgram {
249 message: format!("dangling action {}", id.index()),
250 });
251 };
252 let result = self.action_inner(id, action);
253 self.actions.remove(&id.index());
254 result
255 }
256
257 fn action_inner(
258 &mut self,
259 id: ActionId,
260 action: &Action,
261 ) -> Result<Counted, ElementCountError> {
262 let span = action.span();
263 let mut children = Vec::new();
264 let mut heroes = 0;
265 let name;
266 match action {
267 Action::SetGlobalVariable { value, .. }
268 | Action::ModifyGlobalVariable { value, .. } => {
269 name = "variable action";
270 self.push_action_value(&mut children, &mut heroes, *value)?;
271 }
272 Action::SetPlayerVariable { player, value, .. }
273 | Action::ModifyPlayerVariable { player, value, .. } => {
274 name = "player variable action";
275 self.push_action_value(&mut children, &mut heroes, *player)?;
276 self.push_action_value(&mut children, &mut heroes, *value)?;
277 }
278 Action::AssignMember { target, value, .. } => {
279 name = "member assignment";
280 self.push_action_value(&mut children, &mut heroes, *target)?;
281 self.push_action_value(&mut children, &mut heroes, *value)?;
282 }
283 Action::CallSubroutine { .. } => {
284 name = "call subroutine";
285 }
286 Action::If {
287 branches,
288 else_body,
289 ..
290 } => {
291 name = "if";
292 for branch in branches {
293 self.push_action_value(&mut children, &mut heroes, branch.condition)?;
294 for nested in &branch.body {
295 children.push(self.action(*nested)?.node);
296 }
297 }
298 if let Some(body) = else_body {
299 for nested in body {
300 children.push(self.action(*nested)?.node);
301 }
302 }
303 }
304 Action::While {
305 condition, body, ..
306 } => {
307 name = "while";
308 self.push_action_value(&mut children, &mut heroes, *condition)?;
309 for nested in body {
310 children.push(self.action(*nested)?.node);
311 }
312 }
313 Action::ForGlobalVariable {
314 start,
315 stop,
316 step,
317 body,
318 ..
319 } => {
320 name = "for global variable";
321 for value in [start, stop, step] {
322 self.push_action_value(&mut children, &mut heroes, *value)?;
323 }
324 for nested in body {
325 children.push(self.action(*nested)?.node);
326 }
327 }
328 Action::ForPlayerVariable {
329 player,
330 start,
331 stop,
332 step,
333 body,
334 ..
335 } => {
336 name = "for player variable";
337 for value in [player, start, stop, step] {
338 self.push_action_value(&mut children, &mut heroes, *value)?;
339 }
340 for nested in body {
341 children.push(self.action(*nested)?.node);
342 }
343 }
344 Action::Debug { .. } => {
345 return Err(ElementCountError::Unsupported {
346 kind: ElementNodeKind::Action,
347 name: "debug".to_string(),
348 span,
349 reason: "the emitter expands Debug into a HUD action; count the canonical HUD action instead".to_string(),
350 });
351 }
352 Action::Print { .. } => {
353 return Err(ElementCountError::Unsupported {
354 kind: ElementNodeKind::Action,
355 name: "print".to_string(),
356 span,
357 reason: "the emitter expands Print into a HUD action; count the canonical HUD action instead".to_string(),
358 });
359 }
360 Action::Call {
361 name: action_name,
362 args,
363 ..
364 } => {
365 if self.catalog.entry(Kind::Action, action_name).is_none() {
366 return Err(ElementCountError::Unsupported {
367 kind: ElementNodeKind::Action,
368 name: action_name.clone(),
369 span,
370 reason: "the action is not a catalog identity".to_string(),
371 });
372 }
373 name = action_name.as_str();
374 for argument in args {
375 self.push_action_value(&mut children, &mut heroes, *argument)?;
376 }
377 }
378 }
379 Ok(Counted::finish(
380 ElementNodeKind::Action,
381 id.index(),
382 name,
383 span,
384 1,
385 pair_surcharge(heroes),
386 children,
387 heroes,
388 ))
389 }
390
391 fn push_action_value(
392 &mut self,
393 children: &mut Vec<ElementCountNode>,
394 heroes: &mut usize,
395 id: ValueId,
396 ) -> Result<(), ElementCountError> {
397 let counted = self.value(id, true)?;
398 *heroes += counted.heroes;
399 children.push(counted.node);
400 Ok(())
401 }
402
403 fn value(&mut self, id: ValueId, top_level: bool) -> Result<Counted, ElementCountError> {
404 if !self.values.insert(id.index()) {
405 return Err(ElementCountError::Cycle {
406 kind: ElementNodeKind::Value,
407 id: id.index(),
408 });
409 }
410 let Some(value) = self.program.values.get(id) else {
411 return Err(ElementCountError::InvalidProgram {
412 message: format!("dangling value {}", id.index()),
413 });
414 };
415 let span = value.span;
416 let result = match &value.value {
417 Value::Number { .. } => self.value_node(id, "number", span, 1, vec![], 0),
418 Value::String(_) => self.value_node(id, "string", span, 1, vec![], 0),
419 Value::LocalizedString(_) => {
420 self.value_node(id, "localized string", span, 2, vec![], 0)
421 }
422 Value::Bool(_) => self.value_node(id, "boolean", span, 1, vec![], 0),
423 Value::Null => self.value_node(id, "null", span, 1, vec![], 0),
424 Value::Array(elements) => self.value_children(id, "array", span, 2, elements),
425 Value::Vector { x, y, z } => self.value_children(id, "vector", span, 1, &[*x, *y, *z]),
426 Value::Enum { value_type, .. } => {
427 let heroes = usize::from(value_type == "Hero");
428 self.value_node(id, value_type, span, 1, vec![], heroes)
429 }
430 Value::GlobalVariable(_) => self.value_node(id, "global variable", span, 1, vec![], 0),
431 Value::PlayerVariable { player, .. } => {
432 self.value_children(id, "player variable", span, 1, &[*player])
433 }
434 Value::Subroutine(_) => self.value_node(id, "subroutine", span, 1, vec![], 0),
435 Value::EventPlayer => self.value_node(id, "event player", span, 1, vec![], 0),
436 Value::Call { name, args } => {
437 if name != "memberAccess"
438 && self.catalog.entry(Kind::Value, name).is_none()
439 && self.catalog.entry(Kind::Operator, name).is_none()
440 && !is_canonical_helper(name)
441 {
442 return Err(ElementCountError::Unsupported {
443 kind: ElementNodeKind::Value,
444 name: name.clone(),
445 span,
446 reason: "the value is not a catalog identity".to_string(),
447 });
448 }
449 let child_ids: Vec<ValueId> = if name == "memberAccess" {
450 args.first()
451 .copied()
452 .into_iter()
453 .chain(args.iter().copied().skip(2))
454 .collect()
455 } else {
456 args.clone()
457 };
458 let base = if name == "array"
459 || name == "evalOnce"
460 || name.starts_with("workshopSetting")
461 || name.starts_with("createWorkshopSetting")
462 {
463 2
464 } else {
465 1
466 };
467 self.value_children(id, name, span, base, &child_ids)
468 }
469 }?;
470 self.values.remove(&id.index());
471 let mut result = result;
472 if top_level {
473 result.node.adjustment -= 1;
474 result.node.count = (result.node.count as isize - 1).max(0) as usize;
475 }
476 Ok(result)
477 }
478
479 fn value_node(
480 &self,
481 id: ValueId,
482 name: impl Into<String>,
483 span: Option<Span>,
484 base: usize,
485 children: Vec<ElementCountNode>,
486 heroes: usize,
487 ) -> Result<Counted, ElementCountError> {
488 Ok(Counted::finish(
489 ElementNodeKind::Value,
490 id.index(),
491 name,
492 span,
493 base,
494 0,
495 children,
496 heroes,
497 ))
498 }
499
500 fn value_children(
501 &mut self,
502 id: ValueId,
503 name: impl Into<String>,
504 span: Option<Span>,
505 base: usize,
506 ids: &[ValueId],
507 ) -> Result<Counted, ElementCountError> {
508 let mut children = Vec::with_capacity(ids.len());
509 let mut heroes = 0;
510 for child in ids {
511 let counted = self.value(*child, false)?;
512 heroes += counted.heroes;
513 children.push(counted.node);
514 }
515 self.value_node(id, name, span, base, children, heroes)
516 }
517}
518
519fn pair_surcharge(heroes: usize) -> isize {
520 (heroes / 2) as isize
521}
522
523fn is_comparison(name: &str) -> bool {
524 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
525}
526
527fn is_canonical_helper(name: &str) -> bool {
528 matches!(
529 name,
530 "memberAccess"
531 | "+"
532 | "-"
533 | "*"
534 | "/"
535 | "%"
536 | "add"
537 | "subtract"
538 | "multiply"
539 | "divide"
540 | "modulo"
541 | "min"
542 | "max"
543 | "raiseToPower"
544 | "appendToArray"
545 | "removeFromArray"
546 | "removeFromArrayByIndex"
547 )
548}