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. Native display actions are represented by their
107    /// canonical catalog-backed action calls.
108    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}