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 == crate::wir::AMBIGUOUS_ENUM_CALL
436                    && crate::wir::ambiguous_enum_parts(self.program, id).is_some()
437                {
438                    return self.value_node(id, "ambiguous enum", span, 1, vec![], 0);
439                }
440                if name != "memberAccess"
441                    && self.catalog.entry(Kind::Value, name).is_none()
442                    && self.catalog.entry(Kind::Operator, name).is_none()
443                    && !is_canonical_helper(name)
444                {
445                    return Err(ElementCountError::Unsupported {
446                        kind: ElementNodeKind::Value,
447                        name: name.clone(),
448                        span,
449                        reason: "the value is not a catalog identity".to_string(),
450                    });
451                }
452                let child_ids: Vec<ValueId> = if name == "memberAccess" {
453                    args.first()
454                        .copied()
455                        .into_iter()
456                        .chain(args.iter().copied().skip(2))
457                        .collect()
458                } else {
459                    args.clone()
460                };
461                let base = if name == "array"
462                    || name == "evalOnce"
463                    || name.starts_with("workshopSetting")
464                    || name.starts_with("createWorkshopSetting")
465                {
466                    2
467                } else {
468                    1
469                };
470                self.value_children(id, name, span, base, &child_ids)
471            }
472        }?;
473        self.values.remove(&id.index());
474        let mut result = result;
475        if top_level {
476            result.node.adjustment -= 1;
477            result.node.count = (result.node.count as isize - 1).max(0) as usize;
478        }
479        Ok(result)
480    }
481
482    fn value_node(
483        &self,
484        id: ValueId,
485        name: impl Into<String>,
486        span: Option<Span>,
487        base: usize,
488        children: Vec<ElementCountNode>,
489        heroes: usize,
490    ) -> Result<Counted, ElementCountError> {
491        Ok(Counted::finish(
492            ElementNodeKind::Value,
493            id.index(),
494            name,
495            span,
496            base,
497            0,
498            children,
499            heroes,
500        ))
501    }
502
503    fn value_children(
504        &mut self,
505        id: ValueId,
506        name: impl Into<String>,
507        span: Option<Span>,
508        base: usize,
509        ids: &[ValueId],
510    ) -> Result<Counted, ElementCountError> {
511        let mut children = Vec::with_capacity(ids.len());
512        let mut heroes = 0;
513        for child in ids {
514            let counted = self.value(*child, false)?;
515            heroes += counted.heroes;
516            children.push(counted.node);
517        }
518        self.value_node(id, name, span, base, children, heroes)
519    }
520}
521
522fn pair_surcharge(heroes: usize) -> isize {
523    (heroes / 2) as isize
524}
525
526fn is_comparison(name: &str) -> bool {
527    matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
528}
529
530fn is_canonical_helper(name: &str) -> bool {
531    matches!(
532        name,
533        "memberAccess"
534            | "+"
535            | "-"
536            | "*"
537            | "/"
538            | "%"
539            | "add"
540            | "subtract"
541            | "multiply"
542            | "divide"
543            | "modulo"
544            | "min"
545            | "max"
546            | "raiseToPower"
547            | "appendToArray"
548            | "removeFromArray"
549            | "removeFromArrayByIndex"
550    )
551}