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