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