Skip to main content

workshop_rs/analysis/
element_count.rs

1//! Canonical Workshop element-count analysis.
2//!
3//! The calculator operates on the canonical public program, 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::core::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 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    /// 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::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    /// Count the canonical Workshop target represented by this program.
139    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}