Skip to main content

workshop_rs/
element_count.rs

1//! Canonical Workshop element-count analysis.
2//!
3//! The calculator operates on validated WIR, not source-language syntax or
4//! emitted text. Its rules are the documented Workshop.codes model: rules,
5//! actions, conditions, and ordinary values cost one element; arrays and
6//! evaluate-once values cost two; localized strings cost two; direct action or
7//! condition arguments are reduced by one; and every pair of hero literals in
8//! those arguments adds one. Custom game settings and rule parameters cost
9//! zero.
10
11use 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/// The WIR node category represented in an element-count report.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ElementNodeKind {
21    Rule,
22    Condition,
23    Action,
24    Value,
25}
26
27/// One node's contribution, including its nested WIR provenance.
28#[derive(Debug, Clone)]
29pub struct ElementCountNode {
30    pub kind: ElementNodeKind,
31    /// The arena index of the represented WIR node. Rule nodes use the rule
32    /// arena, action nodes use the action arena, and value nodes use the value
33    /// arena. Synthetic condition nodes use the condition's value index.
34    pub id: usize,
35    pub name: String,
36    pub span: Option<Span>,
37    /// The contribution before children and adjustment rules.
38    pub base_count: usize,
39    /// The local adjustment from the canonical model, such as a top-level
40    /// argument reduction or hero-pair surcharge.
41    pub adjustment: isize,
42    /// The complete contribution of this node and its children.
43    pub count: usize,
44    pub children: Vec<ElementCountNode>,
45}
46
47/// A structured element-count report for one canonical Workshop program.
48#[derive(Debug, Clone)]
49pub struct ElementCountReport {
50    pub total: usize,
51    pub rules: Vec<ElementCountNode>,
52}
53
54impl ElementCountReport {
55    /// Return the per-rule total in source/WIR order.
56    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/// A construct for which an exact canonical element count cannot be produced.
64#[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    /// Count the canonical Workshop target represented by this WIR program.
104    ///
105    /// The catalog is used to reject unknown action/value identities before a
106    /// report is produced. Presentation-only `Debug` and `Print` WIR nodes
107    /// are intentionally rejected because their emitted HUD expansion is not
108    /// yet represented as canonical WIR actions.
109    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}