1use std::collections::HashMap;
12use std::fmt;
13
14use crate::catalog::{Catalog, Kind};
15use crate::core::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,
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)]
51pub struct ElementCountReport {
52 pub total: usize,
54 pub rules: Vec<ElementCountNode>,
56}
57
58impl ElementCountReport {
59 pub fn rule_counts(&self) -> impl Iterator<Item = (&str, usize)> {
61 self.rules
62 .iter()
63 .map(|rule| (rule.name.as_str(), rule.count))
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ElementCountError {
70 InvalidProgram { message: String },
73 Unsupported {
76 kind: ElementNodeKind,
77 name: String,
78 span: Option<Span>,
79 reason: String,
80 },
81 Cycle {
83 kind: ElementNodeKind,
84 id: usize,
86 },
87}
88
89impl fmt::Display for ElementCountError {
90 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::InvalidProgram { message } => write!(formatter, "invalid program: {message}"),
93 Self::Unsupported {
94 kind,
95 name,
96 span,
97 reason,
98 } => write!(
99 formatter,
100 "unsupported {kind:?} '{name}'{}: {reason}",
101 span.map_or_else(String::new, |span| format!(" at {span:?}"))
102 ),
103 Self::Cycle { kind, id } => write!(formatter, "cyclic {kind:?} reference at {id}"),
104 }
105 }
106}
107
108impl std::error::Error for ElementCountError {}
109
110impl Program {
111 pub(crate) fn element_count(
117 &self,
118 catalog: &Catalog,
119 ) -> Result<ElementCountReport, ElementCountError> {
120 self.validate()
121 .map_err(|error| ElementCountError::InvalidProgram {
122 message: error.to_string(),
123 })?;
124 crate::rules::validate::validate_wir(self, catalog).map_err(|error| {
125 ElementCountError::InvalidProgram {
126 message: error.to_string(),
127 }
128 })?;
129
130 let mut counter = Counter {
131 program: self,
132 catalog,
133 values: HashMap::new(),
134 actions: HashMap::new(),
135 next_node_id: 0,
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}
203
204impl Counter<'_> {
205 fn next_node_id(&mut self) -> usize {
206 let id = self.next_node_id;
207 self.next_node_id += 1;
208 id
209 }
210
211 fn rule(&mut self, rule: &wir::Rule) -> Result<ElementCountNode, ElementCountError> {
212 let node_id = self.next_node_id();
213 let mut children = Vec::with_capacity(rule.conditions.len() + rule.actions.len());
214 for condition in &rule.conditions {
215 children.push(self.condition(*condition)?.node);
216 }
217 for action in &rule.actions {
218 children.push(self.action(*action)?.node);
219 }
220 Ok(Counted::finish(
221 ElementNodeKind::Rule,
222 node_id,
223 &rule.name,
224 rule.span,
225 1,
226 0,
227 children,
228 0,
229 )
230 .node)
231 }
232
233 fn condition(&mut self, id: ValueId) -> Result<Counted, ElementCountError> {
234 let node_id = self.next_node_id();
235 let Some(value) = self.program.values.get(id) else {
236 return Err(ElementCountError::InvalidProgram {
237 message: format!("dangling condition value {}", id.index()),
238 });
239 };
240 let (children, heroes) = match &value.value {
241 Value::Call { name, args } if is_comparison(name) => {
242 let mut children = Vec::with_capacity(args.len());
243 let mut heroes = 0;
244 for argument in args {
245 let counted = self.value(*argument, true)?;
246 heroes += counted.heroes;
247 children.push(counted.node);
248 }
249 (children, heroes)
250 }
251 _ => {
252 let counted = self.value(id, true)?;
253 (vec![counted.node], counted.heroes)
254 }
255 };
256 Ok(Counted::finish(
257 ElementNodeKind::Condition,
258 node_id,
259 "condition",
260 value.span,
261 1,
262 pair_surcharge(heroes),
263 children,
264 heroes,
265 ))
266 }
267
268 fn action(&mut self, id: ActionId) -> Result<Counted, ElementCountError> {
269 let node_id = self.next_node_id();
270 if let Some(&active_id) = self.actions.get(&id.index()) {
271 return Err(ElementCountError::Cycle {
272 kind: ElementNodeKind::Action,
273 id: active_id,
274 });
275 }
276 self.actions.insert(id.index(), node_id);
277 let Some(action) = self.program.actions.get(id) else {
278 return Err(ElementCountError::InvalidProgram {
279 message: format!("dangling action {}", id.index()),
280 });
281 };
282 let result = self.action_inner(action, node_id);
283 self.actions.remove(&id.index());
284 result
285 }
286
287 fn action_inner(
288 &mut self,
289 action: &Action,
290 node_id: usize,
291 ) -> Result<Counted, ElementCountError> {
292 let span = action.span();
293 let mut children = Vec::new();
294 let mut heroes = 0;
295 let name;
296 match action {
297 Action::SetGlobalVariable { value, .. }
298 | Action::ModifyGlobalVariable { value, .. } => {
299 name = "variable action";
300 self.push_action_value(&mut children, &mut heroes, *value)?;
301 }
302 Action::SetPlayerVariable { player, value, .. }
303 | Action::ModifyPlayerVariable { player, value, .. } => {
304 name = "player variable action";
305 self.push_action_value(&mut children, &mut heroes, *player)?;
306 self.push_action_value(&mut children, &mut heroes, *value)?;
307 }
308 Action::AssignMember { target, value, .. } => {
309 name = "member assignment";
310 self.push_action_value(&mut children, &mut heroes, *target)?;
311 self.push_action_value(&mut children, &mut heroes, *value)?;
312 }
313 Action::CallSubroutine { .. } => {
314 name = "call subroutine";
315 }
316 Action::If {
317 branches,
318 else_body,
319 ..
320 } => {
321 name = "if";
322 for branch in branches {
323 self.push_action_value(&mut children, &mut heroes, branch.condition)?;
324 for nested in &branch.body {
325 children.push(self.action(*nested)?.node);
326 }
327 }
328 if let Some(body) = else_body {
329 for nested in body {
330 children.push(self.action(*nested)?.node);
331 }
332 }
333 }
334 Action::While {
335 condition, body, ..
336 } => {
337 name = "while";
338 self.push_action_value(&mut children, &mut heroes, *condition)?;
339 for nested in body {
340 children.push(self.action(*nested)?.node);
341 }
342 }
343 Action::ForGlobalVariable {
344 start,
345 stop,
346 step,
347 body,
348 ..
349 } => {
350 name = "for global variable";
351 for value in [start, stop, step] {
352 self.push_action_value(&mut children, &mut heroes, *value)?;
353 }
354 for nested in body {
355 children.push(self.action(*nested)?.node);
356 }
357 }
358 Action::ForPlayerVariable {
359 player,
360 start,
361 stop,
362 step,
363 body,
364 ..
365 } => {
366 name = "for player variable";
367 for value in [player, start, stop, step] {
368 self.push_action_value(&mut children, &mut heroes, *value)?;
369 }
370 for nested in body {
371 children.push(self.action(*nested)?.node);
372 }
373 }
374 Action::Call {
375 name: action_name,
376 args,
377 ..
378 } => {
379 if self.catalog.entry(Kind::Action, action_name).is_none() {
380 return Err(ElementCountError::Unsupported {
381 kind: ElementNodeKind::Action,
382 name: action_name.clone(),
383 span,
384 reason: "the action is not a catalog identity".to_string(),
385 });
386 }
387 name = action_name.as_str();
388 for argument in args {
389 self.push_action_value(&mut children, &mut heroes, *argument)?;
390 }
391 }
392 }
393 Ok(Counted::finish(
394 ElementNodeKind::Action,
395 node_id,
396 name,
397 span,
398 1,
399 pair_surcharge(heroes),
400 children,
401 heroes,
402 ))
403 }
404
405 fn push_action_value(
406 &mut self,
407 children: &mut Vec<ElementCountNode>,
408 heroes: &mut usize,
409 id: ValueId,
410 ) -> Result<(), ElementCountError> {
411 let counted = self.value(id, true)?;
412 *heroes += counted.heroes;
413 children.push(counted.node);
414 Ok(())
415 }
416
417 fn value(&mut self, id: ValueId, top_level: bool) -> Result<Counted, ElementCountError> {
418 let node_id = self.next_node_id();
419 if let Some(&active_id) = self.values.get(&id.index()) {
420 return Err(ElementCountError::Cycle {
421 kind: ElementNodeKind::Value,
422 id: active_id,
423 });
424 }
425 self.values.insert(id.index(), node_id);
426 let Some(value) = self.program.values.get(id) else {
427 return Err(ElementCountError::InvalidProgram {
428 message: format!("dangling value {}", id.index()),
429 });
430 };
431 let span = value.span;
432 let result = match &value.value {
433 Value::Number { .. } => self.value_node(node_id, "number", span, 1, vec![], 0),
434 Value::String(_) => self.value_node(node_id, "string", span, 1, vec![], 0),
435 Value::LocalizedString(_) => {
436 self.value_node(node_id, "localized string", span, 2, vec![], 0)
437 }
438 Value::Bool(_) => self.value_node(node_id, "boolean", span, 1, vec![], 0),
439 Value::Null => self.value_node(node_id, "null", span, 1, vec![], 0),
440 Value::Array(elements) => self.value_children(node_id, "array", span, 2, elements),
441 Value::Vector { x, y, z } => {
442 self.value_children(node_id, "vector", span, 1, &[*x, *y, *z])
443 }
444 Value::Enum { value_type, .. } => {
445 let heroes = usize::from(value_type == "Hero");
446 self.value_node(node_id, value_type, span, 1, vec![], heroes)
447 }
448 Value::GlobalVariable(_) => {
449 self.value_node(node_id, "global variable", span, 1, vec![], 0)
450 }
451 Value::PlayerVariable { player, .. } => {
452 self.value_children(node_id, "player variable", span, 1, &[*player])
453 }
454 Value::Subroutine(_) => self.value_node(node_id, "subroutine", span, 1, vec![], 0),
455 Value::EventPlayer => self.value_node(node_id, "event player", span, 1, vec![], 0),
456 Value::Call { name, args } => {
457 if name == crate::wir::AMBIGUOUS_ENUM_CALL
458 && crate::wir::ambiguous_enum_parts(self.program, id).is_some()
459 {
460 self.value_node(node_id, "ambiguous enum", span, 1, vec![], 0)
461 } else {
462 if name != "memberAccess"
463 && self.catalog.entry(Kind::Value, name).is_none()
464 && self.catalog.entry(Kind::Operator, name).is_none()
465 && !is_canonical_helper(name)
466 {
467 return Err(ElementCountError::Unsupported {
468 kind: ElementNodeKind::Value,
469 name: name.clone(),
470 span,
471 reason: "the value is not a catalog identity".to_string(),
472 });
473 }
474 let child_ids: Vec<ValueId> = if name == "memberAccess" {
475 args.first()
476 .copied()
477 .into_iter()
478 .chain(args.iter().copied().skip(2))
479 .collect()
480 } else {
481 args.clone()
482 };
483 let base = if name == "array"
484 || name == "evalOnce"
485 || name.starts_with("workshopSetting")
486 || name.starts_with("createWorkshopSetting")
487 {
488 2
489 } else {
490 1
491 };
492 self.value_children(node_id, name, span, base, &child_ids)
493 }
494 }
495 }?;
496 self.values.remove(&id.index());
497 let mut result = result;
498 if top_level {
499 result.node.adjustment -= 1;
500 result.node.count = (result.node.count as isize - 1).max(0) as usize;
501 }
502 Ok(result)
503 }
504
505 fn value_node(
506 &self,
507 id: usize,
508 name: impl Into<String>,
509 span: Option<Span>,
510 base: usize,
511 children: Vec<ElementCountNode>,
512 heroes: usize,
513 ) -> Result<Counted, ElementCountError> {
514 Ok(Counted::finish(
515 ElementNodeKind::Value,
516 id,
517 name,
518 span,
519 base,
520 0,
521 children,
522 heroes,
523 ))
524 }
525
526 fn value_children(
527 &mut self,
528 id: usize,
529 name: impl Into<String>,
530 span: Option<Span>,
531 base: usize,
532 ids: &[ValueId],
533 ) -> Result<Counted, ElementCountError> {
534 let mut children = Vec::with_capacity(ids.len());
535 let mut heroes = 0;
536 for child in ids {
537 let counted = self.value(*child, false)?;
538 heroes += counted.heroes;
539 children.push(counted.node);
540 }
541 self.value_node(id, name, span, base, children, heroes)
542 }
543}
544
545fn pair_surcharge(heroes: usize) -> isize {
546 (heroes / 2) as isize
547}
548
549fn is_comparison(name: &str) -> bool {
550 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
551}
552
553fn is_canonical_helper(name: &str) -> bool {
554 matches!(
555 name,
556 "memberAccess"
557 | "+"
558 | "-"
559 | "*"
560 | "/"
561 | "%"
562 | "add"
563 | "subtract"
564 | "multiply"
565 | "divide"
566 | "modulo"
567 | "min"
568 | "max"
569 | "raiseToPower"
570 | "appendToArray"
571 | "removeFromArray"
572 | "removeFromArrayByValue"
573 | "removeFromArrayByIndex"
574 )
575}