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::HashMap;
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 Workshop 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 and its nested element-count analysis.
28#[derive(Debug, Clone)]
29pub struct ElementCountNode {
30    pub kind: ElementNodeKind,
31    /// An opaque identity unique within this report. It is not a WIR or
32    /// storage arena index and has no meaning across reports.
33    pub id: usize,
34    /// The canonical Workshop or analysis name for this node.
35    pub name: String,
36    /// The authored source span, when the program retained one.
37    pub span: Option<Span>,
38    /// The node-local contribution before child counts and adjustments.
39    pub base_count: usize,
40    /// The signed node-local adjustment, such as a direct-argument reduction
41    /// or hero-pair surcharge.
42    pub adjustment: isize,
43    /// The node's recursive count: `base_count + adjustment + children`.
44    pub count: usize,
45    /// Nested values, conditions, and actions in canonical source order.
46    pub children: Vec<ElementCountNode>,
47}
48
49/// A structured element-count report for one canonical Workshop program.
50#[derive(Debug, Clone)]
51pub struct ElementCountReport {
52    /// The sum of all rule counts.
53    pub total: usize,
54    /// Rule nodes in canonical source/WIR order.
55    pub rules: Vec<ElementCountNode>,
56}
57
58impl ElementCountReport {
59    /// Return the per-rule total in source/WIR order.
60    pub fn rule_counts(&self) -> impl Iterator<Item = (&str, usize)> {
61        self.rules
62            .iter()
63            .map(|rule| (rule.name.as_str(), rule.count))
64    }
65}
66
67/// A construct for which an exact canonical element count cannot be produced.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ElementCountError {
70    /// The input cannot be materialized or structurally validated as a
71    /// canonical Workshop program.
72    InvalidProgram { message: String },
73    /// The input contains a construct for which this analyzer has no exact
74    /// canonical count.
75    Unsupported {
76        kind: ElementNodeKind,
77        name: String,
78        span: Option<Span>,
79        reason: String,
80    },
81    /// The internal graph contains a recursive value or action reference.
82    Cycle {
83        kind: ElementNodeKind,
84        /// The opaque identity of the active node involved in the cycle.
85        id: usize,
86    },
87}
88
89impl fmt::Display for ElementCountError {
90    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::InvalidProgram { message } => write!(formatter, "invalid program: {message}"),
93            Self::Unsupported {
94                kind,
95                name,
96                span,
97                reason,
98            } => write!(
99                formatter,
100                "unsupported {kind:?} '{name}'{}: {reason}",
101                span.map_or_else(String::new, |span| format!(" at {span:?}"))
102            ),
103            Self::Cycle { kind, id } => write!(formatter, "cyclic {kind:?} reference at {id}"),
104        }
105    }
106}
107
108impl std::error::Error for ElementCountError {}
109
110impl Program {
111    /// Count the canonical Workshop target represented by this WIR program.
112    ///
113    /// The catalog is used to reject unknown action/value identities before a
114    /// report is produced. Native display actions are represented by their
115    /// canonical catalog-backed action calls.
116    pub(crate) fn element_count(
117        &self,
118        catalog: &Catalog,
119    ) -> Result<ElementCountReport, ElementCountError> {
120        self.validate()
121            .map_err(|error| ElementCountError::InvalidProgram {
122                message: error.to_string(),
123            })?;
124        crate::rules::validate::validate_wir(self, catalog).map_err(|error| {
125            ElementCountError::InvalidProgram {
126                message: error.to_string(),
127            }
128        })?;
129
130        let mut counter = Counter {
131            program: self,
132            catalog,
133            values: HashMap::new(),
134            actions: HashMap::new(),
135            next_node_id: 0,
136        };
137        let mut rules = Vec::with_capacity(self.rules.len());
138        for rule in self.rules.iter() {
139            rules.push(counter.rule(rule)?);
140        }
141        let total = rules.iter().map(|rule| rule.count).sum();
142        Ok(ElementCountReport { total, rules })
143    }
144}
145
146impl crate::program::Program {
147    /// Count the canonical Workshop target represented by this program.
148    pub fn element_count(
149        &self,
150        catalog: &Catalog,
151    ) -> Result<ElementCountReport, ElementCountError> {
152        let storage = self
153            .to_wir()
154            .map_err(|error| ElementCountError::InvalidProgram {
155                message: error.to_string(),
156            })?;
157        storage.element_count(catalog)
158    }
159}
160
161struct Counted {
162    node: ElementCountNode,
163    heroes: usize,
164}
165
166impl Counted {
167    #[allow(clippy::too_many_arguments)]
168    fn finish(
169        kind: ElementNodeKind,
170        id: usize,
171        name: impl Into<String>,
172        span: Option<Span>,
173        base_count: usize,
174        adjustment: isize,
175        children: Vec<ElementCountNode>,
176        heroes: usize,
177    ) -> Self {
178        let children_count: usize = children.iter().map(|child| child.count).sum();
179        let count = (base_count as isize + children_count as isize + adjustment).max(0) as usize;
180        Self {
181            node: ElementCountNode {
182                kind,
183                id,
184                name: name.into(),
185                span,
186                base_count,
187                adjustment,
188                count,
189                children,
190            },
191            heroes,
192        }
193    }
194}
195
196struct Counter<'a> {
197    program: &'a Program,
198    catalog: &'a Catalog,
199    values: HashMap<usize, usize>,
200    actions: HashMap<usize, usize>,
201    next_node_id: usize,
202}
203
204impl Counter<'_> {
205    fn next_node_id(&mut self) -> usize {
206        let id = self.next_node_id;
207        self.next_node_id += 1;
208        id
209    }
210
211    fn rule(&mut self, rule: &wir::Rule) -> Result<ElementCountNode, ElementCountError> {
212        let node_id = self.next_node_id();
213        let mut children = Vec::with_capacity(rule.conditions.len() + rule.actions.len());
214        for condition in &rule.conditions {
215            children.push(self.condition(*condition)?.node);
216        }
217        for action in &rule.actions {
218            children.push(self.action(*action)?.node);
219        }
220        Ok(Counted::finish(
221            ElementNodeKind::Rule,
222            node_id,
223            &rule.name,
224            rule.span,
225            1,
226            0,
227            children,
228            0,
229        )
230        .node)
231    }
232
233    fn condition(&mut self, id: ValueId) -> Result<Counted, ElementCountError> {
234        let node_id = self.next_node_id();
235        let Some(value) = self.program.values.get(id) else {
236            return Err(ElementCountError::InvalidProgram {
237                message: format!("dangling condition value {}", id.index()),
238            });
239        };
240        let (children, heroes) = match &value.value {
241            Value::Call { name, args } if is_comparison(name) => {
242                let mut children = Vec::with_capacity(args.len());
243                let mut heroes = 0;
244                for argument in args {
245                    let counted = self.value(*argument, true)?;
246                    heroes += counted.heroes;
247                    children.push(counted.node);
248                }
249                (children, heroes)
250            }
251            _ => {
252                let counted = self.value(id, true)?;
253                (vec![counted.node], counted.heroes)
254            }
255        };
256        Ok(Counted::finish(
257            ElementNodeKind::Condition,
258            node_id,
259            "condition",
260            value.span,
261            1,
262            pair_surcharge(heroes),
263            children,
264            heroes,
265        ))
266    }
267
268    fn action(&mut self, id: ActionId) -> Result<Counted, ElementCountError> {
269        let node_id = self.next_node_id();
270        if let Some(&active_id) = self.actions.get(&id.index()) {
271            return Err(ElementCountError::Cycle {
272                kind: ElementNodeKind::Action,
273                id: active_id,
274            });
275        }
276        self.actions.insert(id.index(), node_id);
277        let Some(action) = self.program.actions.get(id) else {
278            return Err(ElementCountError::InvalidProgram {
279                message: format!("dangling action {}", id.index()),
280            });
281        };
282        let result = self.action_inner(action, node_id);
283        self.actions.remove(&id.index());
284        result
285    }
286
287    fn action_inner(
288        &mut self,
289        action: &Action,
290        node_id: usize,
291    ) -> Result<Counted, ElementCountError> {
292        let span = action.span();
293        let mut children = Vec::new();
294        let mut heroes = 0;
295        let name;
296        match action {
297            Action::SetGlobalVariable { value, .. }
298            | Action::ModifyGlobalVariable { value, .. } => {
299                name = "variable action";
300                self.push_action_value(&mut children, &mut heroes, *value)?;
301            }
302            Action::SetPlayerVariable { player, value, .. }
303            | Action::ModifyPlayerVariable { player, value, .. } => {
304                name = "player variable action";
305                self.push_action_value(&mut children, &mut heroes, *player)?;
306                self.push_action_value(&mut children, &mut heroes, *value)?;
307            }
308            Action::AssignMember { target, value, .. } => {
309                name = "member assignment";
310                self.push_action_value(&mut children, &mut heroes, *target)?;
311                self.push_action_value(&mut children, &mut heroes, *value)?;
312            }
313            Action::CallSubroutine { .. } => {
314                name = "call subroutine";
315            }
316            Action::If {
317                branches,
318                else_body,
319                ..
320            } => {
321                name = "if";
322                for branch in branches {
323                    self.push_action_value(&mut children, &mut heroes, branch.condition)?;
324                    for nested in &branch.body {
325                        children.push(self.action(*nested)?.node);
326                    }
327                }
328                if let Some(body) = else_body {
329                    for nested in body {
330                        children.push(self.action(*nested)?.node);
331                    }
332                }
333            }
334            Action::While {
335                condition, body, ..
336            } => {
337                name = "while";
338                self.push_action_value(&mut children, &mut heroes, *condition)?;
339                for nested in body {
340                    children.push(self.action(*nested)?.node);
341                }
342            }
343            Action::ForGlobalVariable {
344                start,
345                stop,
346                step,
347                body,
348                ..
349            } => {
350                name = "for global variable";
351                for value in [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::ForPlayerVariable {
359                player,
360                start,
361                stop,
362                step,
363                body,
364                ..
365            } => {
366                name = "for player variable";
367                for value in [player, start, stop, step] {
368                    self.push_action_value(&mut children, &mut heroes, *value)?;
369                }
370                for nested in body {
371                    children.push(self.action(*nested)?.node);
372                }
373            }
374            Action::Call {
375                name: action_name,
376                args,
377                ..
378            } => {
379                if self.catalog.entry(Kind::Action, action_name).is_none() {
380                    return Err(ElementCountError::Unsupported {
381                        kind: ElementNodeKind::Action,
382                        name: action_name.clone(),
383                        span,
384                        reason: "the action is not a catalog identity".to_string(),
385                    });
386                }
387                name = action_name.as_str();
388                for argument in args {
389                    self.push_action_value(&mut children, &mut heroes, *argument)?;
390                }
391            }
392        }
393        Ok(Counted::finish(
394            ElementNodeKind::Action,
395            node_id,
396            name,
397            span,
398            1,
399            pair_surcharge(heroes),
400            children,
401            heroes,
402        ))
403    }
404
405    fn push_action_value(
406        &mut self,
407        children: &mut Vec<ElementCountNode>,
408        heroes: &mut usize,
409        id: ValueId,
410    ) -> Result<(), ElementCountError> {
411        let counted = self.value(id, true)?;
412        *heroes += counted.heroes;
413        children.push(counted.node);
414        Ok(())
415    }
416
417    fn value(&mut self, id: ValueId, top_level: bool) -> Result<Counted, ElementCountError> {
418        let node_id = self.next_node_id();
419        if let Some(&active_id) = self.values.get(&id.index()) {
420            return Err(ElementCountError::Cycle {
421                kind: ElementNodeKind::Value,
422                id: active_id,
423            });
424        }
425        self.values.insert(id.index(), node_id);
426        let Some(value) = self.program.values.get(id) else {
427            return Err(ElementCountError::InvalidProgram {
428                message: format!("dangling value {}", id.index()),
429            });
430        };
431        let span = value.span;
432        let result = match &value.value {
433            Value::Number { .. } => self.value_node(node_id, "number", span, 1, vec![], 0),
434            Value::String(_) => self.value_node(node_id, "string", span, 1, vec![], 0),
435            Value::LocalizedString(_) => {
436                self.value_node(node_id, "localized string", span, 2, vec![], 0)
437            }
438            Value::Bool(_) => self.value_node(node_id, "boolean", span, 1, vec![], 0),
439            Value::Null => self.value_node(node_id, "null", span, 1, vec![], 0),
440            Value::Array(elements) => self.value_children(node_id, "array", span, 2, elements),
441            Value::Vector { x, y, z } => {
442                self.value_children(node_id, "vector", span, 1, &[*x, *y, *z])
443            }
444            Value::Enum { value_type, .. } => {
445                let heroes = usize::from(value_type == "Hero");
446                self.value_node(node_id, value_type, span, 1, vec![], heroes)
447            }
448            Value::GlobalVariable(_) => {
449                self.value_node(node_id, "global variable", span, 1, vec![], 0)
450            }
451            Value::PlayerVariable { player, .. } => {
452                self.value_children(node_id, "player variable", span, 1, &[*player])
453            }
454            Value::Subroutine(_) => self.value_node(node_id, "subroutine", span, 1, vec![], 0),
455            Value::EventPlayer => self.value_node(node_id, "event player", span, 1, vec![], 0),
456            Value::Call { name, args } => {
457                if name == crate::wir::AMBIGUOUS_ENUM_CALL
458                    && crate::wir::ambiguous_enum_parts(self.program, id).is_some()
459                {
460                    self.value_node(node_id, "ambiguous enum", span, 1, vec![], 0)
461                } else {
462                    if name != "memberAccess"
463                        && self.catalog.entry(Kind::Value, name).is_none()
464                        && self.catalog.entry(Kind::Operator, name).is_none()
465                        && !is_canonical_helper(name)
466                    {
467                        return Err(ElementCountError::Unsupported {
468                            kind: ElementNodeKind::Value,
469                            name: name.clone(),
470                            span,
471                            reason: "the value is not a catalog identity".to_string(),
472                        });
473                    }
474                    let child_ids: Vec<ValueId> = if name == "memberAccess" {
475                        args.first()
476                            .copied()
477                            .into_iter()
478                            .chain(args.iter().copied().skip(2))
479                            .collect()
480                    } else {
481                        args.clone()
482                    };
483                    let base = if name == "array"
484                        || name == "evalOnce"
485                        || name.starts_with("workshopSetting")
486                        || name.starts_with("createWorkshopSetting")
487                    {
488                        2
489                    } else {
490                        1
491                    };
492                    self.value_children(node_id, name, span, base, &child_ids)
493                }
494            }
495        }?;
496        self.values.remove(&id.index());
497        let mut result = result;
498        if top_level {
499            result.node.adjustment -= 1;
500            result.node.count = (result.node.count as isize - 1).max(0) as usize;
501        }
502        Ok(result)
503    }
504
505    fn value_node(
506        &self,
507        id: usize,
508        name: impl Into<String>,
509        span: Option<Span>,
510        base: usize,
511        children: Vec<ElementCountNode>,
512        heroes: usize,
513    ) -> Result<Counted, ElementCountError> {
514        Ok(Counted::finish(
515            ElementNodeKind::Value,
516            id,
517            name,
518            span,
519            base,
520            0,
521            children,
522            heroes,
523        ))
524    }
525
526    fn value_children(
527        &mut self,
528        id: usize,
529        name: impl Into<String>,
530        span: Option<Span>,
531        base: usize,
532        ids: &[ValueId],
533    ) -> Result<Counted, ElementCountError> {
534        let mut children = Vec::with_capacity(ids.len());
535        let mut heroes = 0;
536        for child in ids {
537            let counted = self.value(*child, false)?;
538            heroes += counted.heroes;
539            children.push(counted.node);
540        }
541        self.value_node(id, name, span, base, children, heroes)
542    }
543}
544
545fn pair_surcharge(heroes: usize) -> isize {
546    (heroes / 2) as isize
547}
548
549fn is_comparison(name: &str) -> bool {
550    matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
551}
552
553fn is_canonical_helper(name: &str) -> bool {
554    matches!(
555        name,
556        "memberAccess"
557            | "+"
558            | "-"
559            | "*"
560            | "/"
561            | "%"
562            | "add"
563            | "subtract"
564            | "multiply"
565            | "divide"
566            | "modulo"
567            | "min"
568            | "max"
569            | "raiseToPower"
570            | "appendToArray"
571            | "removeFromArray"
572            | "removeFromArrayByValue"
573            | "removeFromArrayByIndex"
574    )
575}