Skip to main content

workshop_rs/analysis/
element_count.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use crate::catalog::{Catalog, Kind};
5use crate::core::source::Span;
6use crate::wir::{self, Action, ActionId, Program, Value, ValueId};
7
8/// The Workshop node category represented in an element-count report.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ElementNodeKind {
11    Rule,
12    Condition,
13    Action,
14    Value,
15}
16
17/// One node's contribution and its nested element-count analysis.
18#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub struct ElementCountNode {
21    pub kind: ElementNodeKind,
22    /// An opaque identity unique within this report. It is not a WIR or
23    /// storage arena index and has no meaning across reports.
24    pub id: usize,
25    /// The canonical Workshop or analysis name for this node.
26    pub name: String,
27    /// The authored source span, when the program retained one.
28    pub span: Option<Span>,
29    /// The node-local contribution before child counts and adjustments.
30    pub base_count: usize,
31    /// The signed node-local adjustment, such as a direct-argument reduction
32    /// or hero-pair surcharge.
33    pub adjustment: isize,
34    /// The node's recursive count: `base_count + adjustment + children`.
35    pub count: usize,
36    /// Nested values, conditions, and actions in canonical source order.
37    pub children: Vec<ElementCountNode>,
38}
39
40/// A structured element-count report for one canonical Workshop program.
41#[derive(Debug, Clone)]
42#[non_exhaustive]
43pub struct ElementCountReport {
44    /// The sum of all rule counts.
45    pub total: usize,
46    /// Rule nodes in canonical source/WIR order.
47    pub rules: Vec<ElementCountNode>,
48}
49
50impl ElementCountNode {
51    /// The number of nodes on the longest downward path starting at this
52    /// node: `1` for a leaf, otherwise `1 + max(child heights)`.
53    ///
54    /// This is a structural measure of the report tree, not a client
55    /// limit; it answers "how deeply nested is this subtree" without
56    /// claiming anything about importability.
57    pub fn height(&self) -> usize {
58        1 + self
59            .children
60            .iter()
61            .map(Self::height)
62            .max()
63            .unwrap_or_default()
64    }
65
66    /// The number of nodes in this subtree, including this node.
67    ///
68    /// Unlike `count`, which is the weighted client element cost, this is
69    /// the plain cardinality of the report subtree.
70    pub fn node_count(&self) -> usize {
71        1 + self.children.iter().map(Self::node_count).sum::<usize>()
72    }
73
74    /// The number of value nodes in this node's own statement value trees:
75    /// `Value` nodes reachable from this node without entering a descendant
76    /// `Action` subtree.
77    ///
78    /// On an `Action` this counts its own argument trees; actions nested in
79    /// block bodies are separate statements and are excluded. On a
80    /// `Condition` it is the condition's value tree, and on a `Rule` it is
81    /// the conditions' value trees only — each action carries its own
82    /// statement. On a `Value` node this equals `node_count()`.
83    ///
84    /// A comparison keeps its operator as a `Value` node inside an `If` or
85    /// `While` argument, while a rule condition absorbs it into the
86    /// `Condition` node — so one source expression measures one node fewer
87    /// as a rule condition than as a block condition.
88    pub fn statement_value_nodes(&self) -> usize {
89        if self.kind == ElementNodeKind::Value {
90            return self.node_count();
91        }
92        self.children
93            .iter()
94            .map(|child| match child.kind {
95                ElementNodeKind::Value | ElementNodeKind::Condition => {
96                    child.statement_value_nodes()
97                }
98                ElementNodeKind::Rule | ElementNodeKind::Action => 0,
99            })
100            .sum()
101    }
102}
103
104impl ElementCountReport {
105    /// Return the per-rule total in source/WIR order.
106    pub fn rule_counts(&self) -> impl Iterator<Item = (&str, usize)> {
107        self.rules
108            .iter()
109            .map(|rule| (rule.name.as_str(), rule.count))
110    }
111}
112
113/// A construct for which an exact canonical element count cannot be produced.
114#[derive(Debug, Clone, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum ElementCountError {
117    /// The input cannot be materialized or structurally validated as a
118    /// canonical Workshop program.
119    InvalidProgram { message: String },
120    /// The input contains a construct for which this analyzer has no exact
121    /// canonical count.
122    Unsupported {
123        kind: ElementNodeKind,
124        name: String,
125        span: Option<Span>,
126        reason: String,
127    },
128    /// The internal graph contains a recursive value or action reference.
129    Cycle { kind: ElementNodeKind, id: usize },
130}
131
132impl fmt::Display for ElementCountError {
133    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            Self::InvalidProgram { message } => write!(formatter, "invalid program: {message}"),
136            Self::Unsupported {
137                kind,
138                name,
139                span,
140                reason,
141            } => write!(
142                formatter,
143                "unsupported {kind:?} '{name}'{}: {reason}",
144                span.map_or_else(String::new, |span| format!(" at {span:?}"))
145            ),
146            Self::Cycle { kind, id } => write!(formatter, "cyclic {kind:?} reference at {id}"),
147        }
148    }
149}
150
151impl std::error::Error for ElementCountError {}
152
153impl Program {
154    pub(crate) fn element_count(
155        &self,
156        catalog: &Catalog,
157    ) -> Result<ElementCountReport, ElementCountError> {
158        self.validate()
159            .map_err(|error| ElementCountError::InvalidProgram {
160                message: error.to_string(),
161            })?;
162        crate::rules::validate::validate_wir(self, catalog, false).map_err(|error| {
163            ElementCountError::InvalidProgram {
164                message: error.to_string(),
165            }
166        })?;
167
168        let mut counter = Counter {
169            program: self,
170            catalog,
171            values: HashMap::new(),
172            actions: HashMap::new(),
173            next_node_id: 0,
174            variable_name_arg: false,
175        };
176        let mut rules = Vec::with_capacity(self.rules.len());
177        for rule in self.rules.iter() {
178            rules.push(counter.rule(rule)?);
179        }
180        let total = rules.iter().map(|rule| rule.count).sum();
181        Ok(ElementCountReport { total, rules })
182    }
183}
184
185impl crate::program::Program {
186    /// Count the canonical Workshop target represented by this program.
187    pub fn element_count(
188        &self,
189        catalog: &Catalog,
190    ) -> Result<ElementCountReport, ElementCountError> {
191        let storage = self
192            .to_wir()
193            .map_err(|error| ElementCountError::InvalidProgram {
194                message: error.to_string(),
195            })?;
196        storage.element_count(catalog)
197    }
198}
199
200struct Counted {
201    node: ElementCountNode,
202    heroes: usize,
203}
204
205impl Counted {
206    #[allow(clippy::too_many_arguments)]
207    fn finish(
208        kind: ElementNodeKind,
209        id: usize,
210        name: impl Into<String>,
211        span: Option<Span>,
212        base_count: usize,
213        adjustment: isize,
214        children: Vec<ElementCountNode>,
215        heroes: usize,
216    ) -> Self {
217        let children_count: usize = children.iter().map(|child| child.count).sum();
218        let count = (base_count as isize + children_count as isize + adjustment).max(0) as usize;
219        Self {
220            node: ElementCountNode {
221                kind,
222                id,
223                name: name.into(),
224                span,
225                base_count,
226                adjustment,
227                count,
228                children,
229            },
230            heroes,
231        }
232    }
233}
234
235struct Counter<'a> {
236    program: &'a Program,
237    catalog: &'a Catalog,
238    values: HashMap<usize, usize>,
239    actions: HashMap<usize, usize>,
240    next_node_id: usize,
241    variable_name_arg: bool,
242}
243
244impl Counter<'_> {
245    fn next_node_id(&mut self) -> usize {
246        let id = self.next_node_id;
247        self.next_node_id += 1;
248        id
249    }
250
251    fn rule(&mut self, rule: &wir::Rule) -> Result<ElementCountNode, ElementCountError> {
252        let node_id = self.next_node_id();
253        let mut children = Vec::with_capacity(rule.conditions.len() + rule.actions.len());
254        for condition in &rule.conditions {
255            children.push(self.condition(condition.value)?.node);
256        }
257        for (index, action) in rule.actions.iter().enumerate() {
258            let rule_final = index + 1 == rule.actions.len();
259            children.push(self.action(*action, rule_final)?.node);
260        }
261        Ok(Counted::finish(
262            ElementNodeKind::Rule,
263            node_id,
264            &rule.name,
265            rule.span,
266            1,
267            0,
268            children,
269            0,
270        )
271        .node)
272    }
273
274    fn condition(&mut self, id: ValueId) -> Result<Counted, ElementCountError> {
275        let node_id = self.next_node_id();
276        let Some(value) = self.program.values.get(id) else {
277            return Err(ElementCountError::InvalidProgram {
278                message: format!("dangling condition value {}", id.index()),
279            });
280        };
281        let (children, heroes) = match &value.value {
282            Value::Call { name, args } if wir::is_comparison_operator(name) => {
283                let mut children = Vec::with_capacity(args.len());
284                let mut heroes = 0;
285                for argument in args {
286                    let counted = self.value(*argument, true)?;
287                    heroes += counted.heroes / 2 * 2;
288                    children.push(counted.node);
289                }
290                (children, heroes)
291            }
292            _ => {
293                let counted = self.value(id, true)?;
294                (vec![counted.node], counted.heroes)
295            }
296        };
297        Ok(Counted::finish(
298            ElementNodeKind::Condition,
299            node_id,
300            "condition",
301            value.span,
302            1,
303            pair_surcharge(heroes),
304            children,
305            heroes,
306        ))
307    }
308
309    fn action(&mut self, id: ActionId, rule_final: bool) -> Result<Counted, ElementCountError> {
310        let node_id = self.next_node_id();
311        if let Some(&active_id) = self.actions.get(&id.index()) {
312            return Err(ElementCountError::Cycle {
313                kind: ElementNodeKind::Action,
314                id: active_id,
315            });
316        }
317        self.actions.insert(id.index(), node_id);
318        let mut action = self.program.actions.get(id);
319        while let Some(Action::Disabled { action: inner, .. }) = action {
320            action = self.program.actions.get(*inner);
321        }
322        let Some(action) = action else {
323            return Err(ElementCountError::InvalidProgram {
324                message: format!("dangling action {}", id.index()),
325            });
326        };
327        let result = self.action_inner(action, node_id, rule_final);
328        self.actions.remove(&id.index());
329        result
330    }
331
332    fn action_inner(
333        &mut self,
334        action: &Action,
335        node_id: usize,
336        rule_final: bool,
337    ) -> Result<Counted, ElementCountError> {
338        let span = action.span();
339        let mut children = Vec::new();
340        let mut heroes = 0;
341        let mut base = 1;
342        let name;
343        match action {
344            Action::SetGlobalVariable { value, .. }
345            | Action::ModifyGlobalVariable { value, .. } => {
346                name = "variable action";
347                self.push_action_value(&mut children, &mut heroes, *value)?;
348            }
349            Action::SetPlayerVariable { player, value, .. }
350            | Action::ModifyPlayerVariable { player, value, .. } => {
351                name = "player variable action";
352                self.push_action_value(&mut children, &mut heroes, *player)?;
353                self.push_action_value(&mut children, &mut heroes, *value)?;
354            }
355            Action::AssignMember { target, value, .. } => {
356                name = "member assignment";
357                self.push_action_value(&mut children, &mut heroes, *target)?;
358                self.push_action_value(&mut children, &mut heroes, *value)?;
359            }
360            Action::CallSubroutine { .. } => {
361                name = "call subroutine";
362            }
363            Action::If {
364                branches,
365                else_body,
366                ..
367            } => {
368                name = "if";
369                base += branches.len().saturating_sub(1)
370                    + usize::from(else_body.is_some())
371                    + usize::from(!rule_final);
372                for branch in branches {
373                    self.push_action_value(&mut children, &mut heroes, branch.condition)?;
374                    for nested in &branch.body {
375                        children.push(self.action(*nested, false)?.node);
376                    }
377                }
378                if let Some(body) = else_body {
379                    for nested in body {
380                        children.push(self.action(*nested, false)?.node);
381                    }
382                }
383            }
384            Action::While {
385                condition, body, ..
386            } => {
387                name = "while";
388                base += 1;
389                self.push_action_value(&mut children, &mut heroes, *condition)?;
390                for nested in body {
391                    children.push(self.action(*nested, false)?.node);
392                }
393            }
394            Action::ForGlobalVariable {
395                start,
396                stop,
397                step,
398                body,
399                ..
400            } => {
401                name = "for global variable";
402                base += 1;
403                for value in [start, stop, step] {
404                    self.push_action_value(&mut children, &mut heroes, *value)?;
405                }
406                for nested in body {
407                    children.push(self.action(*nested, false)?.node);
408                }
409            }
410            Action::ForPlayerVariable {
411                player,
412                start,
413                stop,
414                step,
415                body,
416                ..
417            } => {
418                name = "for player variable";
419                base += 1;
420                for value in [player, start, stop, step] {
421                    self.push_action_value(&mut children, &mut heroes, *value)?;
422                }
423                for nested in body {
424                    children.push(self.action(*nested, false)?.node);
425                }
426            }
427            Action::Disabled { .. } => {
428                return Err(ElementCountError::InvalidProgram {
429                    message: "a disabled action wraps no enabled action".to_string(),
430                });
431            }
432            Action::Call {
433                name: action_name,
434                args,
435                ..
436            } => {
437                if self.catalog.entry(Kind::Action, action_name).is_none() {
438                    return Err(ElementCountError::Unsupported {
439                        kind: ElementNodeKind::Action,
440                        name: action_name.clone(),
441                        span,
442                        reason: "the action is not a catalog identity".to_string(),
443                    });
444                }
445                name = action_name.as_str();
446                for (index, argument) in args.iter().enumerate() {
447                    self.variable_name_arg = variable_slot(
448                        self.program,
449                        self.catalog,
450                        Kind::Action,
451                        action_name,
452                        args,
453                        index,
454                    ) || (index == 0
455                        && takes_variable(self.catalog, Kind::Action, action_name));
456                    self.push_action_value(&mut children, &mut heroes, *argument)?;
457                }
458                base = (base as isize
459                    + omitted_default_cost(
460                        self.program,
461                        self.catalog,
462                        Kind::Action,
463                        action_name,
464                        args,
465                        true,
466                    ))
467                .max(0) as usize;
468            }
469        }
470        Ok(Counted::finish(
471            ElementNodeKind::Action,
472            node_id,
473            name,
474            span,
475            base,
476            pair_surcharge(heroes),
477            children,
478            heroes,
479        ))
480    }
481
482    fn push_action_value(
483        &mut self,
484        children: &mut Vec<ElementCountNode>,
485        heroes: &mut usize,
486        id: ValueId,
487    ) -> Result<(), ElementCountError> {
488        let counted = self.value(id, true)?;
489        *heroes += counted.heroes / 2 * 2;
490        children.push(counted.node);
491        Ok(())
492    }
493
494    fn value(&mut self, id: ValueId, top_level: bool) -> Result<Counted, ElementCountError> {
495        let variable_name_arg = std::mem::take(&mut self.variable_name_arg);
496        let node_id = self.next_node_id();
497        if let Some(&active_id) = self.values.get(&id.index()) {
498            return Err(ElementCountError::Cycle {
499                kind: ElementNodeKind::Value,
500                id: active_id,
501            });
502        }
503        self.values.insert(id.index(), node_id);
504        let Some(value) = self.program.values.get(id) else {
505            return Err(ElementCountError::InvalidProgram {
506                message: format!("dangling value {}", id.index()),
507            });
508        };
509        let span = value.span;
510        let result = match &value.value {
511            Value::Number { .. } => self.value_node(node_id, "number", span, 2, vec![], 0),
512            Value::String(_) => self.value_node(node_id, "string", span, 1, vec![], 0),
513            Value::LocalizedString(_) => {
514                self.value_node(node_id, "localized string", span, 2, vec![], 0)
515            }
516            Value::Bool(_) => self.value_node(node_id, "boolean", span, 1, vec![], 0),
517            Value::Null => self.value_node(node_id, "null", span, 1, vec![], 0),
518            Value::Array(elements) => {
519                self.value_children(node_id, "array", span, 2, elements, None)
520            }
521            Value::Vector { x, y, z } => {
522                self.value_children(node_id, "vector", span, 1, &[*x, *y, *z], None)
523            }
524            Value::Enum { value_type, .. } => {
525                let heroes = usize::from(value_type == "Hero");
526                let base = literal_enum_cost(value_type);
527                self.value_node(node_id, value_type, span, base, vec![], heroes)
528            }
529            Value::GlobalVariable(_) => {
530                let base = if variable_name_arg { 1 } else { 2 };
531                self.value_node(node_id, "global variable", span, base, vec![], 0)
532            }
533            Value::PlayerVariable { player, .. } => {
534                let base = if variable_name_arg { 0 } else { 2 };
535                self.value_children(node_id, "player variable", span, base, &[*player], None)
536            }
537            Value::Subroutine(_) => self.value_node(node_id, "subroutine", span, 1, vec![], 0),
538            Value::EventPlayer => self.value_node(node_id, "event player", span, 1, vec![], 0),
539            Value::Call { name, args } => {
540                if name == crate::wir::AMBIGUOUS_ENUM_CALL
541                    && crate::wir::ambiguous_enum_parts(self.program, id).is_some()
542                {
543                    self.value_node(node_id, "ambiguous enum", span, 1, vec![], 0)
544                } else {
545                    if name != "memberAccess"
546                        && self.catalog.entry(Kind::Value, name).is_none()
547                        && self.catalog.entry(Kind::Operator, name).is_none()
548                        && !crate::wir::is_canonical_helper(name)
549                    {
550                        return Err(ElementCountError::Unsupported {
551                            kind: ElementNodeKind::Value,
552                            name: name.clone(),
553                            span,
554                            reason: "the value is not a catalog identity".to_string(),
555                        });
556                    }
557                    let child_ids: Vec<ValueId> = if name == "memberAccess" {
558                        args.first()
559                            .copied()
560                            .into_iter()
561                            .chain(args.iter().copied().skip(2))
562                            .collect()
563                    } else {
564                        args.clone()
565                    };
566                    let base = if wir::is_comparison_operator(name)
567                        || name == "array"
568                        || name == "evaluateOnce"
569                    {
570                        2
571                    } else {
572                        1
573                    };
574                    let omitted = omitted_default_cost(
575                        self.program,
576                        self.catalog,
577                        Kind::Value,
578                        name,
579                        args,
580                        false,
581                    );
582                    let mut counted = self.value_children(
583                        node_id,
584                        name,
585                        span,
586                        (base as isize + omitted) as usize,
587                        &child_ids,
588                        Some((Kind::Value, name)),
589                    )?;
590                    let setting = workshop_setting_adjustment(name);
591                    counted.node.adjustment += setting;
592                    counted.node.count = (counted.node.count as isize + setting).max(0) as usize;
593                    Ok(counted)
594                }
595            }
596        }?;
597        self.values.remove(&id.index());
598        let mut result = result;
599        if top_level {
600            result.node.adjustment -= 1;
601            result.node.count = (result.node.count as isize - 1).max(0) as usize;
602        }
603        Ok(result)
604    }
605
606    fn value_node(
607        &self,
608        id: usize,
609        name: impl Into<String>,
610        span: Option<Span>,
611        base: usize,
612        children: Vec<ElementCountNode>,
613        heroes: usize,
614    ) -> Result<Counted, ElementCountError> {
615        Ok(Counted::finish(
616            ElementNodeKind::Value,
617            id,
618            name,
619            span,
620            base,
621            0,
622            children,
623            heroes,
624        ))
625    }
626
627    fn value_children(
628        &mut self,
629        id: usize,
630        name: impl Into<String>,
631        span: Option<Span>,
632        base: usize,
633        ids: &[ValueId],
634        owner: Option<(Kind, &str)>,
635    ) -> Result<Counted, ElementCountError> {
636        let mut children = Vec::with_capacity(ids.len());
637        let mut heroes = 0;
638        for (index, child) in ids.iter().enumerate() {
639            self.variable_name_arg = owner.is_some_and(|(kind, name)| {
640                variable_slot(self.program, self.catalog, kind, name, ids, index)
641            });
642            let counted = self.value(*child, false)?;
643            heroes += counted.heroes;
644            children.push(counted.node);
645        }
646        self.value_node(id, name, span, base, children, heroes)
647    }
648}
649
650fn pair_surcharge(heroes: usize) -> isize {
651    (heroes / 2) as isize
652}
653
654fn omitted_default_cost(
655    program: &wir::Program,
656    catalog: &Catalog,
657    kind: Kind,
658    name: &str,
659    args: &[ValueId],
660    direct: bool,
661) -> isize {
662    let Some(entry) = catalog.entry(kind, name) else {
663        return 0;
664    };
665    let given = args.len() + usize::from(entry.wir_args_folded(program, args));
666    (given..entry.param_count())
667        .filter_map(|index| entry.param_default(index))
668        .map(|default| {
669            let cost = if default.parse::<f64>().is_ok() {
670                2
671            } else {
672                default.split('.').next().map_or(1, literal_enum_cost) as isize
673            };
674            cost - isize::from(direct)
675        })
676        .sum()
677}
678
679fn literal_enum_cost(value_type: &str) -> usize {
680    if matches!(value_type, "Team" | "Hero" | "Color" | "Button" | "Map") {
681        2
682    } else {
683        1
684    }
685}
686
687fn variable_slot(
688    program: &wir::Program,
689    catalog: &Catalog,
690    kind: Kind,
691    name: &str,
692    args: &[ValueId],
693    index: usize,
694) -> bool {
695    catalog.entry(kind, name).is_some_and(|entry| {
696        let index = entry.wir_arg_param_index(program, args, index);
697        entry.param_type(index) == Some("Variable")
698            || entry
699                .params()
700                .get(index)
701                .is_some_and(|param| param == "Variable")
702    })
703}
704
705fn takes_variable(catalog: &Catalog, kind: Kind, name: &str) -> bool {
706    catalog.entry(kind, name).is_some_and(|entry| {
707        (0..entry.param_count()).any(|index| {
708            entry.param_type(index) == Some("Variable")
709                || entry
710                    .params()
711                    .get(index)
712                    .is_some_and(|param| param == "Variable")
713        })
714    })
715}
716
717fn workshop_setting_adjustment(name: &str) -> isize {
718    match name {
719        "workshopSettingInteger"
720        | "workshopSettingFloat"
721        | "createWorkshopSettingInt"
722        | "createWorkshopSettingFloat" => -3,
723        "workshopSettingCombo" | "createWorkshopSettingEnum" => -2,
724        _ => 0,
725    }
726}