Skip to main content

workshop_rs/
program.rs

1//! Canonical Workshop program concepts.
2
3use self::identity::{
4    NodeIdentity, action_identity, condition_identity, declaration_identity, rule_identity,
5    value_identity,
6};
7use crate::core::error::WorkshopError;
8use crate::settings::Settings;
9use crate::source::{FileId, SourceDocument, SourceFile, Span};
10
11mod conversion;
12mod identity;
13pub(crate) mod shared;
14mod source_map;
15pub use shared::{EventTarget, EventTeam, ModifyOp, PlayerEventKind};
16pub use source_map::{MAPPED_TEXT_V1, MappedText, SourceMap, SourceMapError, TEXT_V1};
17
18/// A complete Workshop program built from Workshop concepts.
19#[derive(Debug, Clone, Default)]
20pub struct Program {
21    pub settings: Option<Settings>,
22    pub global_variables: Vec<Variable>,
23    pub player_variables: Vec<Variable>,
24    pub subroutines: Vec<Subroutine>,
25    pub rules: Vec<Rule>,
26    files: Vec<SourceFile>,
27    provenance: Option<Box<ProgramProvenance>>,
28}
29
30#[derive(Debug, Clone, Default)]
31struct ProgramProvenance {
32    global_variables: Vec<DeclarationProvenance>,
33    player_variables: Vec<DeclarationProvenance>,
34    subroutines: Vec<DeclarationProvenance>,
35    rules: Vec<RuleProvenance>,
36}
37
38#[derive(Debug, Clone, Copy, Default)]
39struct DeclarationProvenance {
40    span: Option<Span>,
41    name_span: Option<Span>,
42    identity: NodeIdentity,
43}
44
45#[derive(Debug, Clone, Default)]
46struct RuleProvenance {
47    span: Option<crate::source::Span>,
48    name: Option<Span>,
49    event_name: Option<Span>,
50    identity: NodeIdentity,
51    conditions: Vec<ValueProvenance>,
52    actions: Vec<ActionProvenance>,
53}
54
55impl RuleProvenance {
56    fn reseat(&mut self, identity: NodeIdentity) {
57        if self.identity != identity {
58            self.span = None;
59            self.name = None;
60            self.event_name = None;
61        }
62        self.identity = identity;
63    }
64}
65
66#[derive(Debug, Clone, Default)]
67struct ActionProvenance {
68    span: Option<Span>,
69    identifier: Option<Span>,
70    identity: NodeIdentity,
71    arguments: Vec<ValueProvenance>,
72}
73
74trait SpanSlot {
75    fn span_slot(&mut self) -> &mut Option<Span>;
76    fn reseat(&mut self, identity: NodeIdentity);
77}
78
79impl SpanSlot for ValueProvenance {
80    fn span_slot(&mut self) -> &mut Option<Span> {
81        &mut self.span
82    }
83    fn reseat(&mut self, identity: NodeIdentity) {
84        if self.identity != identity {
85            self.span = None;
86            self.identifier = None;
87        }
88        self.identity = identity;
89    }
90}
91
92impl SpanSlot for ActionProvenance {
93    fn span_slot(&mut self) -> &mut Option<Span> {
94        &mut self.span
95    }
96    fn reseat(&mut self, identity: NodeIdentity) {
97        if self.identity != identity {
98            self.span = None;
99            self.identifier = None;
100        }
101        self.identity = identity;
102    }
103}
104
105fn prune_spans_outside(
106    provenance: &mut ProgramProvenance,
107    file: FileId,
108    document: &SourceDocument,
109) {
110    fn prune_slot(slot: &mut Option<Span>, file: FileId, document: &SourceDocument) {
111        if let Some(span) = slot
112            && span.file == file
113            && document.byte_range(*span).is_none()
114        {
115            *slot = None;
116        }
117    }
118    fn prune_value(record: &mut ValueProvenance, file: FileId, document: &SourceDocument) {
119        prune_slot(&mut record.span, file, document);
120        prune_slot(&mut record.identifier, file, document);
121        for child in &mut record.children {
122            prune_value(child, file, document);
123        }
124    }
125    for declaration in provenance
126        .global_variables
127        .iter_mut()
128        .chain(&mut provenance.player_variables)
129        .chain(&mut provenance.subroutines)
130    {
131        prune_slot(&mut declaration.span, file, document);
132        prune_slot(&mut declaration.name_span, file, document);
133    }
134    for rule in &mut provenance.rules {
135        prune_slot(&mut rule.span, file, document);
136        prune_slot(&mut rule.name, file, document);
137        prune_slot(&mut rule.event_name, file, document);
138        for condition in &mut rule.conditions {
139            prune_value(condition, file, document);
140        }
141        for action in &mut rule.actions {
142            prune_slot(&mut action.span, file, document);
143            prune_slot(&mut action.identifier, file, document);
144            for argument in &mut action.arguments {
145                prune_value(argument, file, document);
146            }
147        }
148    }
149}
150
151#[derive(Debug, Clone, Default)]
152struct ValueProvenance {
153    span: Option<Span>,
154    identifier: Option<Span>,
155    identity: NodeIdentity,
156    children: Vec<ValueProvenance>,
157}
158
159/// A failure while attaching source mappings to a public [`Program`].
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum SourceMappingError {
163    UnknownFile(FileId),
164    InvalidSpan(Span),
165    InvalidRule(usize),
166    InvalidCondition {
167        rule: usize,
168        condition: usize,
169    },
170    InvalidAction {
171        rule: usize,
172        action: usize,
173    },
174    InvalidActionArgument {
175        rule: usize,
176        action: usize,
177        argument: usize,
178    },
179    InvalidGlobalVariable(usize),
180    InvalidPlayerVariable(usize),
181    InvalidSubroutine(usize),
182}
183
184impl std::fmt::Display for SourceMappingError {
185    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        match self {
187            Self::UnknownFile(file) => {
188                write!(formatter, "source span references unknown file {file}")
189            }
190            Self::InvalidSpan(span) => write!(formatter, "invalid source span {span:?}"),
191            Self::InvalidRule(rule) => write!(formatter, "invalid rule index {rule}"),
192            Self::InvalidCondition { rule, condition } => {
193                write!(
194                    formatter,
195                    "invalid condition index {condition} in rule {rule}"
196                )
197            }
198            Self::InvalidAction { rule, action } => {
199                write!(formatter, "invalid action index {action} in rule {rule}")
200            }
201            Self::InvalidActionArgument {
202                rule,
203                action,
204                argument,
205            } => write!(
206                formatter,
207                "invalid argument index {argument} in action {action} of rule {rule}"
208            ),
209            Self::InvalidGlobalVariable(variable) => {
210                write!(formatter, "invalid global variable index {variable}")
211            }
212            Self::InvalidPlayerVariable(variable) => {
213                write!(formatter, "invalid player variable index {variable}")
214            }
215            Self::InvalidSubroutine(subroutine) => {
216                write!(formatter, "invalid subroutine index {subroutine}")
217            }
218        }
219    }
220}
221
222impl std::error::Error for SourceMappingError {}
223
224impl Program {
225    pub fn new() -> Self {
226        Self::default()
227    }
228
229    /// Register a source file and return its public file identity.
230    pub fn add_file(&mut self, mut file: SourceFile) -> FileId {
231        let id = FileId::from_index(self.files.len());
232        file.bind_file(id);
233        self.files.push(file);
234        id
235    }
236
237    pub fn global_variable(&mut self, variable: Variable) -> &mut Self {
238        self.global_variables.push(variable);
239        self
240    }
241
242    pub fn player_variable(&mut self, variable: Variable) -> &mut Self {
243        self.player_variables.push(variable);
244        self
245    }
246
247    pub fn subroutine(&mut self, subroutine: Subroutine) -> &mut Self {
248        self.subroutines.push(subroutine);
249        self
250    }
251
252    pub fn rule(&mut self, rule: Rule) -> &mut Self {
253        self.rules.push(rule);
254        self
255    }
256
257    /// Return the retained source document for a parsed file.
258    pub fn source(&self, file: FileId) -> Option<&SourceDocument> {
259        self.files.get(file.index()).and_then(SourceFile::source)
260    }
261
262    /// Attach authored source text to a registered file. Returns `false` when
263    /// `file` is not a known file entry.
264    ///
265    /// Attaching text audits the provenance that belongs to this file: a
266    /// recorded span in `file` that does not resolve inside the retained
267    /// document is stale data — for a provider-mapped program it can describe
268    /// the pre-expansion token stream or a coordinate space the map no longer
269    /// owns — and is cleared so consumers see no span rather than a wrong
270    /// one, matching how `record_identities` retires displaced records
271    /// (wrightkit/wright#583). Spans recorded against other files are left
272    /// untouched; they are audited when their own source is attached.
273    pub fn set_file_source(&mut self, file: FileId, source: impl Into<String>) -> bool {
274        let Some(entry) = self.files.get_mut(file.index()) else {
275            return false;
276        };
277        entry.set_source(source);
278        if let (Some(provenance), Some(document)) = (
279            self.provenance.as_mut(),
280            self.files.get(file.index()).and_then(SourceFile::source),
281        ) {
282            prune_spans_outside(provenance, file, document);
283        }
284        true
285    }
286
287    /// Attach the authored span of a public rule.
288    pub fn set_rule_span(
289        &mut self,
290        rule: usize,
291        span: Option<Span>,
292    ) -> std::result::Result<(), SourceMappingError> {
293        self.validate_span(span)?;
294        self.rule_provenance_mut(rule)?.span = span;
295        Ok(())
296    }
297
298    /// Attach the authored span of a public rule condition value.
299    pub fn set_condition_span(
300        &mut self,
301        rule: usize,
302        condition: usize,
303        span: Option<Span>,
304    ) -> std::result::Result<(), SourceMappingError> {
305        self.set_child_span(
306            rule,
307            condition,
308            span,
309            |rule| rule.conditions.len(),
310            |rule, index| condition_identity(&rule.conditions[index]),
311            |rule, index| SourceMappingError::InvalidCondition {
312                rule,
313                condition: index,
314            },
315            |rule_data| &mut rule_data.conditions,
316        )
317    }
318
319    /// Attach the authored span of a public action in its linear rule order.
320    pub fn set_action_span(
321        &mut self,
322        rule: usize,
323        action: usize,
324        span: Option<Span>,
325    ) -> std::result::Result<(), SourceMappingError> {
326        self.set_child_span(
327            rule,
328            action,
329            span,
330            |rule| rule.actions.len(),
331            |rule, index| action_identity(&rule.actions[index]),
332            |rule, index| SourceMappingError::InvalidAction {
333                rule,
334                action: index,
335            },
336            |rule_data| &mut rule_data.actions,
337        )
338    }
339
340    #[allow(clippy::too_many_arguments)]
341    fn set_child_span<T: SpanSlot + Default>(
342        &mut self,
343        rule: usize,
344        index: usize,
345        span: Option<Span>,
346        count: impl Fn(&crate::Rule) -> usize,
347        identity: impl Fn(&crate::Rule, usize) -> NodeIdentity,
348        invalid: impl Fn(usize, usize) -> SourceMappingError,
349        slots: impl Fn(&mut RuleProvenance) -> &mut Vec<T>,
350    ) -> std::result::Result<(), SourceMappingError> {
351        self.validate_span(span)?;
352        let public = self
353            .rules
354            .get(rule)
355            .ok_or(SourceMappingError::InvalidRule(rule))?;
356        let child_count = count(public);
357        if index >= child_count {
358            return Err(invalid(rule, index));
359        }
360        let identity = identity(public, index);
361        let rule_data = self.rule_provenance_mut(rule)?;
362        fit(slots(rule_data), child_count);
363        let record = &mut slots(rule_data)[index];
364        record.reseat(identity);
365        *record.span_slot() = span;
366        Ok(())
367    }
368
369    /// Attach the authored span of a direct value argument of a public action.
370    pub fn set_action_argument_span(
371        &mut self,
372        rule: usize,
373        action: usize,
374        argument: usize,
375        span: Option<Span>,
376    ) -> std::result::Result<(), SourceMappingError> {
377        self.validate_span(span)?;
378        let (identity, argument_count) = {
379            let action_value = self
380                .rules
381                .get(rule)
382                .ok_or(SourceMappingError::InvalidRule(rule))?
383                .actions
384                .get(action)
385                .ok_or(SourceMappingError::InvalidAction { rule, action })?;
386            let argument_values = action_argument_values(action_value);
387            let Some(&argument_value) = argument_values.get(argument) else {
388                return Err(SourceMappingError::InvalidActionArgument {
389                    rule,
390                    action,
391                    argument,
392                });
393            };
394            (value_identity(argument_value), argument_values.len())
395        };
396        let action_data = self.action_provenance_mut(rule, action)?;
397        fit(&mut action_data.arguments, argument_count);
398        let record = &mut action_data.arguments[argument];
399        record.reseat(identity);
400        record.span = span;
401        Ok(())
402    }
403
404    /// Attach the authored and identifier spans of a global variable.
405    pub fn set_global_variable_spans(
406        &mut self,
407        variable: usize,
408        span: Option<Span>,
409        name_span: Option<Span>,
410    ) -> std::result::Result<(), SourceMappingError> {
411        self.set_declaration_spans(DeclarationTable::GlobalVariables, variable, span, name_span)
412    }
413
414    /// Attach the authored and identifier spans of a player variable.
415    pub fn set_player_variable_spans(
416        &mut self,
417        variable: usize,
418        span: Option<Span>,
419        name_span: Option<Span>,
420    ) -> std::result::Result<(), SourceMappingError> {
421        self.set_declaration_spans(DeclarationTable::PlayerVariables, variable, span, name_span)
422    }
423
424    /// Attach the authored and identifier spans of a subroutine.
425    pub fn set_subroutine_spans(
426        &mut self,
427        subroutine: usize,
428        span: Option<Span>,
429        name_span: Option<Span>,
430    ) -> std::result::Result<(), SourceMappingError> {
431        self.set_declaration_spans(DeclarationTable::Subroutines, subroutine, span, name_span)
432    }
433
434    fn set_declaration_spans(
435        &mut self,
436        table: DeclarationTable,
437        index: usize,
438        span: Option<Span>,
439        name_span: Option<Span>,
440    ) -> std::result::Result<(), SourceMappingError> {
441        self.validate_span(span)?;
442        self.validate_span(name_span)?;
443        let count = table.count(self);
444        if index >= count {
445            return Err(table.invalid(index));
446        }
447        let identity = table.identity(self, index);
448        let slots = table.slots(self.provenance_mut());
449        fit(slots, count);
450        slots[index] = DeclarationProvenance {
451            span,
452            name_span,
453            identity,
454        };
455        Ok(())
456    }
457
458    /// Return the authored span of a public rule, when source metadata exists.
459    ///
460    /// Attached mappings record the program shape and node content they were
461    /// attached to. Every span accessor returns `None` once the corresponding
462    /// public list has been inserted into or removed from, or once the node
463    /// at that position no longer has the content the mapping was attached
464    /// to — see "Shape and content guard" in `docs/source-preservation.md`.
465    pub fn rule_span(&self, rule: usize) -> Option<crate::source::Span> {
466        self.rule_provenance(rule)?.span
467    }
468
469    /// Return the authored span of a public rule condition value.
470    pub fn condition_span(&self, rule: usize, condition: usize) -> Option<crate::source::Span> {
471        self.condition_provenance(rule, condition)?.span
472    }
473
474    /// Return the authored span of a public action in its linear rule order.
475    pub fn action_span(&self, rule: usize, action: usize) -> Option<crate::source::Span> {
476        self.action_provenance(rule, action)?.span
477    }
478
479    /// Return the authored span of a direct value argument of a public action.
480    pub fn action_argument_span(
481        &self,
482        rule: usize,
483        action: usize,
484        argument: usize,
485    ) -> Option<crate::source::Span> {
486        self.action_argument_provenance(rule, action, argument)?
487            .span
488    }
489
490    /// Return the authored span of the declared name of a global variable.
491    ///
492    /// Source-language providers attach an explicit identifier span through
493    /// [`set_global_variable_spans`](Self::set_global_variable_spans). For raw
494    /// Workshop parses the recorded declaration span already covers exactly
495    /// the declared name and is returned as the identifier span.
496    pub fn global_variable_name_span(&self, variable: usize) -> Option<Span> {
497        self.declaration_name_span(
498            |provenance| &provenance.global_variables,
499            &self.global_variables,
500            variable,
501        )
502    }
503
504    /// Return the authored span of the declared name of a player variable.
505    /// See [`global_variable_name_span`](Self::global_variable_name_span).
506    pub fn player_variable_name_span(&self, variable: usize) -> Option<Span> {
507        self.declaration_name_span(
508            |provenance| &provenance.player_variables,
509            &self.player_variables,
510            variable,
511        )
512    }
513
514    /// Return the authored span of the declared name of a subroutine.
515    /// See [`global_variable_name_span`](Self::global_variable_name_span).
516    pub fn subroutine_name_span(&self, subroutine: usize) -> Option<Span> {
517        self.declaration_name_span(
518            |provenance| &provenance.subroutines,
519            &self.subroutines,
520            subroutine,
521        )
522    }
523
524    /// Return the span recorded for the variable or subroutine identifier a
525    /// public action names: the target of set/modify and for-variable actions,
526    /// or the callee of a [`Call Subroutine`](Action::CallSubroutine) action.
527    ///
528    /// Raw Workshop parses record the variable name for `Set`/`Modify`
529    /// variable actions, `For` variable loops, and `Global.name`/`Event
530    /// Player.name` infix assignments, and the callee name for `Call
531    /// Subroutine`. Indexed writes lower to `... Variable At Index` calls and
532    /// record the name on their variable argument — see
533    /// [`action_argument_value_span`](Self::action_argument_value_span).
534    /// Other action forms always return `None`.
535    pub fn action_identifier_span(&self, rule: usize, action: usize) -> Option<Span> {
536        self.action_provenance(rule, action)?.identifier
537    }
538
539    /// Return the span recorded for a rule's name inside its `rule("name")`
540    /// string, or `None` when no provenance was recorded.
541    pub fn rule_name_span(&self, rule: usize) -> Option<Span> {
542        self.rule_provenance(rule)?.name
543    }
544
545    /// Return the span recorded for the subroutine name a rule's `Subroutine`
546    /// event binding names, or `None` for other event kinds and when no
547    /// provenance was recorded.
548    pub fn rule_event_name_span(&self, rule: usize) -> Option<Span> {
549        self.rule_provenance(rule)?.event_name
550    }
551
552    /// Return the authored span of a value nested inside a public rule
553    /// condition.
554    ///
555    /// `path` walks the public [`Value`] tree: each element selects a child by
556    /// position — `Value::Array` elements and `Value::Call` arguments by
557    /// index, `Value::Vector` components as `0`/`1`/`2` for x/y/z, and a
558    /// `Value::PlayerVariable` player at `0`. An empty path returns the
559    /// condition value's own span, matching [`condition_span`](Self::condition_span).
560    ///
561    /// For a variable or subroutine reference the identifier span is returned
562    /// when the parser recorded one: raw Workshop records the variable name
563    /// for `Global.name`, `Global/Player Variable(name)`, `Event Player.name`,
564    /// bare-name, and `... At Index` argument spellings. Other nodes return
565    /// the span recorded for the node itself.
566    pub fn condition_value_span(
567        &self,
568        rule: usize,
569        condition: usize,
570        path: &[usize],
571    ) -> Option<crate::source::Span> {
572        let record = self.condition_value_record(rule, condition, path)?;
573        record.identifier.or(record.span)
574    }
575
576    fn condition_value_record(
577        &self,
578        rule: usize,
579        condition: usize,
580        path: &[usize],
581    ) -> Option<&ValueProvenance> {
582        let record = self.condition_record(rule, condition)?;
583        let value = &self.rules[rule].conditions[condition].value;
584        let identity = condition_identity(&self.rules[rule].conditions[condition]);
585        value_provenance_at(record, value, path, identity)
586    }
587
588    pub(crate) fn condition_value_node_span(
589        &self,
590        rule: usize,
591        condition: usize,
592        path: &[usize],
593    ) -> Option<crate::source::Span> {
594        self.condition_value_record(rule, condition, path)?.span
595    }
596
597    /// Return the authored span of a value nested inside a direct value
598    /// argument of a public action.
599    ///
600    /// `argument` selects the same direct argument as
601    /// [`action_argument_span`](Self::action_argument_span) and `path` walks
602    /// into it the way [`condition_value_span`](Self::condition_value_span)
603    /// describes; an empty path returns the argument's own span. Like
604    /// `condition_value_span`, a variable or subroutine reference returns its
605    /// recorded identifier span.
606    pub fn action_argument_value_span(
607        &self,
608        rule: usize,
609        action: usize,
610        argument: usize,
611        path: &[usize],
612    ) -> Option<crate::source::Span> {
613        let record = self.action_argument_value_record(rule, action, argument, path)?;
614        record.identifier.or(record.span)
615    }
616
617    fn action_argument_value_record(
618        &self,
619        rule: usize,
620        action: usize,
621        argument: usize,
622        path: &[usize],
623    ) -> Option<&ValueProvenance> {
624        let record = self.argument_record(rule, action, argument)?;
625        let value = action_argument_values(&self.rules[rule].actions[action])[argument];
626        value_provenance_at(record, value, path, value_identity(value))
627    }
628
629    pub(crate) fn action_argument_value_node_span(
630        &self,
631        rule: usize,
632        action: usize,
633        argument: usize,
634        path: &[usize],
635    ) -> Option<crate::source::Span> {
636        self.action_argument_value_record(rule, action, argument, path)?
637            .span
638    }
639
640    /// Create a checked source edit through the authored source attached to
641    /// this canonical program.
642    pub fn edit_source(
643        &self,
644        span: crate::source::Span,
645        replacement: impl Into<String>,
646    ) -> std::result::Result<crate::source::SourceEdit, crate::source::SourceEditError> {
647        self.source(span.file)
648            .ok_or(crate::source::SourceEditError::InvalidRange)?
649            .edit_span(span, replacement)
650    }
651
652    /// Validate the structural invariants of the canonical program.
653    ///
654    /// Settings are emission-checked too, so a program that would fail to
655    /// emit fails validation the same way.
656    pub fn validate(&self) -> std::result::Result<(), WorkshopError> {
657        let storage = self.to_wir()?;
658        storage
659            .validate()
660            .map_err(|error| WorkshopError::malformed(error.to_string(), error.span()))?;
661        if let Some(settings) = &self.settings
662            && let Some(error) = crate::settings::check_emission(settings).into_iter().next()
663        {
664            return Err(error);
665        }
666        Ok(())
667    }
668
669    /// Report every settings member the emission table rejects — plus, as
670    /// [`crate::settings::DiagnosticSeverity::Warning`] entries, carried
671    /// members close enough to a declared spelling to look like a
672    /// misspelling — each with its source span and, when exactly one
673    /// canonical spelling is close enough, the structured suggestion a
674    /// caller can apply. [`validate`] already fails on the first error;
675    /// warnings describe accepted members and never fail validation.
676    ///
677    /// [`validate`]: Program::validate
678    pub fn settings_diagnostics(&self) -> Vec<crate::settings::SettingsDiagnostic> {
679        self.settings
680            .as_ref()
681            .map(crate::settings::check_emission_diagnostics)
682            .unwrap_or_default()
683    }
684
685    /// Report constructs that are structurally preserved but not fully
686    /// understood by the canonical catalog.
687    ///
688    /// Residual inspection is defined over this public canonical model and is
689    /// independent from [`validate`](Self::validate): it may be used on
690    /// programs `validate` rejects, and an internal materialization failure
691    /// does not suppress observable residuals. Callers that also require
692    /// structural validity call `validate` separately; this inventory does
693    /// not report validation errors.
694    pub fn semantic_issues(
695        &self,
696        catalog: &crate::catalog::Catalog,
697    ) -> Vec<crate::rules::SemanticIssue> {
698        crate::analysis::semantic::inspect(self, catalog)
699    }
700
701    /// Render the program through the canonical Workshop debug representation.
702    pub fn dump(&self) -> String {
703        self.to_wir().map_or_else(
704            |error| format!("invalid program: {error}"),
705            |program| program.dump(),
706        )
707    }
708
709    fn rule_record(&self, rule: usize) -> Option<&RuleProvenance> {
710        let recorded = &self.provenance.as_deref()?.rules;
711        if recorded.len() != self.rules.len() {
712            return None;
713        }
714        recorded.get(rule)
715    }
716
717    fn rule_provenance(&self, rule: usize) -> Option<&RuleProvenance> {
718        self.rule_record(rule)
719            .filter(|record| record.identity == rule_identity(&self.rules[rule]))
720    }
721
722    fn action_record(&self, rule: usize, action: usize) -> Option<&ActionProvenance> {
723        let recorded = self.rule_record(rule)?;
724        if recorded.actions.len() != self.rules[rule].actions.len() {
725            return None;
726        }
727        recorded.actions.get(action)
728    }
729
730    fn action_provenance(&self, rule: usize, action: usize) -> Option<&ActionProvenance> {
731        self.action_record(rule, action)
732            .filter(|record| record.identity == action_identity(&self.rules[rule].actions[action]))
733    }
734
735    fn argument_record(
736        &self,
737        rule: usize,
738        action: usize,
739        argument: usize,
740    ) -> Option<&ValueProvenance> {
741        let recorded = self.action_record(rule, action)?;
742        let values = action_argument_values(&self.rules[rule].actions[action]);
743        if recorded.arguments.len() != values.len() {
744            return None;
745        }
746        recorded.arguments.get(argument)
747    }
748
749    fn action_argument_provenance(
750        &self,
751        rule: usize,
752        action: usize,
753        argument: usize,
754    ) -> Option<&ValueProvenance> {
755        let record = self.argument_record(rule, action, argument)?;
756        let values = action_argument_values(&self.rules[rule].actions[action]);
757        (record.identity == value_identity(values[argument])).then_some(record)
758    }
759
760    fn condition_record(&self, rule: usize, condition: usize) -> Option<&ValueProvenance> {
761        let recorded = self.rule_record(rule)?;
762        if recorded.conditions.len() != self.rules[rule].conditions.len() {
763            return None;
764        }
765        recorded.conditions.get(condition)
766    }
767
768    fn condition_provenance(&self, rule: usize, condition: usize) -> Option<&ValueProvenance> {
769        self.condition_record(rule, condition).filter(|record| {
770            record.identity == condition_identity(&self.rules[rule].conditions[condition])
771        })
772    }
773
774    fn declaration_name_span<T: std::fmt::Debug>(
775        &self,
776        recorded: impl Fn(&ProgramProvenance) -> &[DeclarationProvenance],
777        nodes: &[T],
778        position: usize,
779    ) -> Option<Span> {
780        let declaration = self.declaration_provenance(recorded, nodes, position);
781        declaration.name_span.or(declaration.span)
782    }
783
784    fn declaration_provenance<T: std::fmt::Debug>(
785        &self,
786        recorded: impl Fn(&ProgramProvenance) -> &[DeclarationProvenance],
787        nodes: &[T],
788        position: usize,
789    ) -> DeclarationProvenance {
790        self.provenance
791            .as_deref()
792            .map(recorded)
793            .filter(|recorded| recorded.len() == nodes.len())
794            .and_then(|recorded| recorded.get(position).zip(nodes.get(position)))
795            .filter(|(record, node)| record.identity == declaration_identity(node))
796            .map(|(record, _)| *record)
797            .unwrap_or_default()
798    }
799
800    fn validate_span(&self, span: Option<Span>) -> std::result::Result<(), SourceMappingError> {
801        let Some(span) = span else {
802            return Ok(());
803        };
804        if !span.is_valid() {
805            return Err(SourceMappingError::InvalidSpan(span));
806        }
807        if self.files.get(span.file.index()).is_none() {
808            return Err(SourceMappingError::UnknownFile(span.file));
809        }
810        Ok(())
811    }
812
813    fn provenance_mut(&mut self) -> &mut ProgramProvenance {
814        self.provenance
815            .get_or_insert_with(|| Box::new(ProgramProvenance::default()))
816            .as_mut()
817    }
818
819    fn rule_provenance_mut(
820        &mut self,
821        rule: usize,
822    ) -> std::result::Result<&mut RuleProvenance, SourceMappingError> {
823        if rule >= self.rules.len() {
824            return Err(SourceMappingError::InvalidRule(rule));
825        }
826        let identity = rule_identity(&self.rules[rule]);
827        let rule_count = self.rules.len();
828        let provenance = self.provenance_mut();
829        fit(&mut provenance.rules, rule_count);
830        let record = &mut provenance.rules[rule];
831        record.reseat(identity);
832        Ok(record)
833    }
834
835    fn action_provenance_mut(
836        &mut self,
837        rule: usize,
838        action: usize,
839    ) -> std::result::Result<&mut ActionProvenance, SourceMappingError> {
840        let public = self
841            .rules
842            .get(rule)
843            .ok_or(SourceMappingError::InvalidRule(rule))?;
844        let action_count = public.actions.len();
845        if action >= action_count {
846            return Err(SourceMappingError::InvalidAction { rule, action });
847        }
848        let identity = action_identity(&public.actions[action]);
849        let rule_data = self.rule_provenance_mut(rule)?;
850        fit(&mut rule_data.actions, action_count);
851        let record = &mut rule_data.actions[action];
852        record.reseat(identity);
853        Ok(record)
854    }
855
856    fn record_identities(&mut self) {
857        let Some(mut provenance) = self.provenance.take() else {
858            return;
859        };
860        identity::record_identities(&mut provenance, self);
861        self.provenance = Some(provenance);
862    }
863}
864
865fn value_provenance_at<'a>(
866    record: &'a ValueProvenance,
867    value: &'a Value,
868    path: &[usize],
869    root_identity: NodeIdentity,
870) -> Option<&'a ValueProvenance> {
871    let mut record = record;
872    let mut value = value;
873    let mut identity = root_identity;
874    for &index in path {
875        let children = value_children(value);
876        if record.children.len() != children.len() {
877            return None;
878        }
879        record = record.children.get(index)?;
880        value = children[index];
881        identity = value_identity(value);
882    }
883    (record.identity == identity).then_some(record)
884}
885
886fn fit<T: Default>(items: &mut Vec<T>, len: usize) {
887    items.truncate(len);
888    items.resize_with(len, T::default);
889}
890
891#[derive(Copy, Clone)]
892enum DeclarationTable {
893    GlobalVariables,
894    PlayerVariables,
895    Subroutines,
896}
897
898impl DeclarationTable {
899    fn count(self, program: &Program) -> usize {
900        match self {
901            Self::GlobalVariables => program.global_variables.len(),
902            Self::PlayerVariables => program.player_variables.len(),
903            Self::Subroutines => program.subroutines.len(),
904        }
905    }
906
907    fn invalid(self, index: usize) -> SourceMappingError {
908        match self {
909            Self::GlobalVariables => SourceMappingError::InvalidGlobalVariable(index),
910            Self::PlayerVariables => SourceMappingError::InvalidPlayerVariable(index),
911            Self::Subroutines => SourceMappingError::InvalidSubroutine(index),
912        }
913    }
914
915    fn identity(self, program: &Program, index: usize) -> NodeIdentity {
916        match self {
917            Self::GlobalVariables => declaration_identity(&program.global_variables[index]),
918            Self::PlayerVariables => declaration_identity(&program.player_variables[index]),
919            Self::Subroutines => declaration_identity(&program.subroutines[index]),
920        }
921    }
922
923    fn slots(self, provenance: &mut ProgramProvenance) -> &mut Vec<DeclarationProvenance> {
924        match self {
925            Self::GlobalVariables => &mut provenance.global_variables,
926            Self::PlayerVariables => &mut provenance.player_variables,
927            Self::Subroutines => &mut provenance.subroutines,
928        }
929    }
930}
931
932/// A Workshop global or player variable declaration.
933#[derive(Debug, Clone, PartialEq, Eq)]
934pub struct Variable {
935    pub name: String,
936    /// The raw Workshop declaration index, when the declaration has one.
937    pub index: Option<u32>,
938}
939
940impl Variable {
941    pub fn new(name: impl Into<String>) -> Self {
942        Self {
943            name: name.into(),
944            index: None,
945        }
946    }
947
948    pub fn with_index(name: impl Into<String>, index: u32) -> Self {
949        Self {
950            name: name.into(),
951            index: Some(index),
952        }
953    }
954}
955
956/// A Workshop subroutine declaration.
957#[derive(Debug, Clone, PartialEq, Eq)]
958pub struct Subroutine {
959    pub name: String,
960    /// The raw Workshop declaration index, when the declaration has one.
961    pub index: Option<u32>,
962}
963
964impl Subroutine {
965    pub fn new(name: impl Into<String>) -> Self {
966        Self {
967            name: name.into(),
968            index: None,
969        }
970    }
971
972    pub fn with_index(name: impl Into<String>, index: u32) -> Self {
973        Self {
974            name: name.into(),
975            index: Some(index),
976        }
977    }
978}
979
980/// A Workshop rule with explicit conditions and a linear action stream.
981#[derive(Debug, Clone)]
982#[non_exhaustive]
983pub struct Rule {
984    pub name: String,
985    pub disabled: bool,
986    pub event: Event,
987    pub conditions: Vec<Condition>,
988    pub actions: Vec<Action>,
989}
990
991impl Rule {
992    pub fn new(name: impl Into<String>, event: Event) -> Self {
993        Self {
994            name: name.into(),
995            disabled: false,
996            event,
997            conditions: Vec::new(),
998            actions: Vec::new(),
999        }
1000    }
1001
1002    pub fn condition(mut self, condition: impl Into<Condition>) -> Self {
1003        self.conditions.push(condition.into());
1004        self
1005    }
1006
1007    pub fn action(mut self, action: Action) -> Self {
1008        self.actions.push(action);
1009        self
1010    }
1011}
1012
1013/// A rule condition. Conditions remain distinct from general value expressions.
1014#[derive(Debug, Clone)]
1015#[non_exhaustive]
1016pub struct Condition {
1017    pub value: Value,
1018    pub disabled: bool,
1019}
1020
1021impl Condition {
1022    pub fn new(value: Value) -> Self {
1023        Self {
1024            value,
1025            disabled: false,
1026        }
1027    }
1028
1029    pub fn disabled(value: Value) -> Self {
1030        Self {
1031            value,
1032            disabled: true,
1033        }
1034    }
1035}
1036
1037impl From<Value> for Condition {
1038    fn from(value: Value) -> Self {
1039        Self::new(value)
1040    }
1041}
1042
1043pub(crate) fn action_argument_values(action: &Action) -> Vec<&Value> {
1044    match action {
1045        Action::SetGlobalVariable { value, .. } | Action::ModifyGlobalVariable { value, .. } => {
1046            vec![value]
1047        }
1048        Action::SetPlayerVariable { player, value, .. }
1049        | Action::ModifyPlayerVariable { player, value, .. } => vec![player, value],
1050        Action::AssignMember { target, value, .. } => vec![target, value],
1051        Action::If { condition } | Action::ElseIf { condition } | Action::While { condition } => {
1052            vec![condition]
1053        }
1054        Action::ForGlobalVariable {
1055            start, stop, step, ..
1056        } => vec![start, stop, step],
1057        Action::ForPlayerVariable {
1058            player,
1059            start,
1060            stop,
1061            step,
1062            ..
1063        } => vec![player, start, stop, step],
1064        Action::Call { args, .. } => args.iter().collect(),
1065        Action::CallSubroutine { .. } | Action::Else | Action::End => Vec::new(),
1066        Action::Disabled { action } => action_argument_values(action),
1067    }
1068}
1069
1070pub(crate) fn value_children(value: &Value) -> Vec<&Value> {
1071    match value {
1072        Value::Array(values) => values.iter().collect(),
1073        Value::Vector { x, y, z } => vec![x.as_ref(), y.as_ref(), z.as_ref()],
1074        Value::PlayerVariable { player, .. } => vec![player.as_ref()],
1075        Value::Call { args, .. } => args.iter().collect(),
1076        _ => Vec::new(),
1077    }
1078}
1079
1080/// A Workshop event identity and its native filters.
1081#[derive(Debug, Clone, PartialEq, Eq)]
1082pub enum Event {
1083    Global,
1084    EachPlayer,
1085    EachPlayerWithFilters {
1086        team: EventTeam,
1087        target: EventTarget,
1088    },
1089    Player {
1090        kind: PlayerEventKind,
1091        team: EventTeam,
1092        target: EventTarget,
1093    },
1094    Subroutine(String),
1095}
1096
1097impl Event {
1098    /// The canonical catalog event id (`global`, `eachPlayer`, `subroutine`,
1099    /// or a [`PlayerEventKind`] catalog id) this event declares. A
1100    /// `Subroutine` event's declared name is data, not part of the id.
1101    /// Public since `rule-content-v1` names events by this id.
1102    pub fn catalog_id(&self) -> &'static str {
1103        match self {
1104            Self::Global => "global",
1105            Self::EachPlayer | Self::EachPlayerWithFilters { .. } => "eachPlayer",
1106            Self::Player { kind, .. } => kind.catalog_id(),
1107            Self::Subroutine(_) => "subroutine",
1108        }
1109    }
1110}
1111
1112/// A Workshop action line. Control flow is represented in the same order as
1113/// the Workshop source, including its explicit `End` lines.
1114#[derive(Debug, Clone)]
1115pub enum Action {
1116    SetGlobalVariable {
1117        variable: String,
1118        value: Value,
1119    },
1120    ModifyGlobalVariable {
1121        variable: String,
1122        op: ModifyOp,
1123        value: Value,
1124    },
1125    SetPlayerVariable {
1126        player: Value,
1127        variable: String,
1128        value: Value,
1129    },
1130    ModifyPlayerVariable {
1131        player: Value,
1132        variable: String,
1133        op: ModifyOp,
1134        value: Value,
1135    },
1136    AssignMember {
1137        target: Value,
1138        op: Option<ModifyOp>,
1139        value: Value,
1140    },
1141    CallSubroutine {
1142        subroutine: String,
1143    },
1144    If {
1145        condition: Value,
1146    },
1147    ElseIf {
1148        condition: Value,
1149    },
1150    Else,
1151    While {
1152        condition: Value,
1153    },
1154    ForGlobalVariable {
1155        variable: String,
1156        start: Value,
1157        stop: Value,
1158        step: Value,
1159    },
1160    ForPlayerVariable {
1161        player: Value,
1162        variable: String,
1163        start: Value,
1164        stop: Value,
1165        step: Value,
1166    },
1167    End,
1168    Disabled {
1169        action: Box<Action>,
1170    },
1171    Call {
1172        name: String,
1173        args: Vec<Value>,
1174    },
1175}
1176
1177impl Action {
1178    /// Mark an action as disabled.
1179    pub fn disabled(action: Action) -> Self {
1180        Self::Disabled {
1181            action: Box::new(action),
1182        }
1183    }
1184
1185    /// Construct a dynamic action call by canonical Workshop id.
1186    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Value>) -> Self {
1187        Self::Call {
1188            name: name.into(),
1189            args: args.into_iter().collect(),
1190        }
1191    }
1192}
1193
1194/// A composable Workshop value expression.
1195#[derive(Debug, Clone)]
1196pub enum Value {
1197    Number(f64),
1198    String(String),
1199    LocalizedString(String),
1200    Bool(bool),
1201    Null,
1202    Array(Vec<Value>),
1203    Vector {
1204        x: Box<Value>,
1205        y: Box<Value>,
1206        z: Box<Value>,
1207    },
1208    Enum {
1209        value_type: String,
1210        value: String,
1211    },
1212    GlobalVariable(String),
1213    PlayerVariable {
1214        player: Box<Value>,
1215        variable: String,
1216    },
1217    Subroutine(String),
1218    EventPlayer,
1219    Call {
1220        name: String,
1221        args: Vec<Value>,
1222    },
1223}
1224
1225impl Value {
1226    /// Construct a numeric Workshop literal.
1227    pub fn number(value: f64) -> Self {
1228        Self::Number(value)
1229    }
1230
1231    /// Construct a custom Workshop string literal.
1232    pub fn string(value: impl Into<String>) -> Self {
1233        Self::String(value.into())
1234    }
1235
1236    pub fn global_variable(name: impl Into<String>) -> Self {
1237        Self::GlobalVariable(name.into())
1238    }
1239
1240    pub fn player_variable(player: Value, name: impl Into<String>) -> Self {
1241        Self::PlayerVariable {
1242            player: Box::new(player),
1243            variable: name.into(),
1244        }
1245    }
1246
1247    /// Construct a dynamic value call by canonical Workshop id.
1248    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Value>) -> Self {
1249        Self::Call {
1250            name: name.into(),
1251            args: args.into_iter().collect(),
1252        }
1253    }
1254}
1255
1256impl From<bool> for Value {
1257    fn from(value: bool) -> Self {
1258        Self::Bool(value)
1259    }
1260}
1261
1262impl From<f64> for Value {
1263    fn from(value: f64) -> Self {
1264        Self::Number(value)
1265    }
1266}
1267
1268impl From<f32> for Value {
1269    fn from(value: f32) -> Self {
1270        Self::Number(f64::from(value))
1271    }
1272}
1273
1274macro_rules! impl_integer_value {
1275    ($($type:ty),+ $(,)?) => {
1276        $(
1277            impl From<$type> for Value {
1278                fn from(value: $type) -> Self {
1279                    Self::Number(value as f64)
1280                }
1281            }
1282        )+
1283    };
1284}
1285
1286impl_integer_value!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
1287
1288impl From<String> for Value {
1289    fn from(value: String) -> Self {
1290        Self::String(value)
1291    }
1292}
1293
1294impl From<&str> for Value {
1295    fn from(value: &str) -> Self {
1296        Self::String(value.to_string())
1297    }
1298}
1299
1300impl<T: Into<Value>> From<Vec<T>> for Value {
1301    fn from(values: Vec<T>) -> Self {
1302        Self::Array(values.into_iter().map(Into::into).collect())
1303    }
1304}
1305
1306impl<T: Into<Value>, const N: usize> From<[T; N]> for Value {
1307    fn from(values: [T; N]) -> Self {
1308        Self::Array(values.into_iter().map(Into::into).collect())
1309    }
1310}