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