Skip to main content

workshop_rs/
program.rs

1//! Canonical Workshop program concepts.
2
3use std::collections::HashMap;
4
5use crate::core::error::{Result, WorkshopError};
6use crate::settings::Settings;
7use crate::source::{FileId, SourceDocument, SourceFile, Span};
8use crate::wir;
9
10mod source_map;
11pub use source_map::{MAPPED_TEXT_V1, MappedText, SourceMap, SourceMapError, TEXT_V1};
12
13/// A complete Workshop program built from Workshop concepts.
14#[derive(Debug, Clone, Default)]
15pub struct Program {
16    pub settings: Option<Settings>,
17    pub global_variables: Vec<Variable>,
18    pub player_variables: Vec<Variable>,
19    pub subroutines: Vec<Subroutine>,
20    pub rules: Vec<Rule>,
21    files: Vec<SourceFile>,
22    provenance: Option<Box<ProgramProvenance>>,
23}
24
25#[derive(Debug, Clone, Default)]
26struct ProgramProvenance {
27    global_variables: Vec<DeclarationProvenance>,
28    player_variables: Vec<DeclarationProvenance>,
29    subroutines: Vec<DeclarationProvenance>,
30    rules: Vec<RuleProvenance>,
31}
32
33#[derive(Debug, Clone, Copy, Default)]
34struct DeclarationProvenance {
35    span: Option<Span>,
36    name_span: Option<Span>,
37}
38
39#[derive(Debug, Clone, Default)]
40struct RuleProvenance {
41    span: Option<crate::source::Span>,
42    conditions: Vec<Option<crate::source::Span>>,
43    actions: Vec<ActionProvenance>,
44}
45
46#[derive(Debug, Clone, Default)]
47struct ActionProvenance {
48    span: Option<Span>,
49    arguments: Vec<Option<Span>>,
50}
51
52/// A failure while attaching source mappings to a public [`Program`].
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum SourceMappingError {
56    UnknownFile(FileId),
57    InvalidSpan(Span),
58    InvalidRule(usize),
59    InvalidCondition {
60        rule: usize,
61        condition: usize,
62    },
63    InvalidAction {
64        rule: usize,
65        action: usize,
66    },
67    InvalidActionArgument {
68        rule: usize,
69        action: usize,
70        argument: usize,
71    },
72    InvalidGlobalVariable(usize),
73    InvalidPlayerVariable(usize),
74    InvalidSubroutine(usize),
75}
76
77impl std::fmt::Display for SourceMappingError {
78    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::UnknownFile(file) => {
81                write!(formatter, "source span references unknown file {file}")
82            }
83            Self::InvalidSpan(span) => write!(formatter, "invalid source span {span:?}"),
84            Self::InvalidRule(rule) => write!(formatter, "invalid rule index {rule}"),
85            Self::InvalidCondition { rule, condition } => {
86                write!(
87                    formatter,
88                    "invalid condition index {condition} in rule {rule}"
89                )
90            }
91            Self::InvalidAction { rule, action } => {
92                write!(formatter, "invalid action index {action} in rule {rule}")
93            }
94            Self::InvalidActionArgument {
95                rule,
96                action,
97                argument,
98            } => write!(
99                formatter,
100                "invalid argument index {argument} in action {action} of rule {rule}"
101            ),
102            Self::InvalidGlobalVariable(variable) => {
103                write!(formatter, "invalid global variable index {variable}")
104            }
105            Self::InvalidPlayerVariable(variable) => {
106                write!(formatter, "invalid player variable index {variable}")
107            }
108            Self::InvalidSubroutine(subroutine) => {
109                write!(formatter, "invalid subroutine index {subroutine}")
110            }
111        }
112    }
113}
114
115impl std::error::Error for SourceMappingError {}
116
117impl Program {
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Register a source file and return its public file identity.
123    pub fn add_file(&mut self, mut file: SourceFile) -> FileId {
124        let id = FileId::from_index(self.files.len());
125        file.bind_file(id);
126        self.files.push(file);
127        id
128    }
129
130    pub fn global_variable(&mut self, variable: Variable) -> &mut Self {
131        self.global_variables.push(variable);
132        self
133    }
134
135    pub fn player_variable(&mut self, variable: Variable) -> &mut Self {
136        self.player_variables.push(variable);
137        self
138    }
139
140    pub fn subroutine(&mut self, subroutine: Subroutine) -> &mut Self {
141        self.subroutines.push(subroutine);
142        self
143    }
144
145    pub fn rule(&mut self, rule: Rule) -> &mut Self {
146        self.rules.push(rule);
147        self
148    }
149
150    /// Return the retained source document for a parsed file.
151    pub fn source(&self, file: FileId) -> Option<&SourceDocument> {
152        self.files.get(file.index()).and_then(SourceFile::source)
153    }
154
155    /// Attach the authored span of a public rule.
156    pub fn set_rule_span(
157        &mut self,
158        rule: usize,
159        span: Option<Span>,
160    ) -> std::result::Result<(), SourceMappingError> {
161        self.validate_span(span)?;
162        self.rule_provenance_mut(rule)?.span = span;
163        Ok(())
164    }
165
166    /// Attach the authored span of a public rule condition value.
167    pub fn set_condition_span(
168        &mut self,
169        rule: usize,
170        condition: usize,
171        span: Option<Span>,
172    ) -> std::result::Result<(), SourceMappingError> {
173        self.validate_span(span)?;
174        let condition_count = self
175            .rules
176            .get(rule)
177            .ok_or(SourceMappingError::InvalidRule(rule))?
178            .conditions
179            .len();
180        if condition >= condition_count {
181            return Err(SourceMappingError::InvalidCondition { rule, condition });
182        }
183        let rule_data = self.rule_provenance_mut(rule)?;
184        fit(&mut rule_data.conditions, condition_count);
185        rule_data.conditions[condition] = span;
186        Ok(())
187    }
188
189    /// Attach the authored span of a public action in its linear rule order.
190    pub fn set_action_span(
191        &mut self,
192        rule: usize,
193        action: usize,
194        span: Option<Span>,
195    ) -> std::result::Result<(), SourceMappingError> {
196        self.validate_span(span)?;
197        let action_count = self
198            .rules
199            .get(rule)
200            .ok_or(SourceMappingError::InvalidRule(rule))?
201            .actions
202            .len();
203        if action >= action_count {
204            return Err(SourceMappingError::InvalidAction { rule, action });
205        }
206        let rule_data = self.rule_provenance_mut(rule)?;
207        fit(&mut rule_data.actions, action_count);
208        rule_data.actions[action].span = span;
209        Ok(())
210    }
211
212    /// Attach the authored span of a direct value argument of a public action.
213    pub fn set_action_argument_span(
214        &mut self,
215        rule: usize,
216        action: usize,
217        argument: usize,
218        span: Option<Span>,
219    ) -> std::result::Result<(), SourceMappingError> {
220        self.validate_span(span)?;
221        let action_value = self
222            .rules
223            .get(rule)
224            .ok_or(SourceMappingError::InvalidRule(rule))?
225            .actions
226            .get(action)
227            .ok_or(SourceMappingError::InvalidAction { rule, action })?;
228        let argument_count = action_argument_count(action_value);
229        if argument >= argument_count {
230            return Err(SourceMappingError::InvalidActionArgument {
231                rule,
232                action,
233                argument,
234            });
235        }
236        let action_data = self.action_provenance_mut(rule, action)?;
237        action_data.arguments.resize(argument + 1, None);
238        action_data.arguments[argument] = span;
239        Ok(())
240    }
241
242    /// Attach the authored and identifier spans of a global variable.
243    pub fn set_global_variable_spans(
244        &mut self,
245        variable: usize,
246        span: Option<Span>,
247        name_span: Option<Span>,
248    ) -> std::result::Result<(), SourceMappingError> {
249        self.validate_span(span)?;
250        self.validate_span(name_span)?;
251        if variable >= self.global_variables.len() {
252            return Err(SourceMappingError::InvalidGlobalVariable(variable));
253        }
254        let variable_count = self.global_variables.len();
255        let provenance = self.provenance_mut();
256        fit(&mut provenance.global_variables, variable_count);
257        provenance.global_variables[variable] = DeclarationProvenance { span, name_span };
258        Ok(())
259    }
260
261    /// Attach the authored and identifier spans of a player variable.
262    pub fn set_player_variable_spans(
263        &mut self,
264        variable: usize,
265        span: Option<Span>,
266        name_span: Option<Span>,
267    ) -> std::result::Result<(), SourceMappingError> {
268        self.validate_span(span)?;
269        self.validate_span(name_span)?;
270        if variable >= self.player_variables.len() {
271            return Err(SourceMappingError::InvalidPlayerVariable(variable));
272        }
273        let variable_count = self.player_variables.len();
274        let provenance = self.provenance_mut();
275        fit(&mut provenance.player_variables, variable_count);
276        provenance.player_variables[variable] = DeclarationProvenance { span, name_span };
277        Ok(())
278    }
279
280    /// Attach the authored and identifier spans of a subroutine.
281    pub fn set_subroutine_spans(
282        &mut self,
283        subroutine: usize,
284        span: Option<Span>,
285        name_span: Option<Span>,
286    ) -> std::result::Result<(), SourceMappingError> {
287        self.validate_span(span)?;
288        self.validate_span(name_span)?;
289        if subroutine >= self.subroutines.len() {
290            return Err(SourceMappingError::InvalidSubroutine(subroutine));
291        }
292        let subroutine_count = self.subroutines.len();
293        let provenance = self.provenance_mut();
294        fit(&mut provenance.subroutines, subroutine_count);
295        provenance.subroutines[subroutine] = DeclarationProvenance { span, name_span };
296        Ok(())
297    }
298
299    /// Return the authored span of a public rule, when source metadata exists.
300    ///
301    /// Attached mappings record the program shape they were attached to. Every
302    /// span accessor returns `None` once the public rules, conditions, or
303    /// actions have been inserted or removed since the mapping was attached.
304    pub fn rule_span(&self, rule: usize) -> Option<crate::source::Span> {
305        self.rule_provenance(rule)?.span
306    }
307
308    /// Return the authored span of a public rule condition value.
309    pub fn condition_span(&self, rule: usize, condition: usize) -> Option<crate::source::Span> {
310        let recorded = self.rule_provenance(rule)?;
311        if recorded.conditions.len() != self.rules[rule].conditions.len() {
312            return None;
313        }
314        recorded.conditions.get(condition).copied().flatten()
315    }
316
317    /// Return the authored span of a public action in its linear rule order.
318    pub fn action_span(&self, rule: usize, action: usize) -> Option<crate::source::Span> {
319        self.action_provenance(rule, action)?.span
320    }
321
322    /// Return the authored span of a direct value argument of a public action.
323    pub fn action_argument_span(
324        &self,
325        rule: usize,
326        action: usize,
327        argument: usize,
328    ) -> Option<crate::source::Span> {
329        self.action_provenance(rule, action)?
330            .arguments
331            .get(argument)
332            .copied()
333            .flatten()
334    }
335
336    /// Create a checked source edit through the authored source attached to
337    /// this canonical program.
338    pub fn edit_source(
339        &self,
340        span: crate::source::Span,
341        replacement: impl Into<String>,
342    ) -> std::result::Result<crate::source::SourceEdit, crate::source::SourceEditError> {
343        self.source(span.file)
344            .ok_or(crate::source::SourceEditError::InvalidRange)?
345            .edit_span(span, replacement)
346    }
347
348    /// Validate the structural invariants of the canonical program.
349    pub fn validate(&self) -> std::result::Result<(), WorkshopError> {
350        let storage = self.to_wir()?;
351        storage
352            .validate()
353            .map_err(|error| WorkshopError::Malformed {
354                message: error.to_string(),
355                span: error.span(),
356            })
357    }
358
359    /// Report constructs that are structurally preserved but not fully
360    /// understood by the canonical catalog.
361    pub fn semantic_issues(
362        &self,
363        catalog: &crate::catalog::Catalog,
364    ) -> Vec<crate::rules::SemanticIssue> {
365        crate::analysis::semantic::inspect(self, catalog)
366    }
367
368    /// Render the program through the canonical Workshop debug representation.
369    pub fn dump(&self) -> String {
370        self.to_wir().map_or_else(
371            |error| format!("invalid program: {error}"),
372            |program| program.dump(),
373        )
374    }
375
376    fn rule_provenance(&self, rule: usize) -> Option<&RuleProvenance> {
377        let recorded = &self.provenance.as_deref()?.rules;
378        if recorded.len() != self.rules.len() {
379            return None;
380        }
381        recorded.get(rule)
382    }
383
384    fn action_provenance(&self, rule: usize, action: usize) -> Option<&ActionProvenance> {
385        let recorded = self.rule_provenance(rule)?;
386        if recorded.actions.len() != self.rules[rule].actions.len() {
387            return None;
388        }
389        recorded.actions.get(action)
390    }
391
392    fn declaration_provenance(
393        &self,
394        recorded: impl Fn(&ProgramProvenance) -> &[DeclarationProvenance],
395        count: usize,
396        position: usize,
397    ) -> DeclarationProvenance {
398        self.provenance
399            .as_deref()
400            .map(recorded)
401            .filter(|recorded| recorded.len() == count)
402            .and_then(|recorded| recorded.get(position))
403            .copied()
404            .unwrap_or_default()
405    }
406
407    fn validate_span(&self, span: Option<Span>) -> std::result::Result<(), SourceMappingError> {
408        let Some(span) = span else {
409            return Ok(());
410        };
411        if !span.is_valid() {
412            return Err(SourceMappingError::InvalidSpan(span));
413        }
414        if self.files.get(span.file.index()).is_none() {
415            return Err(SourceMappingError::UnknownFile(span.file));
416        }
417        Ok(())
418    }
419
420    fn provenance_mut(&mut self) -> &mut ProgramProvenance {
421        self.provenance
422            .get_or_insert_with(|| Box::new(ProgramProvenance::default()))
423            .as_mut()
424    }
425
426    fn rule_provenance_mut(
427        &mut self,
428        rule: usize,
429    ) -> std::result::Result<&mut RuleProvenance, SourceMappingError> {
430        if rule >= self.rules.len() {
431            return Err(SourceMappingError::InvalidRule(rule));
432        }
433        let rule_count = self.rules.len();
434        let provenance = self.provenance_mut();
435        fit(&mut provenance.rules, rule_count);
436        Ok(&mut provenance.rules[rule])
437    }
438
439    fn action_provenance_mut(
440        &mut self,
441        rule: usize,
442        action: usize,
443    ) -> std::result::Result<&mut ActionProvenance, SourceMappingError> {
444        let action_count = self
445            .rules
446            .get(rule)
447            .ok_or(SourceMappingError::InvalidRule(rule))?
448            .actions
449            .len();
450        if action >= action_count {
451            return Err(SourceMappingError::InvalidAction { rule, action });
452        }
453        let rule_data = self.rule_provenance_mut(rule)?;
454        fit(&mut rule_data.actions, action_count);
455        Ok(&mut rule_data.actions[action])
456    }
457
458    pub(crate) fn from_wir(storage: wir::Program) -> Result<Self> {
459        let mut program = Self {
460            settings: storage.settings.clone(),
461            global_variables: storage
462                .global_variables
463                .iter()
464                .map(|variable| Variable::with_index(variable.name.clone(), variable.index))
465                .collect(),
466            player_variables: storage
467                .player_variables
468                .iter()
469                .map(|variable| Variable::with_index(variable.name.clone(), variable.index))
470                .collect(),
471            subroutines: storage
472                .subroutines
473                .iter()
474                .map(|subroutine| Subroutine::with_index(subroutine.name.clone(), subroutine.index))
475                .collect(),
476            rules: Vec::with_capacity(storage.rules.len()),
477            files: storage.files.iter().cloned().collect(),
478            provenance: Some(Box::new(ProgramProvenance {
479                global_variables: storage
480                    .global_variables
481                    .iter()
482                    .map(|variable| DeclarationProvenance {
483                        span: variable.span,
484                        name_span: variable.name_span,
485                    })
486                    .collect(),
487                player_variables: storage
488                    .player_variables
489                    .iter()
490                    .map(|variable| DeclarationProvenance {
491                        span: variable.span,
492                        name_span: variable.name_span,
493                    })
494                    .collect(),
495                subroutines: storage
496                    .subroutines
497                    .iter()
498                    .map(|subroutine| DeclarationProvenance {
499                        span: subroutine.span,
500                        name_span: subroutine.name_span,
501                    })
502                    .collect(),
503                rules: Vec::with_capacity(storage.rules.len()),
504            })),
505        };
506        for rule in storage.rules.iter() {
507            let event = public_event(&storage, &rule.event)?;
508            let conditions = rule
509                .conditions
510                .iter()
511                .map(|condition| {
512                    Ok(Condition {
513                        value: public_value(&storage, condition.value)?,
514                        disabled: condition.disabled,
515                    })
516                })
517                .collect::<Result<Vec<_>>>()?;
518            let mut actions = Vec::new();
519            let mut action_provenance = Vec::new();
520            for action in &rule.actions {
521                public_actions(&storage, *action, &mut actions)?;
522                public_action_provenance(&storage, *action, &mut action_provenance)?;
523            }
524            program
525                .provenance
526                .as_mut()
527                .expect("parsed programs retain provenance")
528                .rules
529                .push(RuleProvenance {
530                    span: rule.span,
531                    conditions: rule
532                        .conditions
533                        .iter()
534                        .map(|condition| {
535                            storage
536                                .values
537                                .get(condition.value)
538                                .and_then(|value| value.span)
539                        })
540                        .collect(),
541                    actions: action_provenance,
542                });
543            program.rules.push(Rule {
544                name: rule.name.clone(),
545                disabled: rule.disabled,
546                event,
547                conditions,
548                actions,
549            });
550        }
551        Ok(program)
552    }
553
554    pub(crate) fn to_wir(&self) -> Result<wir::Program> {
555        let mut storage = wir::Program {
556            settings: self.settings.clone(),
557            ..Default::default()
558        };
559
560        for file in &self.files {
561            storage.add_file(file.clone());
562        }
563
564        let mut globals = HashMap::new();
565        for (position, variable) in self.global_variables.iter().enumerate() {
566            let declaration = self.declaration_provenance(
567                |provenance| &provenance.global_variables,
568                self.global_variables.len(),
569                position,
570            );
571            let id = storage.global_variables.push(wir::WorkshopVariable {
572                name: variable.name.clone(),
573                index: variable.index.unwrap_or(position as u32),
574                span: declaration.span,
575                name_span: declaration.name_span,
576            });
577            globals.insert(variable.name.clone(), id);
578        }
579        let mut players = HashMap::new();
580        for (position, variable) in self.player_variables.iter().enumerate() {
581            let declaration = self.declaration_provenance(
582                |provenance| &provenance.player_variables,
583                self.player_variables.len(),
584                position,
585            );
586            let id = storage.player_variables.push(wir::WorkshopVariable {
587                name: variable.name.clone(),
588                index: variable.index.unwrap_or(position as u32),
589                span: declaration.span,
590                name_span: declaration.name_span,
591            });
592            players.insert(variable.name.clone(), id);
593        }
594        let mut subroutines = HashMap::new();
595        for (position, subroutine) in self.subroutines.iter().enumerate() {
596            let declaration = self.declaration_provenance(
597                |provenance| &provenance.subroutines,
598                self.subroutines.len(),
599                position,
600            );
601            let id = storage.subroutines.push(wir::WorkshopSubroutine {
602                name: subroutine.name.clone(),
603                index: subroutine.index.unwrap_or(position as u32),
604                span: declaration.span,
605                name_span: declaration.name_span,
606            });
607            subroutines.insert(subroutine.name.clone(), id);
608        }
609
610        for (rule_index, rule) in self.rules.iter().enumerate() {
611            let event = wir_event(&rule.event, &subroutines)?;
612            let conditions = rule
613                .conditions
614                .iter()
615                .enumerate()
616                .map(|(condition_index, condition)| {
617                    wir_value(
618                        &condition.value,
619                        &mut storage,
620                        &globals,
621                        &players,
622                        &subroutines,
623                    )
624                    .inspect(|&value| {
625                        if let Some(span) = self.condition_span(rule_index, condition_index) {
626                            storage.values.get_mut(value).unwrap().span = Some(span);
627                        }
628                    })
629                    .map(|value| wir::Condition {
630                        value,
631                        disabled: condition.disabled,
632                    })
633                })
634                .collect::<Result<Vec<_>>>()?;
635            let mut actions = Vec::new();
636            let mut position = 0;
637            lower_actions(
638                &rule.actions,
639                &mut position,
640                &mut actions,
641                &mut storage,
642                &globals,
643                &players,
644                &subroutines,
645            )?;
646            if let Some(provenance) = self
647                .rule_provenance(rule_index)
648                .filter(|provenance| provenance.actions.len() == rule.actions.len())
649            {
650                let mut public_position = 0;
651                apply_action_provenance(
652                    &mut storage,
653                    &actions,
654                    &provenance.actions,
655                    &mut public_position,
656                )?;
657            }
658            if position != rule.actions.len() {
659                return Err(WorkshopError::Malformed {
660                    message: "unexpected control-flow terminator in rule actions".to_string(),
661                    span: None,
662                });
663            }
664            storage.rules.push(wir::Rule {
665                name: rule.name.clone(),
666                span: self.rule_span(rule_index),
667                name_span: None,
668                disabled: rule.disabled,
669                event,
670                conditions,
671                actions,
672            });
673        }
674        Ok(storage)
675    }
676}
677
678fn fit<T: Default>(items: &mut Vec<T>, len: usize) {
679    items.truncate(len);
680    items.resize_with(len, T::default);
681}
682
683fn public_event(storage: &wir::Program, event: &wir::Event) -> Result<Event> {
684    Ok(match event {
685        wir::Event::Global => Event::Global,
686        wir::Event::EachPlayer => Event::EachPlayer,
687        wir::Event::EachPlayerWithFilters { team, target } => Event::EachPlayerWithFilters {
688            team: public_team(*team),
689            target: public_target(target),
690        },
691        wir::Event::Player { kind, team, target } => Event::Player {
692            kind: public_player_event(*kind),
693            team: public_team(*team),
694            target: public_target(target),
695        },
696        wir::Event::Subroutine(id) => Event::Subroutine(
697            storage
698                .subroutines
699                .get(*id)
700                .ok_or_else(|| malformed_id("subroutine", id.index()))?
701                .name
702                .clone(),
703        ),
704    })
705}
706
707fn public_team(team: wir::EventTeam) -> EventTeam {
708    match team {
709        wir::EventTeam::All => EventTeam::All,
710        wir::EventTeam::Team1 => EventTeam::Team1,
711        wir::EventTeam::Team2 => EventTeam::Team2,
712    }
713}
714
715fn public_target(target: &wir::EventTarget) -> EventTarget {
716    match target {
717        wir::EventTarget::All => EventTarget::All,
718        wir::EventTarget::Slot(slot) => EventTarget::Slot(*slot),
719        wir::EventTarget::Hero(hero) => EventTarget::Hero(hero.clone()),
720    }
721}
722
723fn public_player_event(kind: wir::PlayerEventKind) -> PlayerEventKind {
724    match kind {
725        wir::PlayerEventKind::DealtDamage => PlayerEventKind::DealtDamage,
726        wir::PlayerEventKind::DealtFinalBlow => PlayerEventKind::DealtFinalBlow,
727        wir::PlayerEventKind::DealtHealing => PlayerEventKind::DealtHealing,
728        wir::PlayerEventKind::DealtKnockback => PlayerEventKind::DealtKnockback,
729        wir::PlayerEventKind::Died => PlayerEventKind::Died,
730        wir::PlayerEventKind::EarnedElimination => PlayerEventKind::EarnedElimination,
731        wir::PlayerEventKind::Joined => PlayerEventKind::Joined,
732        wir::PlayerEventKind::Left => PlayerEventKind::Left,
733        wir::PlayerEventKind::ReceivedHealing => PlayerEventKind::ReceivedHealing,
734        wir::PlayerEventKind::ReceivedKnockback => PlayerEventKind::ReceivedKnockback,
735        wir::PlayerEventKind::TookDamage => PlayerEventKind::TookDamage,
736    }
737}
738
739fn public_value(storage: &wir::Program, id: wir::ValueId) -> Result<Value> {
740    let node = storage
741        .values
742        .get(id)
743        .ok_or_else(|| malformed_id("value", id.index()))?;
744    Ok(match &node.value {
745        wir::Value::Number { value, .. } => Value::Number(*value),
746        wir::Value::String(value) => Value::String(value.clone()),
747        wir::Value::LocalizedString(value) => Value::LocalizedString(value.clone()),
748        wir::Value::Bool(value) => Value::Bool(*value),
749        wir::Value::Null => Value::Null,
750        wir::Value::Array(values) => Value::Array(
751            values
752                .iter()
753                .map(|value| public_value(storage, *value))
754                .collect::<Result<Vec<_>>>()?,
755        ),
756        wir::Value::Vector { x, y, z } => Value::Vector {
757            x: Box::new(public_value(storage, *x)?),
758            y: Box::new(public_value(storage, *y)?),
759            z: Box::new(public_value(storage, *z)?),
760        },
761        wir::Value::Enum { value_type, value } => Value::Enum {
762            value_type: value_type.clone(),
763            value: value.clone(),
764        },
765        wir::Value::GlobalVariable(id) => Value::GlobalVariable(
766            storage
767                .global_variables
768                .get(*id)
769                .ok_or_else(|| malformed_id("global variable", id.index()))?
770                .name
771                .clone(),
772        ),
773        wir::Value::PlayerVariable { player, variable } => Value::PlayerVariable {
774            player: Box::new(public_value(storage, *player)?),
775            variable: storage
776                .player_variables
777                .get(*variable)
778                .ok_or_else(|| malformed_id("player variable", variable.index()))?
779                .name
780                .clone(),
781        },
782        wir::Value::Subroutine(id) => Value::Subroutine(
783            storage
784                .subroutines
785                .get(*id)
786                .ok_or_else(|| malformed_id("subroutine", id.index()))?
787                .name
788                .clone(),
789        ),
790        wir::Value::EventPlayer => Value::EventPlayer,
791        wir::Value::Call { name, args } => Value::Call {
792            name: name.clone(),
793            args: args
794                .iter()
795                .map(|arg| public_value(storage, *arg))
796                .collect::<Result<Vec<_>>>()?,
797        },
798    })
799}
800
801fn public_actions(
802    storage: &wir::Program,
803    id: wir::ActionId,
804    output: &mut Vec<Action>,
805) -> Result<()> {
806    let action = storage
807        .actions
808        .get(id)
809        .ok_or_else(|| malformed_id("action", id.index()))?;
810    match action {
811        wir::Action::SetGlobalVariable {
812            variable, value, ..
813        } => output.push(Action::SetGlobalVariable {
814            variable: storage
815                .global_variables
816                .get(*variable)
817                .ok_or_else(|| malformed_id("global variable", variable.index()))?
818                .name
819                .clone(),
820            value: public_value(storage, *value)?,
821        }),
822        wir::Action::ModifyGlobalVariable {
823            variable,
824            op,
825            value,
826            ..
827        } => output.push(Action::ModifyGlobalVariable {
828            variable: storage
829                .global_variables
830                .get(*variable)
831                .ok_or_else(|| malformed_id("global variable", variable.index()))?
832                .name
833                .clone(),
834            op: public_modify(*op),
835            value: public_value(storage, *value)?,
836        }),
837        wir::Action::SetPlayerVariable {
838            player,
839            variable,
840            value,
841            ..
842        } => output.push(Action::SetPlayerVariable {
843            player: public_value(storage, *player)?,
844            variable: storage
845                .player_variables
846                .get(*variable)
847                .ok_or_else(|| malformed_id("player variable", variable.index()))?
848                .name
849                .clone(),
850            value: public_value(storage, *value)?,
851        }),
852        wir::Action::ModifyPlayerVariable {
853            player,
854            variable,
855            op,
856            value,
857            ..
858        } => output.push(Action::ModifyPlayerVariable {
859            player: public_value(storage, *player)?,
860            variable: storage
861                .player_variables
862                .get(*variable)
863                .ok_or_else(|| malformed_id("player variable", variable.index()))?
864                .name
865                .clone(),
866            op: public_modify(*op),
867            value: public_value(storage, *value)?,
868        }),
869        wir::Action::AssignMember {
870            target, op, value, ..
871        } => output.push(Action::AssignMember {
872            target: public_value(storage, *target)?,
873            op: op.map(public_modify),
874            value: public_value(storage, *value)?,
875        }),
876        wir::Action::CallSubroutine { subroutine, .. } => output.push(Action::CallSubroutine {
877            subroutine: storage
878                .subroutines
879                .get(*subroutine)
880                .ok_or_else(|| malformed_id("subroutine", subroutine.index()))?
881                .name
882                .clone(),
883        }),
884        wir::Action::If {
885            branches,
886            else_body,
887            ..
888        } => {
889            for (index, branch) in branches.iter().enumerate() {
890                output.push(if index == 0 {
891                    Action::If {
892                        condition: public_value(storage, branch.condition)?,
893                    }
894                } else {
895                    Action::ElseIf {
896                        condition: public_value(storage, branch.condition)?,
897                    }
898                });
899                for action in &branch.body {
900                    public_actions(storage, *action, output)?;
901                }
902            }
903            if let Some(body) = else_body {
904                output.push(Action::Else);
905                for action in body {
906                    public_actions(storage, *action, output)?;
907                }
908            }
909            output.push(Action::End);
910        }
911        wir::Action::While {
912            condition, body, ..
913        } => {
914            output.push(Action::While {
915                condition: public_value(storage, *condition)?,
916            });
917            for action in body {
918                public_actions(storage, *action, output)?;
919            }
920            output.push(Action::End);
921        }
922        wir::Action::ForGlobalVariable {
923            variable,
924            start,
925            stop,
926            step,
927            body,
928            ..
929        } => {
930            output.push(Action::ForGlobalVariable {
931                variable: storage
932                    .global_variables
933                    .get(*variable)
934                    .ok_or_else(|| malformed_id("global variable", variable.index()))?
935                    .name
936                    .clone(),
937                start: public_value(storage, *start)?,
938                stop: public_value(storage, *stop)?,
939                step: public_value(storage, *step)?,
940            });
941            for action in body {
942                public_actions(storage, *action, output)?;
943            }
944            output.push(Action::End);
945        }
946        wir::Action::ForPlayerVariable {
947            player,
948            variable,
949            start,
950            stop,
951            step,
952            body,
953            ..
954        } => {
955            output.push(Action::ForPlayerVariable {
956                player: public_value(storage, *player)?,
957                variable: storage
958                    .player_variables
959                    .get(*variable)
960                    .ok_or_else(|| malformed_id("player variable", variable.index()))?
961                    .name
962                    .clone(),
963                start: public_value(storage, *start)?,
964                stop: public_value(storage, *stop)?,
965                step: public_value(storage, *step)?,
966            });
967            for action in body {
968                public_actions(storage, *action, output)?;
969            }
970            output.push(Action::End);
971        }
972        wir::Action::Disabled { action, .. } => {
973            let first = output.len();
974            public_actions(storage, *action, output)?;
975            let inner = output.remove(first);
976            output.insert(first, Action::disabled(inner));
977        }
978        wir::Action::Call { name, args, .. } => output.push(Action::Call {
979            name: name.clone(),
980            args: args
981                .iter()
982                .map(|arg| public_value(storage, *arg))
983                .collect::<Result<Vec<_>>>()?,
984        }),
985    }
986    Ok(())
987}
988
989fn public_action_provenance(
990    storage: &wir::Program,
991    id: wir::ActionId,
992    output: &mut Vec<ActionProvenance>,
993) -> Result<()> {
994    let action = storage
995        .actions
996        .get(id)
997        .ok_or_else(|| malformed_id("action", id.index()))?;
998    let push = |output: &mut Vec<ActionProvenance>, arguments: &[wir::ValueId]| {
999        output.push(ActionProvenance {
1000            span: action.span(),
1001            arguments: arguments
1002                .iter()
1003                .map(|value| storage.values.get(*value).and_then(|value| value.span))
1004                .collect(),
1005        });
1006    };
1007    let push_without_span = |output: &mut Vec<ActionProvenance>, arguments: &[wir::ValueId]| {
1008        output.push(ActionProvenance {
1009            span: None,
1010            arguments: arguments
1011                .iter()
1012                .map(|value| storage.values.get(*value).and_then(|value| value.span))
1013                .collect(),
1014        });
1015    };
1016    match action {
1017        wir::Action::SetGlobalVariable { value, .. }
1018        | wir::Action::ModifyGlobalVariable { value, .. } => push(output, &[*value]),
1019        wir::Action::SetPlayerVariable { player, value, .. }
1020        | wir::Action::ModifyPlayerVariable { player, value, .. } => {
1021            push(output, &[*player, *value])
1022        }
1023        wir::Action::AssignMember { target, value, .. } => push(output, &[*target, *value]),
1024        wir::Action::CallSubroutine { .. } => push(output, &[]),
1025        wir::Action::If {
1026            branches,
1027            else_body,
1028            ..
1029        } => {
1030            for (index, branch) in branches.iter().enumerate() {
1031                if index == 0 {
1032                    push(output, &[branch.condition]);
1033                } else {
1034                    push_without_span(output, &[branch.condition]);
1035                }
1036                for action in &branch.body {
1037                    public_action_provenance(storage, *action, output)?;
1038                }
1039                if index + 1 == branches.len() && else_body.is_none() {
1040                    push_without_span(output, &[]);
1041                }
1042            }
1043            if let Some(body) = else_body {
1044                push_without_span(output, &[]);
1045                for action in body {
1046                    public_action_provenance(storage, *action, output)?;
1047                }
1048                push_without_span(output, &[]);
1049            }
1050        }
1051        wir::Action::While {
1052            condition, body, ..
1053        } => {
1054            push(output, &[*condition]);
1055            for action in body {
1056                public_action_provenance(storage, *action, output)?;
1057            }
1058            push_without_span(output, &[]);
1059        }
1060        wir::Action::ForGlobalVariable {
1061            start,
1062            stop,
1063            step,
1064            body,
1065            ..
1066        } => {
1067            push(output, &[*start, *stop, *step]);
1068            for action in body {
1069                public_action_provenance(storage, *action, output)?;
1070            }
1071            push_without_span(output, &[]);
1072        }
1073        wir::Action::ForPlayerVariable {
1074            player,
1075            start,
1076            stop,
1077            step,
1078            body,
1079            ..
1080        } => {
1081            push(output, &[*player, *start, *stop, *step]);
1082            for action in body {
1083                public_action_provenance(storage, *action, output)?;
1084            }
1085            push_without_span(output, &[]);
1086        }
1087        wir::Action::Disabled { action, .. } => {
1088            public_action_provenance(storage, *action, output)?;
1089        }
1090        wir::Action::Call { args, .. } => push(output, args),
1091    }
1092    Ok(())
1093}
1094
1095fn lower_actions(
1096    actions: &[Action],
1097    position: &mut usize,
1098    output: &mut Vec<wir::ActionId>,
1099    storage: &mut wir::Program,
1100    globals: &HashMap<String, wir::GlobalVarId>,
1101    players: &HashMap<String, wir::PlayerVarId>,
1102    subroutines: &HashMap<String, wir::SubroutineId>,
1103) -> Result<()> {
1104    while *position < actions.len() {
1105        let (disabled, current) = match &actions[*position] {
1106            Action::Disabled { action } => (true, action.as_ref()),
1107            action => (false, action),
1108        };
1109        if disabled
1110            && matches!(
1111                current,
1112                Action::ElseIf { .. } | Action::Else | Action::End | Action::Disabled { .. }
1113            )
1114        {
1115            return Err(WorkshopError::Unsupported {
1116                message: "the disabled modifier applies to a single executable action".to_string(),
1117                span: None,
1118            });
1119        }
1120        match current {
1121            Action::ElseIf { .. } | Action::Else | Action::End => return Ok(()),
1122            Action::If { condition } => {
1123                *position += 1;
1124                let mut branches = vec![wir::IfBranch {
1125                    condition: wir_value(condition, storage, globals, players, subroutines)?,
1126                    body: Vec::new(),
1127                }];
1128                lower_actions(
1129                    actions,
1130                    position,
1131                    &mut branches[0].body,
1132                    storage,
1133                    globals,
1134                    players,
1135                    subroutines,
1136                )?;
1137                while let Some(Action::ElseIf { condition }) = actions.get(*position) {
1138                    *position += 1;
1139                    let mut body = Vec::new();
1140                    lower_actions(
1141                        actions,
1142                        position,
1143                        &mut body,
1144                        storage,
1145                        globals,
1146                        players,
1147                        subroutines,
1148                    )?;
1149                    branches.push(wir::IfBranch {
1150                        condition: wir_value(condition, storage, globals, players, subroutines)?,
1151                        body,
1152                    });
1153                }
1154                let else_body = if matches!(actions.get(*position), Some(Action::Else)) {
1155                    *position += 1;
1156                    let mut body = Vec::new();
1157                    lower_actions(
1158                        actions,
1159                        position,
1160                        &mut body,
1161                        storage,
1162                        globals,
1163                        players,
1164                        subroutines,
1165                    )?;
1166                    Some(body)
1167                } else {
1168                    None
1169                };
1170                if !matches!(actions.get(*position), Some(Action::End)) {
1171                    return Err(WorkshopError::Malformed {
1172                        message: "control-flow action is missing End".to_string(),
1173                        span: None,
1174                    });
1175                }
1176                *position += 1;
1177                output.push(storage.actions.push(wir::Action::If {
1178                    branches,
1179                    else_body,
1180                    span: None,
1181                }));
1182            }
1183            Action::While { condition } => {
1184                *position += 1;
1185                let mut body = Vec::new();
1186                lower_actions(
1187                    actions,
1188                    position,
1189                    &mut body,
1190                    storage,
1191                    globals,
1192                    players,
1193                    subroutines,
1194                )?;
1195                require_end(actions, position)?;
1196                let condition = wir_value(condition, storage, globals, players, subroutines)?;
1197                output.push(storage.actions.push(wir::Action::While {
1198                    condition,
1199                    body,
1200                    span: None,
1201                }));
1202            }
1203            Action::ForGlobalVariable {
1204                variable,
1205                start,
1206                stop,
1207                step,
1208            } => {
1209                *position += 1;
1210                let mut body = Vec::new();
1211                lower_actions(
1212                    actions,
1213                    position,
1214                    &mut body,
1215                    storage,
1216                    globals,
1217                    players,
1218                    subroutines,
1219                )?;
1220                require_end(actions, position)?;
1221                let variable = *globals
1222                    .get(variable)
1223                    .ok_or_else(|| unknown_name("global variable", variable))?;
1224                let start = wir_value(start, storage, globals, players, subroutines)?;
1225                let stop = wir_value(stop, storage, globals, players, subroutines)?;
1226                let step = wir_value(step, storage, globals, players, subroutines)?;
1227                output.push(storage.actions.push(wir::Action::ForGlobalVariable {
1228                    variable,
1229                    start,
1230                    stop,
1231                    step,
1232                    body,
1233                    span: None,
1234                    target_span: None,
1235                }));
1236            }
1237            Action::ForPlayerVariable {
1238                player,
1239                variable,
1240                start,
1241                stop,
1242                step,
1243            } => {
1244                *position += 1;
1245                let mut body = Vec::new();
1246                lower_actions(
1247                    actions,
1248                    position,
1249                    &mut body,
1250                    storage,
1251                    globals,
1252                    players,
1253                    subroutines,
1254                )?;
1255                require_end(actions, position)?;
1256                let player = wir_value(player, storage, globals, players, subroutines)?;
1257                let variable = *players
1258                    .get(variable)
1259                    .ok_or_else(|| unknown_name("player variable", variable))?;
1260                let start = wir_value(start, storage, globals, players, subroutines)?;
1261                let stop = wir_value(stop, storage, globals, players, subroutines)?;
1262                let step = wir_value(step, storage, globals, players, subroutines)?;
1263                output.push(storage.actions.push(wir::Action::ForPlayerVariable {
1264                    player,
1265                    variable,
1266                    start,
1267                    stop,
1268                    step,
1269                    body,
1270                    span: None,
1271                }));
1272            }
1273            action => {
1274                *position += 1;
1275                let lowered = wir_action(action, storage, globals, players, subroutines)?;
1276                output.push(lowered);
1277            }
1278        }
1279        if disabled {
1280            let action = output.pop().expect("a lowered action was just pushed");
1281            output.push(
1282                storage
1283                    .actions
1284                    .push(wir::Action::Disabled { action, span: None }),
1285            );
1286        }
1287    }
1288    Ok(())
1289}
1290
1291fn apply_action_provenance(
1292    storage: &mut wir::Program,
1293    actions: &[wir::ActionId],
1294    provenance: &[ActionProvenance],
1295    position: &mut usize,
1296) -> Result<()> {
1297    for id in actions {
1298        let action = storage
1299            .actions
1300            .get(*id)
1301            .cloned()
1302            .ok_or_else(|| malformed_id("action", id.index()))?;
1303        match action {
1304            wir::Action::If {
1305                branches,
1306                else_body,
1307                ..
1308            } => {
1309                let source = provenance.get(*position).cloned().unwrap_or_default();
1310                *position += 1;
1311                apply_action_source(storage, *id, &source);
1312                for (branch_index, branch) in branches.iter().enumerate() {
1313                    if branch_index > 0 {
1314                        let source = provenance.get(*position).cloned().unwrap_or_default();
1315                        *position += 1;
1316                        set_value_span(
1317                            storage,
1318                            branch.condition,
1319                            source.arguments.first().copied().flatten(),
1320                        );
1321                    }
1322                    apply_action_provenance(storage, &branch.body, provenance, position)?;
1323                }
1324                if let Some(body) = else_body {
1325                    *position += 1;
1326                    apply_action_provenance(storage, &body, provenance, position)?;
1327                }
1328                *position += 1;
1329            }
1330            wir::Action::While {
1331                condition, body, ..
1332            } => {
1333                let source = provenance.get(*position).cloned().unwrap_or_default();
1334                *position += 1;
1335                apply_action_source(storage, *id, &source);
1336                apply_action_provenance(storage, &body, provenance, position)?;
1337                *position += 1;
1338                set_value_span(
1339                    storage,
1340                    condition,
1341                    source.arguments.first().copied().flatten(),
1342                );
1343            }
1344            wir::Action::ForGlobalVariable {
1345                start,
1346                stop,
1347                step,
1348                body,
1349                ..
1350            } => {
1351                let source = provenance.get(*position).cloned().unwrap_or_default();
1352                *position += 1;
1353                apply_action_source(storage, *id, &source);
1354                apply_action_provenance(storage, &body, provenance, position)?;
1355                *position += 1;
1356                for (value, span) in [start, stop, step].into_iter().zip(source.arguments) {
1357                    set_value_span(storage, value, span);
1358                }
1359            }
1360            wir::Action::ForPlayerVariable {
1361                player,
1362                start,
1363                stop,
1364                step,
1365                body,
1366                ..
1367            } => {
1368                let source = provenance.get(*position).cloned().unwrap_or_default();
1369                *position += 1;
1370                apply_action_source(storage, *id, &source);
1371                apply_action_provenance(storage, &body, provenance, position)?;
1372                *position += 1;
1373                for (value, span) in [player, start, stop, step]
1374                    .into_iter()
1375                    .zip(source.arguments)
1376                {
1377                    set_value_span(storage, value, span);
1378                }
1379            }
1380            wir::Action::Disabled { action, .. } => {
1381                let start = *position;
1382                apply_action_provenance(storage, &[action], provenance, position)?;
1383                let source = provenance.get(start).cloned().unwrap_or_default();
1384                if let Some(wir::Action::Disabled { span, .. }) = storage.actions.get_mut(*id) {
1385                    *span = source.span;
1386                }
1387            }
1388            _ => {
1389                let source = provenance.get(*position).cloned().unwrap_or_default();
1390                *position += 1;
1391                apply_action_source(storage, *id, &source);
1392            }
1393        }
1394    }
1395    Ok(())
1396}
1397
1398fn apply_action_source(storage: &mut wir::Program, id: wir::ActionId, source: &ActionProvenance) {
1399    let arguments = source.arguments.clone();
1400    if let Some(action) = storage.actions.get_mut(id) {
1401        match action {
1402            wir::Action::SetGlobalVariable { span, .. }
1403            | wir::Action::ModifyGlobalVariable { span, .. }
1404            | wir::Action::SetPlayerVariable { span, .. }
1405            | wir::Action::ModifyPlayerVariable { span, .. }
1406            | wir::Action::AssignMember { span, .. }
1407            | wir::Action::CallSubroutine { span, .. }
1408            | wir::Action::If { span, .. }
1409            | wir::Action::While { span, .. }
1410            | wir::Action::ForGlobalVariable { span, .. }
1411            | wir::Action::ForPlayerVariable { span, .. }
1412            | wir::Action::Disabled { span, .. }
1413            | wir::Action::Call { span, .. } => *span = source.span,
1414        }
1415    }
1416    let value_ids = storage
1417        .actions
1418        .get(id)
1419        .map(action_value_ids)
1420        .unwrap_or_default();
1421    for (value, span) in value_ids.into_iter().zip(arguments) {
1422        set_value_span(storage, value, span);
1423    }
1424}
1425
1426fn action_value_ids(action: &wir::Action) -> Vec<wir::ValueId> {
1427    match action {
1428        wir::Action::SetGlobalVariable { value, .. }
1429        | wir::Action::ModifyGlobalVariable { value, .. } => vec![*value],
1430        wir::Action::SetPlayerVariable { player, value, .. }
1431        | wir::Action::ModifyPlayerVariable { player, value, .. } => vec![*player, *value],
1432        wir::Action::AssignMember { target, value, .. } => vec![*target, *value],
1433        wir::Action::If { branches, .. } => {
1434            branches.iter().map(|branch| branch.condition).collect()
1435        }
1436        wir::Action::While { condition, .. } => vec![*condition],
1437        wir::Action::ForGlobalVariable {
1438            start, stop, step, ..
1439        } => vec![*start, *stop, *step],
1440        wir::Action::ForPlayerVariable {
1441            player,
1442            start,
1443            stop,
1444            step,
1445            ..
1446        } => vec![*player, *start, *stop, *step],
1447        wir::Action::Call { args, .. } => args.clone(),
1448        wir::Action::CallSubroutine { .. } | wir::Action::Disabled { .. } => Vec::new(),
1449    }
1450}
1451
1452fn set_value_span(
1453    storage: &mut wir::Program,
1454    value: wir::ValueId,
1455    span: Option<crate::source::Span>,
1456) {
1457    if let Some(node) = storage.values.get_mut(value) {
1458        node.span = span;
1459    }
1460}
1461
1462fn require_end(actions: &[Action], position: &mut usize) -> Result<()> {
1463    if !matches!(actions.get(*position), Some(Action::End)) {
1464        return Err(WorkshopError::Malformed {
1465            message: "control-flow action is missing End".to_string(),
1466            span: None,
1467        });
1468    }
1469    *position += 1;
1470    Ok(())
1471}
1472
1473fn wir_action(
1474    action: &Action,
1475    storage: &mut wir::Program,
1476    globals: &HashMap<String, wir::GlobalVarId>,
1477    players: &HashMap<String, wir::PlayerVarId>,
1478    subroutines: &HashMap<String, wir::SubroutineId>,
1479) -> Result<wir::ActionId> {
1480    let action = match action {
1481        Action::SetGlobalVariable { variable, value } => wir::Action::SetGlobalVariable {
1482            variable: *globals
1483                .get(variable)
1484                .ok_or_else(|| unknown_name("global variable", variable))?,
1485            value: wir_value(value, storage, globals, players, subroutines)?,
1486            span: None,
1487            target_span: None,
1488        },
1489        Action::ModifyGlobalVariable {
1490            variable,
1491            op,
1492            value,
1493        } => wir::Action::ModifyGlobalVariable {
1494            variable: *globals
1495                .get(variable)
1496                .ok_or_else(|| unknown_name("global variable", variable))?,
1497            op: wir_modify(*op),
1498            value: wir_value(value, storage, globals, players, subroutines)?,
1499            span: None,
1500            target_span: None,
1501        },
1502        Action::SetPlayerVariable {
1503            player,
1504            variable,
1505            value,
1506        } => wir::Action::SetPlayerVariable {
1507            player: wir_value(player, storage, globals, players, subroutines)?,
1508            variable: *players
1509                .get(variable)
1510                .ok_or_else(|| unknown_name("player variable", variable))?,
1511            value: wir_value(value, storage, globals, players, subroutines)?,
1512            span: None,
1513            target_span: None,
1514        },
1515        Action::ModifyPlayerVariable {
1516            player,
1517            variable,
1518            op,
1519            value,
1520        } => wir::Action::ModifyPlayerVariable {
1521            player: wir_value(player, storage, globals, players, subroutines)?,
1522            variable: *players
1523                .get(variable)
1524                .ok_or_else(|| unknown_name("player variable", variable))?,
1525            op: wir_modify(*op),
1526            value: wir_value(value, storage, globals, players, subroutines)?,
1527            span: None,
1528            target_span: None,
1529        },
1530        Action::AssignMember { target, op, value } => wir::Action::AssignMember {
1531            target: wir_value(target, storage, globals, players, subroutines)?,
1532            op: op.map(wir_modify),
1533            value: wir_value(value, storage, globals, players, subroutines)?,
1534            span: None,
1535        },
1536        Action::CallSubroutine { subroutine } => wir::Action::CallSubroutine {
1537            subroutine: *subroutines
1538                .get(subroutine)
1539                .ok_or_else(|| unknown_name("subroutine", subroutine))?,
1540            span: None,
1541            callee_span: None,
1542        },
1543        Action::Disabled { .. } => {
1544            unreachable!("disabled actions are lowered by lower_actions")
1545        }
1546        Action::Call { name, args } => wir::Action::Call {
1547            name: name.clone(),
1548            args: args
1549                .iter()
1550                .map(|arg| wir_value(arg, storage, globals, players, subroutines))
1551                .collect::<Result<Vec<_>>>()?,
1552            span: None,
1553        },
1554        Action::ElseIf { .. }
1555        | Action::Else
1556        | Action::End
1557        | Action::If { .. }
1558        | Action::While { .. }
1559        | Action::ForGlobalVariable { .. }
1560        | Action::ForPlayerVariable { .. } => {
1561            unreachable!("structured actions are lowered by lower_actions")
1562        }
1563    };
1564    Ok(storage.actions.push(action))
1565}
1566
1567fn wir_value(
1568    value: &Value,
1569    storage: &mut wir::Program,
1570    globals: &HashMap<String, wir::GlobalVarId>,
1571    players: &HashMap<String, wir::PlayerVarId>,
1572    subroutines: &HashMap<String, wir::SubroutineId>,
1573) -> Result<wir::ValueId> {
1574    let value = match value {
1575        Value::Number(value) => wir::Value::Number {
1576            value: *value,
1577            text: crate::core::format::format_number(*value),
1578        },
1579        Value::String(value) => wir::Value::String(value.clone()),
1580        Value::LocalizedString(value) => wir::Value::LocalizedString(value.clone()),
1581        Value::Bool(value) => wir::Value::Bool(*value),
1582        Value::Null => wir::Value::Null,
1583        Value::Array(values) => wir::Value::Array(
1584            values
1585                .iter()
1586                .map(|value| wir_value(value, storage, globals, players, subroutines))
1587                .collect::<Result<Vec<_>>>()?,
1588        ),
1589        Value::Vector { x, y, z } => wir::Value::Vector {
1590            x: wir_value(x, storage, globals, players, subroutines)?,
1591            y: wir_value(y, storage, globals, players, subroutines)?,
1592            z: wir_value(z, storage, globals, players, subroutines)?,
1593        },
1594        Value::Enum { value_type, value } => wir::Value::Enum {
1595            value_type: value_type.clone(),
1596            value: value.clone(),
1597        },
1598        Value::GlobalVariable(name) => wir::Value::GlobalVariable(
1599            *globals
1600                .get(name)
1601                .ok_or_else(|| unknown_name("global variable", name))?,
1602        ),
1603        Value::PlayerVariable { player, variable } => wir::Value::PlayerVariable {
1604            player: wir_value(player, storage, globals, players, subroutines)?,
1605            variable: *players
1606                .get(variable)
1607                .ok_or_else(|| unknown_name("player variable", variable))?,
1608        },
1609        Value::Subroutine(name) => wir::Value::Subroutine(
1610            *subroutines
1611                .get(name)
1612                .ok_or_else(|| unknown_name("subroutine", name))?,
1613        ),
1614        Value::EventPlayer => wir::Value::EventPlayer,
1615        Value::Call { name, args } => wir::Value::Call {
1616            name: name.clone(),
1617            args: args
1618                .iter()
1619                .map(|arg| wir_value(arg, storage, globals, players, subroutines))
1620                .collect::<Result<Vec<_>>>()?,
1621        },
1622    };
1623    Ok(storage.values.push(wir::ValueNode::new(value, None)))
1624}
1625
1626fn wir_event(
1627    event: &Event,
1628    subroutines: &HashMap<String, wir::SubroutineId>,
1629) -> Result<wir::Event> {
1630    Ok(match event {
1631        Event::Global => wir::Event::Global,
1632        Event::EachPlayer => wir::Event::EachPlayer,
1633        Event::EachPlayerWithFilters { team, target } => wir::Event::EachPlayerWithFilters {
1634            team: wir_team(*team),
1635            target: wir_target(target),
1636        },
1637        Event::Player { kind, team, target } => wir::Event::Player {
1638            kind: wir_player_event(*kind),
1639            team: wir_team(*team),
1640            target: wir_target(target),
1641        },
1642        Event::Subroutine(name) => wir::Event::Subroutine(
1643            *subroutines
1644                .get(name)
1645                .ok_or_else(|| unknown_name("subroutine", name))?,
1646        ),
1647    })
1648}
1649
1650fn wir_team(team: EventTeam) -> wir::EventTeam {
1651    match team {
1652        EventTeam::All => wir::EventTeam::All,
1653        EventTeam::Team1 => wir::EventTeam::Team1,
1654        EventTeam::Team2 => wir::EventTeam::Team2,
1655    }
1656}
1657
1658fn wir_target(target: &EventTarget) -> wir::EventTarget {
1659    match target {
1660        EventTarget::All => wir::EventTarget::All,
1661        EventTarget::Slot(slot) => wir::EventTarget::Slot(*slot),
1662        EventTarget::Hero(hero) => wir::EventTarget::Hero(hero.clone()),
1663    }
1664}
1665
1666fn wir_player_event(kind: PlayerEventKind) -> wir::PlayerEventKind {
1667    match kind {
1668        PlayerEventKind::DealtDamage => wir::PlayerEventKind::DealtDamage,
1669        PlayerEventKind::DealtFinalBlow => wir::PlayerEventKind::DealtFinalBlow,
1670        PlayerEventKind::DealtHealing => wir::PlayerEventKind::DealtHealing,
1671        PlayerEventKind::DealtKnockback => wir::PlayerEventKind::DealtKnockback,
1672        PlayerEventKind::Died => wir::PlayerEventKind::Died,
1673        PlayerEventKind::EarnedElimination => wir::PlayerEventKind::EarnedElimination,
1674        PlayerEventKind::Joined => wir::PlayerEventKind::Joined,
1675        PlayerEventKind::Left => wir::PlayerEventKind::Left,
1676        PlayerEventKind::ReceivedHealing => wir::PlayerEventKind::ReceivedHealing,
1677        PlayerEventKind::ReceivedKnockback => wir::PlayerEventKind::ReceivedKnockback,
1678        PlayerEventKind::TookDamage => wir::PlayerEventKind::TookDamage,
1679    }
1680}
1681
1682fn public_modify(op: wir::ModifyOp) -> ModifyOp {
1683    match op {
1684        wir::ModifyOp::Add => ModifyOp::Add,
1685        wir::ModifyOp::Subtract => ModifyOp::Subtract,
1686        wir::ModifyOp::Multiply => ModifyOp::Multiply,
1687        wir::ModifyOp::Divide => ModifyOp::Divide,
1688        wir::ModifyOp::Modulo => ModifyOp::Modulo,
1689        wir::ModifyOp::Min => ModifyOp::Min,
1690        wir::ModifyOp::Max => ModifyOp::Max,
1691        wir::ModifyOp::RaiseToPower => ModifyOp::RaiseToPower,
1692        wir::ModifyOp::AppendToArray => ModifyOp::AppendToArray,
1693        wir::ModifyOp::RemoveFromArrayByValue => ModifyOp::RemoveFromArrayByValue,
1694        wir::ModifyOp::RemoveFromArrayByIndex => ModifyOp::RemoveFromArrayByIndex,
1695    }
1696}
1697
1698fn wir_modify(op: ModifyOp) -> wir::ModifyOp {
1699    match op {
1700        ModifyOp::Add => wir::ModifyOp::Add,
1701        ModifyOp::Subtract => wir::ModifyOp::Subtract,
1702        ModifyOp::Multiply => wir::ModifyOp::Multiply,
1703        ModifyOp::Divide => wir::ModifyOp::Divide,
1704        ModifyOp::Modulo => wir::ModifyOp::Modulo,
1705        ModifyOp::Min => wir::ModifyOp::Min,
1706        ModifyOp::Max => wir::ModifyOp::Max,
1707        ModifyOp::RaiseToPower => wir::ModifyOp::RaiseToPower,
1708        ModifyOp::AppendToArray => wir::ModifyOp::AppendToArray,
1709        ModifyOp::RemoveFromArrayByValue => wir::ModifyOp::RemoveFromArrayByValue,
1710        ModifyOp::RemoveFromArrayByIndex => wir::ModifyOp::RemoveFromArrayByIndex,
1711    }
1712}
1713
1714fn malformed_id(kind: &str, index: usize) -> WorkshopError {
1715    WorkshopError::Malformed {
1716        message: format!("dangling {kind} {index}"),
1717        span: None,
1718    }
1719}
1720
1721fn unknown_name(kind: &str, name: &str) -> WorkshopError {
1722    WorkshopError::Malformed {
1723        message: format!("unknown {kind} '{name}'"),
1724        span: None,
1725    }
1726}
1727
1728/// A Workshop global or player variable declaration.
1729#[derive(Debug, Clone, PartialEq, Eq)]
1730pub struct Variable {
1731    pub name: String,
1732    /// The raw Workshop declaration index, when the declaration has one.
1733    pub index: Option<u32>,
1734}
1735
1736impl Variable {
1737    pub fn new(name: impl Into<String>) -> Self {
1738        Self {
1739            name: name.into(),
1740            index: None,
1741        }
1742    }
1743
1744    pub fn with_index(name: impl Into<String>, index: u32) -> Self {
1745        Self {
1746            name: name.into(),
1747            index: Some(index),
1748        }
1749    }
1750}
1751
1752/// A Workshop subroutine declaration.
1753#[derive(Debug, Clone, PartialEq, Eq)]
1754pub struct Subroutine {
1755    pub name: String,
1756    /// The raw Workshop declaration index, when the declaration has one.
1757    pub index: Option<u32>,
1758}
1759
1760impl Subroutine {
1761    pub fn new(name: impl Into<String>) -> Self {
1762        Self {
1763            name: name.into(),
1764            index: None,
1765        }
1766    }
1767
1768    pub fn with_index(name: impl Into<String>, index: u32) -> Self {
1769        Self {
1770            name: name.into(),
1771            index: Some(index),
1772        }
1773    }
1774}
1775
1776/// A Workshop rule with explicit conditions and a linear action stream.
1777#[derive(Debug, Clone)]
1778#[non_exhaustive]
1779pub struct Rule {
1780    pub name: String,
1781    pub disabled: bool,
1782    pub event: Event,
1783    pub conditions: Vec<Condition>,
1784    pub actions: Vec<Action>,
1785}
1786
1787impl Rule {
1788    pub fn new(name: impl Into<String>, event: Event) -> Self {
1789        Self {
1790            name: name.into(),
1791            disabled: false,
1792            event,
1793            conditions: Vec::new(),
1794            actions: Vec::new(),
1795        }
1796    }
1797
1798    pub fn condition(mut self, condition: impl Into<Condition>) -> Self {
1799        self.conditions.push(condition.into());
1800        self
1801    }
1802
1803    pub fn action(mut self, action: Action) -> Self {
1804        self.actions.push(action);
1805        self
1806    }
1807}
1808
1809/// A rule condition. Conditions remain distinct from general value expressions.
1810#[derive(Debug, Clone)]
1811#[non_exhaustive]
1812pub struct Condition {
1813    pub value: Value,
1814    pub disabled: bool,
1815}
1816
1817impl Condition {
1818    pub fn new(value: Value) -> Self {
1819        Self {
1820            value,
1821            disabled: false,
1822        }
1823    }
1824
1825    pub fn disabled(value: Value) -> Self {
1826        Self {
1827            value,
1828            disabled: true,
1829        }
1830    }
1831}
1832
1833impl From<Value> for Condition {
1834    fn from(value: Value) -> Self {
1835        Self::new(value)
1836    }
1837}
1838
1839fn action_argument_count(action: &Action) -> usize {
1840    match action {
1841        Action::SetGlobalVariable { .. }
1842        | Action::ModifyGlobalVariable { .. }
1843        | Action::If { .. }
1844        | Action::ElseIf { .. }
1845        | Action::While { .. } => 1,
1846        Action::SetPlayerVariable { .. } | Action::ModifyPlayerVariable { .. } => 2,
1847        Action::AssignMember { .. } => 2,
1848        Action::ForGlobalVariable { .. } => 3,
1849        Action::ForPlayerVariable { .. } => 4,
1850        Action::Call { args, .. } => args.len(),
1851        Action::CallSubroutine { .. } | Action::Else | Action::End => 0,
1852        Action::Disabled { action } => action_argument_count(action),
1853    }
1854}
1855
1856/// A Workshop event identity and its native filters.
1857#[derive(Debug, Clone, PartialEq, Eq)]
1858pub enum Event {
1859    Global,
1860    EachPlayer,
1861    EachPlayerWithFilters {
1862        team: EventTeam,
1863        target: EventTarget,
1864    },
1865    Player {
1866        kind: PlayerEventKind,
1867        team: EventTeam,
1868        target: EventTarget,
1869    },
1870    Subroutine(String),
1871}
1872
1873#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1874pub enum EventTeam {
1875    All,
1876    Team1,
1877    Team2,
1878}
1879
1880#[derive(Debug, Clone, PartialEq, Eq)]
1881pub enum EventTarget {
1882    All,
1883    Slot(u8),
1884    Hero(String),
1885}
1886
1887/// A non-ongoing player event identity.
1888///
1889/// Workshop can add player-scoped event identities independently of this
1890/// crate. Consumers should use a wildcard arm when matching this type.
1891#[non_exhaustive]
1892#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1893pub enum PlayerEventKind {
1894    DealtDamage,
1895    DealtFinalBlow,
1896    DealtHealing,
1897    DealtKnockback,
1898    Died,
1899    EarnedElimination,
1900    Joined,
1901    Left,
1902    ReceivedHealing,
1903    ReceivedKnockback,
1904    TookDamage,
1905}
1906
1907/// A Workshop action line. Control flow is represented in the same order as
1908/// the Workshop source, including its explicit `End` lines.
1909#[derive(Debug, Clone)]
1910pub enum Action {
1911    SetGlobalVariable {
1912        variable: String,
1913        value: Value,
1914    },
1915    ModifyGlobalVariable {
1916        variable: String,
1917        op: ModifyOp,
1918        value: Value,
1919    },
1920    SetPlayerVariable {
1921        player: Value,
1922        variable: String,
1923        value: Value,
1924    },
1925    ModifyPlayerVariable {
1926        player: Value,
1927        variable: String,
1928        op: ModifyOp,
1929        value: Value,
1930    },
1931    AssignMember {
1932        target: Value,
1933        op: Option<ModifyOp>,
1934        value: Value,
1935    },
1936    CallSubroutine {
1937        subroutine: String,
1938    },
1939    If {
1940        condition: Value,
1941    },
1942    ElseIf {
1943        condition: Value,
1944    },
1945    Else,
1946    While {
1947        condition: Value,
1948    },
1949    ForGlobalVariable {
1950        variable: String,
1951        start: Value,
1952        stop: Value,
1953        step: Value,
1954    },
1955    ForPlayerVariable {
1956        player: Value,
1957        variable: String,
1958        start: Value,
1959        stop: Value,
1960        step: Value,
1961    },
1962    End,
1963    Disabled {
1964        action: Box<Action>,
1965    },
1966    Call {
1967        name: String,
1968        args: Vec<Value>,
1969    },
1970}
1971
1972impl Action {
1973    /// Mark an action as disabled.
1974    pub fn disabled(action: Action) -> Self {
1975        Self::Disabled {
1976            action: Box::new(action),
1977        }
1978    }
1979
1980    /// Construct a dynamic action call by canonical Workshop id.
1981    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Value>) -> Self {
1982        Self::Call {
1983            name: name.into(),
1984            args: args.into_iter().collect(),
1985        }
1986    }
1987}
1988
1989/// The operation used by a Workshop variable modification action.
1990///
1991/// Workshop can add modification operations independently of this crate.
1992/// Consumers should use a wildcard arm when matching this type.
1993#[non_exhaustive]
1994#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1995pub enum ModifyOp {
1996    Add,
1997    Subtract,
1998    Multiply,
1999    Divide,
2000    Modulo,
2001    Min,
2002    Max,
2003    RaiseToPower,
2004    AppendToArray,
2005    RemoveFromArrayByValue,
2006    RemoveFromArrayByIndex,
2007}
2008
2009/// A composable Workshop value expression.
2010#[derive(Debug, Clone)]
2011pub enum Value {
2012    Number(f64),
2013    String(String),
2014    LocalizedString(String),
2015    Bool(bool),
2016    Null,
2017    Array(Vec<Value>),
2018    Vector {
2019        x: Box<Value>,
2020        y: Box<Value>,
2021        z: Box<Value>,
2022    },
2023    Enum {
2024        value_type: String,
2025        value: String,
2026    },
2027    GlobalVariable(String),
2028    PlayerVariable {
2029        player: Box<Value>,
2030        variable: String,
2031    },
2032    Subroutine(String),
2033    EventPlayer,
2034    Call {
2035        name: String,
2036        args: Vec<Value>,
2037    },
2038}
2039
2040impl Value {
2041    /// Construct a numeric Workshop literal.
2042    pub fn number(value: f64) -> Self {
2043        Self::Number(value)
2044    }
2045
2046    /// Construct a custom Workshop string literal.
2047    pub fn string(value: impl Into<String>) -> Self {
2048        Self::String(value.into())
2049    }
2050
2051    pub fn global_variable(name: impl Into<String>) -> Self {
2052        Self::GlobalVariable(name.into())
2053    }
2054
2055    pub fn player_variable(player: Value, name: impl Into<String>) -> Self {
2056        Self::PlayerVariable {
2057            player: Box::new(player),
2058            variable: name.into(),
2059        }
2060    }
2061
2062    /// Construct a dynamic value call by canonical Workshop id.
2063    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Value>) -> Self {
2064        Self::Call {
2065            name: name.into(),
2066            args: args.into_iter().collect(),
2067        }
2068    }
2069}
2070
2071impl From<bool> for Value {
2072    fn from(value: bool) -> Self {
2073        Self::Bool(value)
2074    }
2075}
2076
2077impl From<f64> for Value {
2078    fn from(value: f64) -> Self {
2079        Self::Number(value)
2080    }
2081}
2082
2083impl From<f32> for Value {
2084    fn from(value: f32) -> Self {
2085        Self::Number(f64::from(value))
2086    }
2087}
2088
2089macro_rules! impl_integer_value {
2090    ($($type:ty),+ $(,)?) => {
2091        $(
2092            impl From<$type> for Value {
2093                fn from(value: $type) -> Self {
2094                    Self::Number(value as f64)
2095                }
2096            }
2097        )+
2098    };
2099}
2100
2101impl_integer_value!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
2102
2103impl From<String> for Value {
2104    fn from(value: String) -> Self {
2105        Self::String(value)
2106    }
2107}
2108
2109impl From<&str> for Value {
2110    fn from(value: &str) -> Self {
2111        Self::String(value.to_string())
2112    }
2113}
2114
2115impl<T: Into<Value>> From<Vec<T>> for Value {
2116    fn from(values: Vec<T>) -> Self {
2117        Self::Array(values.into_iter().map(Into::into).collect())
2118    }
2119}
2120
2121impl<T: Into<Value>, const N: usize> From<[T; N]> for Value {
2122    fn from(values: [T; N]) -> Self {
2123        Self::Array(values.into_iter().map(Into::into).collect())
2124    }
2125}