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::wir;
8
9/// A complete Workshop program built from Workshop concepts.
10#[derive(Debug, Clone, Default)]
11pub struct Program {
12    pub settings: Option<Settings>,
13    pub global_variables: Vec<Variable>,
14    pub player_variables: Vec<Variable>,
15    pub subroutines: Vec<Subroutine>,
16    pub rules: Vec<Rule>,
17    /// Raw source metadata is retained by parsed programs without becoming a
18    /// required field for independently constructed programs.
19    source: Option<Box<wir::Program>>,
20    provenance: Option<Box<ProgramProvenance>>,
21}
22
23#[derive(Debug, Clone, Default)]
24struct ProgramProvenance {
25    rules: Vec<RuleProvenance>,
26}
27
28#[derive(Debug, Clone, Default)]
29struct RuleProvenance {
30    span: Option<crate::source::Span>,
31    conditions: Vec<Option<crate::source::Span>>,
32    actions: Vec<ActionProvenance>,
33}
34
35#[derive(Debug, Clone, Default)]
36struct ActionProvenance {
37    span: Option<crate::source::Span>,
38    arguments: Vec<Option<crate::source::Span>>,
39}
40
41impl Program {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    pub fn global_variable(&mut self, variable: Variable) -> &mut Self {
47        self.global_variables.push(variable);
48        self
49    }
50
51    pub fn player_variable(&mut self, variable: Variable) -> &mut Self {
52        self.player_variables.push(variable);
53        self
54    }
55
56    pub fn subroutine(&mut self, subroutine: Subroutine) -> &mut Self {
57        self.subroutines.push(subroutine);
58        self
59    }
60
61    pub fn rule(&mut self, rule: Rule) -> &mut Self {
62        self.rules.push(rule);
63        self
64    }
65
66    /// Return the retained source document for a parsed file.
67    pub fn source(&self, file: crate::source::FileId) -> Option<&crate::source::SourceDocument> {
68        self.source
69            .as_deref()
70            .and_then(|program| program.source(file))
71    }
72
73    /// Return the authored span of a public rule, when source metadata exists.
74    pub fn rule_span(&self, rule: usize) -> Option<crate::source::Span> {
75        self.provenance
76            .as_deref()
77            .and_then(|provenance| provenance.rules.get(rule))
78            .and_then(|rule| rule.span)
79    }
80
81    /// Return the authored span of a public rule condition value.
82    pub fn condition_span(&self, rule: usize, condition: usize) -> Option<crate::source::Span> {
83        self.provenance
84            .as_deref()
85            .and_then(|provenance| provenance.rules.get(rule))
86            .and_then(|rule| rule.conditions.get(condition))
87            .copied()
88            .flatten()
89    }
90
91    /// Return the authored span of a public action in its linear rule order.
92    pub fn action_span(&self, rule: usize, action: usize) -> Option<crate::source::Span> {
93        self.provenance
94            .as_deref()
95            .and_then(|provenance| provenance.rules.get(rule))
96            .and_then(|rule| rule.actions.get(action))
97            .and_then(|action| action.span)
98    }
99
100    /// Return the authored span of a direct value argument of a public action.
101    pub fn action_argument_span(
102        &self,
103        rule: usize,
104        action: usize,
105        argument: usize,
106    ) -> Option<crate::source::Span> {
107        self.provenance
108            .as_deref()
109            .and_then(|provenance| provenance.rules.get(rule))
110            .and_then(|rule| rule.actions.get(action))
111            .and_then(|action| action.arguments.get(argument))
112            .copied()
113            .flatten()
114    }
115
116    /// Create a checked source edit through the authored source attached to
117    /// this canonical program.
118    pub fn edit_source(
119        &self,
120        span: crate::source::Span,
121        replacement: impl Into<String>,
122    ) -> std::result::Result<crate::source::SourceEdit, crate::source::SourceEditError> {
123        self.source(span.file)
124            .ok_or(crate::source::SourceEditError::InvalidRange)?
125            .edit_span(span, replacement)
126    }
127
128    /// Validate the structural invariants of the canonical program.
129    pub fn validate(&self) -> std::result::Result<(), WorkshopError> {
130        let storage = self.to_wir()?;
131        storage
132            .validate()
133            .map_err(|error| WorkshopError::Malformed {
134                message: error.to_string(),
135                span: None,
136            })
137    }
138
139    /// Report constructs that are structurally preserved but not fully
140    /// understood by the canonical catalog.
141    pub fn semantic_issues(
142        &self,
143        catalog: &crate::catalog::Catalog,
144    ) -> Vec<crate::rules::SemanticIssue> {
145        crate::analysis::semantic::inspect(self, catalog)
146    }
147
148    /// Render the program through the canonical Workshop debug representation.
149    pub fn dump(&self) -> String {
150        self.to_wir().map_or_else(
151            |error| format!("invalid program: {error}"),
152            |program| program.dump(),
153        )
154    }
155
156    pub(crate) fn from_wir(storage: wir::Program) -> Result<Self> {
157        let mut program = Self {
158            settings: storage.settings.clone(),
159            global_variables: storage
160                .global_variables
161                .iter()
162                .map(|variable| Variable::with_index(variable.name.clone(), variable.index))
163                .collect(),
164            player_variables: storage
165                .player_variables
166                .iter()
167                .map(|variable| Variable::with_index(variable.name.clone(), variable.index))
168                .collect(),
169            subroutines: storage
170                .subroutines
171                .iter()
172                .map(|subroutine| Subroutine::with_index(subroutine.name.clone(), subroutine.index))
173                .collect(),
174            rules: Vec::with_capacity(storage.rules.len()),
175            source: Some(Box::new(storage.clone())),
176            provenance: Some(Box::new(ProgramProvenance::default())),
177        };
178        for rule in storage.rules.iter() {
179            let event = public_event(&storage, &rule.event)?;
180            let conditions = rule
181                .conditions
182                .iter()
183                .map(|condition| public_value(&storage, *condition))
184                .collect::<Result<Vec<_>>>()?;
185            let mut actions = Vec::new();
186            let mut action_provenance = Vec::new();
187            for action in &rule.actions {
188                public_actions(&storage, *action, &mut actions)?;
189                public_action_provenance(&storage, *action, &mut action_provenance)?;
190            }
191            program
192                .provenance
193                .as_mut()
194                .expect("parsed programs retain provenance")
195                .rules
196                .push(RuleProvenance {
197                    span: rule.span,
198                    conditions: rule
199                        .conditions
200                        .iter()
201                        .map(|condition| {
202                            storage.values.get(*condition).and_then(|value| value.span)
203                        })
204                        .collect(),
205                    actions: action_provenance,
206                });
207            program.rules.push(Rule {
208                name: rule.name.clone(),
209                disabled: rule.disabled,
210                event,
211                conditions: conditions.into_iter().map(Condition::new).collect(),
212                actions,
213            });
214        }
215        Ok(program)
216    }
217
218    pub(crate) fn to_wir(&self) -> Result<wir::Program> {
219        let mut storage = wir::Program {
220            settings: self.settings.clone(),
221            ..Default::default()
222        };
223
224        if let Some(source) = &self.source {
225            for file in source.files.iter() {
226                storage.add_file(file.clone());
227            }
228        }
229
230        let mut globals = HashMap::new();
231        for (position, variable) in self.global_variables.iter().enumerate() {
232            let id = storage.global_variables.push(wir::WorkshopVariable {
233                name: variable.name.clone(),
234                index: variable.index.unwrap_or(position as u32),
235                span: None,
236                name_span: None,
237            });
238            globals.insert(variable.name.clone(), id);
239        }
240        let mut players = HashMap::new();
241        for (position, variable) in self.player_variables.iter().enumerate() {
242            let id = storage.player_variables.push(wir::WorkshopVariable {
243                name: variable.name.clone(),
244                index: variable.index.unwrap_or(position as u32),
245                span: None,
246                name_span: None,
247            });
248            players.insert(variable.name.clone(), id);
249        }
250        let mut subroutines = HashMap::new();
251        for (position, subroutine) in self.subroutines.iter().enumerate() {
252            let id = storage.subroutines.push(wir::WorkshopSubroutine {
253                name: subroutine.name.clone(),
254                index: subroutine.index.unwrap_or(position as u32),
255                span: None,
256                name_span: None,
257            });
258            subroutines.insert(subroutine.name.clone(), id);
259        }
260
261        for (rule_index, rule) in self.rules.iter().enumerate() {
262            let event = wir_event(&rule.event, &subroutines)?;
263            let conditions = rule
264                .conditions
265                .iter()
266                .enumerate()
267                .map(|(condition_index, condition)| {
268                    if condition.disabled {
269                        return Err(WorkshopError::Unsupported {
270                            message: "disabled conditions are not representable by the canonical storage model"
271                                .to_string(),
272                            span: None,
273                        });
274                    }
275                    wir_value(
276                        &condition.value,
277                        &mut storage,
278                        &globals,
279                        &players,
280                        &subroutines,
281                    )
282                    .inspect(|&value| {
283                        if let Some(span) = self.condition_span(rule_index, condition_index) {
284                            storage.values.get_mut(value).unwrap().span = Some(span);
285                        }
286                    })
287                })
288                .collect::<Result<Vec<_>>>()?;
289            let mut actions = Vec::new();
290            let mut position = 0;
291            lower_actions(
292                &rule.actions,
293                &mut position,
294                &mut actions,
295                &mut storage,
296                &globals,
297                &players,
298                &subroutines,
299            )?;
300            if let Some(provenance) = self
301                .provenance
302                .as_deref()
303                .and_then(|provenance| provenance.rules.get(rule_index))
304            {
305                let mut public_position = 0;
306                apply_action_provenance(
307                    &mut storage,
308                    &actions,
309                    &provenance.actions,
310                    &mut public_position,
311                )?;
312            }
313            if position != rule.actions.len() {
314                return Err(WorkshopError::Malformed {
315                    message: "unexpected control-flow terminator in rule actions".to_string(),
316                    span: None,
317                });
318            }
319            storage.rules.push(wir::Rule {
320                name: rule.name.clone(),
321                span: self.rule_span(rule_index),
322                name_span: None,
323                disabled: rule.disabled,
324                event,
325                conditions,
326                actions,
327            });
328        }
329        Ok(storage)
330    }
331}
332
333fn public_event(storage: &wir::Program, event: &wir::Event) -> Result<Event> {
334    Ok(match event {
335        wir::Event::Global => Event::Global,
336        wir::Event::EachPlayer => Event::EachPlayer,
337        wir::Event::EachPlayerWithFilters { team, target } => Event::EachPlayerWithFilters {
338            team: public_team(*team),
339            target: public_target(target),
340        },
341        wir::Event::Player { kind, team, target } => Event::Player {
342            kind: public_player_event(*kind),
343            team: public_team(*team),
344            target: public_target(target),
345        },
346        wir::Event::Subroutine(id) => Event::Subroutine(
347            storage
348                .subroutines
349                .get(*id)
350                .ok_or_else(|| malformed_id("subroutine", id.index()))?
351                .name
352                .clone(),
353        ),
354    })
355}
356
357fn public_team(team: wir::EventTeam) -> EventTeam {
358    match team {
359        wir::EventTeam::All => EventTeam::All,
360        wir::EventTeam::Team1 => EventTeam::Team1,
361        wir::EventTeam::Team2 => EventTeam::Team2,
362    }
363}
364
365fn public_target(target: &wir::EventTarget) -> EventTarget {
366    match target {
367        wir::EventTarget::All => EventTarget::All,
368        wir::EventTarget::Slot(slot) => EventTarget::Slot(*slot),
369        wir::EventTarget::Hero(hero) => EventTarget::Hero(hero.clone()),
370    }
371}
372
373fn public_player_event(kind: wir::PlayerEventKind) -> PlayerEventKind {
374    match kind {
375        wir::PlayerEventKind::DealtDamage => PlayerEventKind::DealtDamage,
376        wir::PlayerEventKind::DealtFinalBlow => PlayerEventKind::DealtFinalBlow,
377        wir::PlayerEventKind::DealtHealing => PlayerEventKind::DealtHealing,
378        wir::PlayerEventKind::DealtKnockback => PlayerEventKind::DealtKnockback,
379        wir::PlayerEventKind::Died => PlayerEventKind::Died,
380        wir::PlayerEventKind::EarnedElimination => PlayerEventKind::EarnedElimination,
381        wir::PlayerEventKind::Joined => PlayerEventKind::Joined,
382        wir::PlayerEventKind::Left => PlayerEventKind::Left,
383        wir::PlayerEventKind::ReceivedHealing => PlayerEventKind::ReceivedHealing,
384        wir::PlayerEventKind::ReceivedKnockback => PlayerEventKind::ReceivedKnockback,
385        wir::PlayerEventKind::TookDamage => PlayerEventKind::TookDamage,
386    }
387}
388
389fn public_value(storage: &wir::Program, id: wir::ValueId) -> Result<Value> {
390    let node = storage
391        .values
392        .get(id)
393        .ok_or_else(|| malformed_id("value", id.index()))?;
394    Ok(match &node.value {
395        wir::Value::Number { value, .. } => Value::Number(*value),
396        wir::Value::String(value) => Value::String(value.clone()),
397        wir::Value::LocalizedString(value) => Value::LocalizedString(value.clone()),
398        wir::Value::Bool(value) => Value::Bool(*value),
399        wir::Value::Null => Value::Null,
400        wir::Value::Array(values) => Value::Array(
401            values
402                .iter()
403                .map(|value| public_value(storage, *value))
404                .collect::<Result<Vec<_>>>()?,
405        ),
406        wir::Value::Vector { x, y, z } => Value::Vector {
407            x: Box::new(public_value(storage, *x)?),
408            y: Box::new(public_value(storage, *y)?),
409            z: Box::new(public_value(storage, *z)?),
410        },
411        wir::Value::Enum { value_type, value } => Value::Enum {
412            value_type: value_type.clone(),
413            value: value.clone(),
414        },
415        wir::Value::GlobalVariable(id) => Value::GlobalVariable(
416            storage
417                .global_variables
418                .get(*id)
419                .ok_or_else(|| malformed_id("global variable", id.index()))?
420                .name
421                .clone(),
422        ),
423        wir::Value::PlayerVariable { player, variable } => Value::PlayerVariable {
424            player: Box::new(public_value(storage, *player)?),
425            variable: storage
426                .player_variables
427                .get(*variable)
428                .ok_or_else(|| malformed_id("player variable", variable.index()))?
429                .name
430                .clone(),
431        },
432        wir::Value::Subroutine(id) => Value::Subroutine(
433            storage
434                .subroutines
435                .get(*id)
436                .ok_or_else(|| malformed_id("subroutine", id.index()))?
437                .name
438                .clone(),
439        ),
440        wir::Value::EventPlayer => Value::EventPlayer,
441        wir::Value::Call { name, args } => Value::Call {
442            name: name.clone(),
443            args: args
444                .iter()
445                .map(|arg| public_value(storage, *arg))
446                .collect::<Result<Vec<_>>>()?,
447        },
448    })
449}
450
451fn public_actions(
452    storage: &wir::Program,
453    id: wir::ActionId,
454    output: &mut Vec<Action>,
455) -> Result<()> {
456    let action = storage
457        .actions
458        .get(id)
459        .ok_or_else(|| malformed_id("action", id.index()))?;
460    match action {
461        wir::Action::SetGlobalVariable {
462            variable, value, ..
463        } => output.push(Action::SetGlobalVariable {
464            variable: storage
465                .global_variables
466                .get(*variable)
467                .ok_or_else(|| malformed_id("global variable", variable.index()))?
468                .name
469                .clone(),
470            value: public_value(storage, *value)?,
471        }),
472        wir::Action::ModifyGlobalVariable {
473            variable,
474            op,
475            value,
476            ..
477        } => output.push(Action::ModifyGlobalVariable {
478            variable: storage
479                .global_variables
480                .get(*variable)
481                .ok_or_else(|| malformed_id("global variable", variable.index()))?
482                .name
483                .clone(),
484            op: public_modify(*op),
485            value: public_value(storage, *value)?,
486        }),
487        wir::Action::SetPlayerVariable {
488            player,
489            variable,
490            value,
491            ..
492        } => output.push(Action::SetPlayerVariable {
493            player: public_value(storage, *player)?,
494            variable: storage
495                .player_variables
496                .get(*variable)
497                .ok_or_else(|| malformed_id("player variable", variable.index()))?
498                .name
499                .clone(),
500            value: public_value(storage, *value)?,
501        }),
502        wir::Action::ModifyPlayerVariable {
503            player,
504            variable,
505            op,
506            value,
507            ..
508        } => output.push(Action::ModifyPlayerVariable {
509            player: public_value(storage, *player)?,
510            variable: storage
511                .player_variables
512                .get(*variable)
513                .ok_or_else(|| malformed_id("player variable", variable.index()))?
514                .name
515                .clone(),
516            op: public_modify(*op),
517            value: public_value(storage, *value)?,
518        }),
519        wir::Action::AssignMember {
520            target, op, value, ..
521        } => output.push(Action::AssignMember {
522            target: public_value(storage, *target)?,
523            op: op.map(public_modify),
524            value: public_value(storage, *value)?,
525        }),
526        wir::Action::CallSubroutine { subroutine, .. } => output.push(Action::CallSubroutine {
527            subroutine: storage
528                .subroutines
529                .get(*subroutine)
530                .ok_or_else(|| malformed_id("subroutine", subroutine.index()))?
531                .name
532                .clone(),
533        }),
534        wir::Action::If {
535            branches,
536            else_body,
537            ..
538        } => {
539            for (index, branch) in branches.iter().enumerate() {
540                output.push(if index == 0 {
541                    Action::If {
542                        condition: public_value(storage, branch.condition)?,
543                    }
544                } else {
545                    Action::ElseIf {
546                        condition: public_value(storage, branch.condition)?,
547                    }
548                });
549                for action in &branch.body {
550                    public_actions(storage, *action, output)?;
551                }
552            }
553            if let Some(body) = else_body {
554                output.push(Action::Else);
555                for action in body {
556                    public_actions(storage, *action, output)?;
557                }
558            }
559            output.push(Action::End);
560        }
561        wir::Action::While {
562            condition, body, ..
563        } => {
564            output.push(Action::While {
565                condition: public_value(storage, *condition)?,
566            });
567            for action in body {
568                public_actions(storage, *action, output)?;
569            }
570            output.push(Action::End);
571        }
572        wir::Action::ForGlobalVariable {
573            variable,
574            start,
575            stop,
576            step,
577            body,
578            ..
579        } => {
580            output.push(Action::ForGlobalVariable {
581                variable: storage
582                    .global_variables
583                    .get(*variable)
584                    .ok_or_else(|| malformed_id("global variable", variable.index()))?
585                    .name
586                    .clone(),
587                start: public_value(storage, *start)?,
588                stop: public_value(storage, *stop)?,
589                step: public_value(storage, *step)?,
590            });
591            for action in body {
592                public_actions(storage, *action, output)?;
593            }
594            output.push(Action::End);
595        }
596        wir::Action::ForPlayerVariable {
597            player,
598            variable,
599            start,
600            stop,
601            step,
602            body,
603            ..
604        } => {
605            output.push(Action::ForPlayerVariable {
606                player: public_value(storage, *player)?,
607                variable: storage
608                    .player_variables
609                    .get(*variable)
610                    .ok_or_else(|| malformed_id("player variable", variable.index()))?
611                    .name
612                    .clone(),
613                start: public_value(storage, *start)?,
614                stop: public_value(storage, *stop)?,
615                step: public_value(storage, *step)?,
616            });
617            for action in body {
618                public_actions(storage, *action, output)?;
619            }
620            output.push(Action::End);
621        }
622        wir::Action::Call { name, args, .. } => output.push(Action::Call {
623            name: name.clone(),
624            args: args
625                .iter()
626                .map(|arg| public_value(storage, *arg))
627                .collect::<Result<Vec<_>>>()?,
628        }),
629    }
630    Ok(())
631}
632
633fn public_action_provenance(
634    storage: &wir::Program,
635    id: wir::ActionId,
636    output: &mut Vec<ActionProvenance>,
637) -> Result<()> {
638    let action = storage
639        .actions
640        .get(id)
641        .ok_or_else(|| malformed_id("action", id.index()))?;
642    let push = |output: &mut Vec<ActionProvenance>, arguments: &[wir::ValueId]| {
643        output.push(ActionProvenance {
644            span: action.span(),
645            arguments: arguments
646                .iter()
647                .map(|value| storage.values.get(*value).and_then(|value| value.span))
648                .collect(),
649        });
650    };
651    let push_without_span = |output: &mut Vec<ActionProvenance>, arguments: &[wir::ValueId]| {
652        output.push(ActionProvenance {
653            span: None,
654            arguments: arguments
655                .iter()
656                .map(|value| storage.values.get(*value).and_then(|value| value.span))
657                .collect(),
658        });
659    };
660    match action {
661        wir::Action::SetGlobalVariable { value, .. }
662        | wir::Action::ModifyGlobalVariable { value, .. } => push(output, &[*value]),
663        wir::Action::SetPlayerVariable { player, value, .. }
664        | wir::Action::ModifyPlayerVariable { player, value, .. } => {
665            push(output, &[*player, *value])
666        }
667        wir::Action::AssignMember { target, value, .. } => push(output, &[*target, *value]),
668        wir::Action::CallSubroutine { .. } => push(output, &[]),
669        wir::Action::If {
670            branches,
671            else_body,
672            ..
673        } => {
674            for (index, branch) in branches.iter().enumerate() {
675                if index == 0 {
676                    push(output, &[branch.condition]);
677                } else {
678                    push_without_span(output, &[branch.condition]);
679                }
680                for action in &branch.body {
681                    public_action_provenance(storage, *action, output)?;
682                }
683                if index + 1 == branches.len() && else_body.is_none() {
684                    push_without_span(output, &[]);
685                }
686            }
687            if let Some(body) = else_body {
688                push_without_span(output, &[]);
689                for action in body {
690                    public_action_provenance(storage, *action, output)?;
691                }
692                push_without_span(output, &[]);
693            }
694        }
695        wir::Action::While {
696            condition, body, ..
697        } => {
698            push(output, &[*condition]);
699            for action in body {
700                public_action_provenance(storage, *action, output)?;
701            }
702            push_without_span(output, &[]);
703        }
704        wir::Action::ForGlobalVariable {
705            start,
706            stop,
707            step,
708            body,
709            ..
710        } => {
711            push(output, &[*start, *stop, *step]);
712            for action in body {
713                public_action_provenance(storage, *action, output)?;
714            }
715            push_without_span(output, &[]);
716        }
717        wir::Action::ForPlayerVariable {
718            player,
719            start,
720            stop,
721            step,
722            body,
723            ..
724        } => {
725            push(output, &[*player, *start, *stop, *step]);
726            for action in body {
727                public_action_provenance(storage, *action, output)?;
728            }
729            push_without_span(output, &[]);
730        }
731        wir::Action::Call { args, .. } => push(output, args),
732    }
733    Ok(())
734}
735
736fn lower_actions(
737    actions: &[Action],
738    position: &mut usize,
739    output: &mut Vec<wir::ActionId>,
740    storage: &mut wir::Program,
741    globals: &HashMap<String, wir::GlobalVarId>,
742    players: &HashMap<String, wir::PlayerVarId>,
743    subroutines: &HashMap<String, wir::SubroutineId>,
744) -> Result<()> {
745    while *position < actions.len() {
746        match &actions[*position] {
747            Action::ElseIf { .. } | Action::Else | Action::End => return Ok(()),
748            Action::If { condition } => {
749                *position += 1;
750                let mut branches = vec![wir::IfBranch {
751                    condition: wir_value(condition, storage, globals, players, subroutines)?,
752                    body: Vec::new(),
753                }];
754                lower_actions(
755                    actions,
756                    position,
757                    &mut branches[0].body,
758                    storage,
759                    globals,
760                    players,
761                    subroutines,
762                )?;
763                while let Some(Action::ElseIf { condition }) = actions.get(*position) {
764                    *position += 1;
765                    let mut body = Vec::new();
766                    lower_actions(
767                        actions,
768                        position,
769                        &mut body,
770                        storage,
771                        globals,
772                        players,
773                        subroutines,
774                    )?;
775                    branches.push(wir::IfBranch {
776                        condition: wir_value(condition, storage, globals, players, subroutines)?,
777                        body,
778                    });
779                }
780                let else_body = if matches!(actions.get(*position), Some(Action::Else)) {
781                    *position += 1;
782                    let mut body = Vec::new();
783                    lower_actions(
784                        actions,
785                        position,
786                        &mut body,
787                        storage,
788                        globals,
789                        players,
790                        subroutines,
791                    )?;
792                    Some(body)
793                } else {
794                    None
795                };
796                if !matches!(actions.get(*position), Some(Action::End)) {
797                    return Err(WorkshopError::Malformed {
798                        message: "control-flow action is missing End".to_string(),
799                        span: None,
800                    });
801                }
802                *position += 1;
803                output.push(storage.actions.push(wir::Action::If {
804                    branches,
805                    else_body,
806                    span: None,
807                }));
808            }
809            Action::While { condition } => {
810                *position += 1;
811                let mut body = Vec::new();
812                lower_actions(
813                    actions,
814                    position,
815                    &mut body,
816                    storage,
817                    globals,
818                    players,
819                    subroutines,
820                )?;
821                require_end(actions, position)?;
822                let condition = wir_value(condition, storage, globals, players, subroutines)?;
823                output.push(storage.actions.push(wir::Action::While {
824                    condition,
825                    body,
826                    span: None,
827                }));
828            }
829            Action::ForGlobalVariable {
830                variable,
831                start,
832                stop,
833                step,
834            } => {
835                *position += 1;
836                let mut body = Vec::new();
837                lower_actions(
838                    actions,
839                    position,
840                    &mut body,
841                    storage,
842                    globals,
843                    players,
844                    subroutines,
845                )?;
846                require_end(actions, position)?;
847                let variable = *globals
848                    .get(variable)
849                    .ok_or_else(|| unknown_name("global variable", variable))?;
850                let start = wir_value(start, storage, globals, players, subroutines)?;
851                let stop = wir_value(stop, storage, globals, players, subroutines)?;
852                let step = wir_value(step, storage, globals, players, subroutines)?;
853                output.push(storage.actions.push(wir::Action::ForGlobalVariable {
854                    variable,
855                    start,
856                    stop,
857                    step,
858                    body,
859                    span: None,
860                    target_span: None,
861                }));
862            }
863            Action::ForPlayerVariable {
864                player,
865                variable,
866                start,
867                stop,
868                step,
869            } => {
870                *position += 1;
871                let mut body = Vec::new();
872                lower_actions(
873                    actions,
874                    position,
875                    &mut body,
876                    storage,
877                    globals,
878                    players,
879                    subroutines,
880                )?;
881                require_end(actions, position)?;
882                let player = wir_value(player, storage, globals, players, subroutines)?;
883                let variable = *players
884                    .get(variable)
885                    .ok_or_else(|| unknown_name("player variable", variable))?;
886                let start = wir_value(start, storage, globals, players, subroutines)?;
887                let stop = wir_value(stop, storage, globals, players, subroutines)?;
888                let step = wir_value(step, storage, globals, players, subroutines)?;
889                output.push(storage.actions.push(wir::Action::ForPlayerVariable {
890                    player,
891                    variable,
892                    start,
893                    stop,
894                    step,
895                    body,
896                    span: None,
897                }));
898            }
899            action => {
900                *position += 1;
901                let lowered = wir_action(action, storage, globals, players, subroutines)?;
902                output.push(lowered);
903            }
904        }
905    }
906    Ok(())
907}
908
909fn apply_action_provenance(
910    storage: &mut wir::Program,
911    actions: &[wir::ActionId],
912    provenance: &[ActionProvenance],
913    position: &mut usize,
914) -> Result<()> {
915    for id in actions {
916        let action = storage
917            .actions
918            .get(*id)
919            .cloned()
920            .ok_or_else(|| malformed_id("action", id.index()))?;
921        match action {
922            wir::Action::If {
923                branches,
924                else_body,
925                ..
926            } => {
927                let source = provenance.get(*position).cloned().unwrap_or_default();
928                *position += 1;
929                apply_action_source(storage, *id, &source);
930                for (branch_index, branch) in branches.iter().enumerate() {
931                    if branch_index > 0 {
932                        let source = provenance.get(*position).cloned().unwrap_or_default();
933                        *position += 1;
934                        set_value_span(
935                            storage,
936                            branch.condition,
937                            source.arguments.first().copied().flatten(),
938                        );
939                    }
940                    apply_action_provenance(storage, &branch.body, provenance, position)?;
941                }
942                if let Some(body) = else_body {
943                    *position += 1;
944                    apply_action_provenance(storage, &body, provenance, position)?;
945                }
946                *position += 1;
947            }
948            wir::Action::While {
949                condition, body, ..
950            } => {
951                let source = provenance.get(*position).cloned().unwrap_or_default();
952                *position += 1;
953                apply_action_source(storage, *id, &source);
954                apply_action_provenance(storage, &body, provenance, position)?;
955                *position += 1;
956                set_value_span(
957                    storage,
958                    condition,
959                    source.arguments.first().copied().flatten(),
960                );
961            }
962            wir::Action::ForGlobalVariable {
963                start,
964                stop,
965                step,
966                body,
967                ..
968            } => {
969                let source = provenance.get(*position).cloned().unwrap_or_default();
970                *position += 1;
971                apply_action_source(storage, *id, &source);
972                apply_action_provenance(storage, &body, provenance, position)?;
973                *position += 1;
974                for (value, span) in [start, stop, step].into_iter().zip(source.arguments) {
975                    set_value_span(storage, value, span);
976                }
977            }
978            wir::Action::ForPlayerVariable {
979                player,
980                start,
981                stop,
982                step,
983                body,
984                ..
985            } => {
986                let source = provenance.get(*position).cloned().unwrap_or_default();
987                *position += 1;
988                apply_action_source(storage, *id, &source);
989                apply_action_provenance(storage, &body, provenance, position)?;
990                *position += 1;
991                for (value, span) in [player, start, stop, step]
992                    .into_iter()
993                    .zip(source.arguments)
994                {
995                    set_value_span(storage, value, span);
996                }
997            }
998            _ => {
999                let source = provenance.get(*position).cloned().unwrap_or_default();
1000                *position += 1;
1001                apply_action_source(storage, *id, &source);
1002            }
1003        }
1004    }
1005    Ok(())
1006}
1007
1008fn apply_action_source(storage: &mut wir::Program, id: wir::ActionId, source: &ActionProvenance) {
1009    let arguments = source.arguments.clone();
1010    if let Some(action) = storage.actions.get_mut(id) {
1011        match action {
1012            wir::Action::SetGlobalVariable { span, .. }
1013            | wir::Action::ModifyGlobalVariable { span, .. }
1014            | wir::Action::SetPlayerVariable { span, .. }
1015            | wir::Action::ModifyPlayerVariable { span, .. }
1016            | wir::Action::AssignMember { span, .. }
1017            | wir::Action::CallSubroutine { span, .. }
1018            | wir::Action::If { span, .. }
1019            | wir::Action::While { span, .. }
1020            | wir::Action::ForGlobalVariable { span, .. }
1021            | wir::Action::ForPlayerVariable { span, .. }
1022            | wir::Action::Call { span, .. } => *span = source.span,
1023        }
1024    }
1025    let value_ids = storage
1026        .actions
1027        .get(id)
1028        .map(action_value_ids)
1029        .unwrap_or_default();
1030    for (value, span) in value_ids.into_iter().zip(arguments) {
1031        set_value_span(storage, value, span);
1032    }
1033}
1034
1035fn action_value_ids(action: &wir::Action) -> Vec<wir::ValueId> {
1036    match action {
1037        wir::Action::SetGlobalVariable { value, .. }
1038        | wir::Action::ModifyGlobalVariable { value, .. } => vec![*value],
1039        wir::Action::SetPlayerVariable { player, value, .. }
1040        | wir::Action::ModifyPlayerVariable { player, value, .. } => vec![*player, *value],
1041        wir::Action::AssignMember { target, value, .. } => vec![*target, *value],
1042        wir::Action::If { branches, .. } => {
1043            branches.iter().map(|branch| branch.condition).collect()
1044        }
1045        wir::Action::While { condition, .. } => vec![*condition],
1046        wir::Action::ForGlobalVariable {
1047            start, stop, step, ..
1048        } => vec![*start, *stop, *step],
1049        wir::Action::ForPlayerVariable {
1050            player,
1051            start,
1052            stop,
1053            step,
1054            ..
1055        } => vec![*player, *start, *stop, *step],
1056        wir::Action::Call { args, .. } => args.clone(),
1057        wir::Action::CallSubroutine { .. } => Vec::new(),
1058    }
1059}
1060
1061fn set_value_span(
1062    storage: &mut wir::Program,
1063    value: wir::ValueId,
1064    span: Option<crate::source::Span>,
1065) {
1066    if let Some(node) = storage.values.get_mut(value) {
1067        node.span = span;
1068    }
1069}
1070
1071fn require_end(actions: &[Action], position: &mut usize) -> Result<()> {
1072    if !matches!(actions.get(*position), Some(Action::End)) {
1073        return Err(WorkshopError::Malformed {
1074            message: "control-flow action is missing End".to_string(),
1075            span: None,
1076        });
1077    }
1078    *position += 1;
1079    Ok(())
1080}
1081
1082fn wir_action(
1083    action: &Action,
1084    storage: &mut wir::Program,
1085    globals: &HashMap<String, wir::GlobalVarId>,
1086    players: &HashMap<String, wir::PlayerVarId>,
1087    subroutines: &HashMap<String, wir::SubroutineId>,
1088) -> Result<wir::ActionId> {
1089    let action = match action {
1090        Action::SetGlobalVariable { variable, value } => wir::Action::SetGlobalVariable {
1091            variable: *globals
1092                .get(variable)
1093                .ok_or_else(|| unknown_name("global variable", variable))?,
1094            value: wir_value(value, storage, globals, players, subroutines)?,
1095            span: None,
1096            target_span: None,
1097        },
1098        Action::ModifyGlobalVariable {
1099            variable,
1100            op,
1101            value,
1102        } => wir::Action::ModifyGlobalVariable {
1103            variable: *globals
1104                .get(variable)
1105                .ok_or_else(|| unknown_name("global variable", variable))?,
1106            op: wir_modify(*op),
1107            value: wir_value(value, storage, globals, players, subroutines)?,
1108            span: None,
1109            target_span: None,
1110        },
1111        Action::SetPlayerVariable {
1112            player,
1113            variable,
1114            value,
1115        } => wir::Action::SetPlayerVariable {
1116            player: wir_value(player, storage, globals, players, subroutines)?,
1117            variable: *players
1118                .get(variable)
1119                .ok_or_else(|| unknown_name("player variable", variable))?,
1120            value: wir_value(value, storage, globals, players, subroutines)?,
1121            span: None,
1122            target_span: None,
1123        },
1124        Action::ModifyPlayerVariable {
1125            player,
1126            variable,
1127            op,
1128            value,
1129        } => wir::Action::ModifyPlayerVariable {
1130            player: wir_value(player, storage, globals, players, subroutines)?,
1131            variable: *players
1132                .get(variable)
1133                .ok_or_else(|| unknown_name("player variable", variable))?,
1134            op: wir_modify(*op),
1135            value: wir_value(value, storage, globals, players, subroutines)?,
1136            span: None,
1137            target_span: None,
1138        },
1139        Action::AssignMember { target, op, value } => wir::Action::AssignMember {
1140            target: wir_value(target, storage, globals, players, subroutines)?,
1141            op: op.map(wir_modify),
1142            value: wir_value(value, storage, globals, players, subroutines)?,
1143            span: None,
1144        },
1145        Action::CallSubroutine { subroutine } => wir::Action::CallSubroutine {
1146            subroutine: *subroutines
1147                .get(subroutine)
1148                .ok_or_else(|| unknown_name("subroutine", subroutine))?,
1149            span: None,
1150            callee_span: None,
1151        },
1152        Action::Disabled { .. } => {
1153            return Err(WorkshopError::Unsupported {
1154                message: "disabled actions are not representable by the canonical storage model"
1155                    .to_string(),
1156                span: None,
1157            });
1158        }
1159        Action::Call { name, args } => wir::Action::Call {
1160            name: name.clone(),
1161            args: args
1162                .iter()
1163                .map(|arg| wir_value(arg, storage, globals, players, subroutines))
1164                .collect::<Result<Vec<_>>>()?,
1165            span: None,
1166        },
1167        Action::ElseIf { .. }
1168        | Action::Else
1169        | Action::End
1170        | Action::If { .. }
1171        | Action::While { .. }
1172        | Action::ForGlobalVariable { .. }
1173        | Action::ForPlayerVariable { .. } => {
1174            unreachable!("structured actions are lowered by lower_actions")
1175        }
1176    };
1177    Ok(storage.actions.push(action))
1178}
1179
1180fn wir_value(
1181    value: &Value,
1182    storage: &mut wir::Program,
1183    globals: &HashMap<String, wir::GlobalVarId>,
1184    players: &HashMap<String, wir::PlayerVarId>,
1185    subroutines: &HashMap<String, wir::SubroutineId>,
1186) -> Result<wir::ValueId> {
1187    let value = match value {
1188        Value::Number(value) => wir::Value::Number {
1189            value: *value,
1190            text: crate::core::format::format_number(*value),
1191        },
1192        Value::String(value) => wir::Value::String(value.clone()),
1193        Value::LocalizedString(value) => wir::Value::LocalizedString(value.clone()),
1194        Value::Bool(value) => wir::Value::Bool(*value),
1195        Value::Null => wir::Value::Null,
1196        Value::Array(values) => wir::Value::Array(
1197            values
1198                .iter()
1199                .map(|value| wir_value(value, storage, globals, players, subroutines))
1200                .collect::<Result<Vec<_>>>()?,
1201        ),
1202        Value::Vector { x, y, z } => wir::Value::Vector {
1203            x: wir_value(x, storage, globals, players, subroutines)?,
1204            y: wir_value(y, storage, globals, players, subroutines)?,
1205            z: wir_value(z, storage, globals, players, subroutines)?,
1206        },
1207        Value::Enum { value_type, value } => wir::Value::Enum {
1208            value_type: value_type.clone(),
1209            value: value.clone(),
1210        },
1211        Value::GlobalVariable(name) => wir::Value::GlobalVariable(
1212            *globals
1213                .get(name)
1214                .ok_or_else(|| unknown_name("global variable", name))?,
1215        ),
1216        Value::PlayerVariable { player, variable } => wir::Value::PlayerVariable {
1217            player: wir_value(player, storage, globals, players, subroutines)?,
1218            variable: *players
1219                .get(variable)
1220                .ok_or_else(|| unknown_name("player variable", variable))?,
1221        },
1222        Value::Subroutine(name) => wir::Value::Subroutine(
1223            *subroutines
1224                .get(name)
1225                .ok_or_else(|| unknown_name("subroutine", name))?,
1226        ),
1227        Value::EventPlayer => wir::Value::EventPlayer,
1228        Value::Call { name, args } => wir::Value::Call {
1229            name: name.clone(),
1230            args: args
1231                .iter()
1232                .map(|arg| wir_value(arg, storage, globals, players, subroutines))
1233                .collect::<Result<Vec<_>>>()?,
1234        },
1235    };
1236    Ok(storage.values.push(wir::ValueNode::new(value, None)))
1237}
1238
1239fn wir_event(
1240    event: &Event,
1241    subroutines: &HashMap<String, wir::SubroutineId>,
1242) -> Result<wir::Event> {
1243    Ok(match event {
1244        Event::Global => wir::Event::Global,
1245        Event::EachPlayer => wir::Event::EachPlayer,
1246        Event::EachPlayerWithFilters { team, target } => wir::Event::EachPlayerWithFilters {
1247            team: wir_team(*team),
1248            target: wir_target(target),
1249        },
1250        Event::Player { kind, team, target } => wir::Event::Player {
1251            kind: wir_player_event(*kind),
1252            team: wir_team(*team),
1253            target: wir_target(target),
1254        },
1255        Event::Subroutine(name) => wir::Event::Subroutine(
1256            *subroutines
1257                .get(name)
1258                .ok_or_else(|| unknown_name("subroutine", name))?,
1259        ),
1260    })
1261}
1262
1263fn wir_team(team: EventTeam) -> wir::EventTeam {
1264    match team {
1265        EventTeam::All => wir::EventTeam::All,
1266        EventTeam::Team1 => wir::EventTeam::Team1,
1267        EventTeam::Team2 => wir::EventTeam::Team2,
1268    }
1269}
1270
1271fn wir_target(target: &EventTarget) -> wir::EventTarget {
1272    match target {
1273        EventTarget::All => wir::EventTarget::All,
1274        EventTarget::Slot(slot) => wir::EventTarget::Slot(*slot),
1275        EventTarget::Hero(hero) => wir::EventTarget::Hero(hero.clone()),
1276    }
1277}
1278
1279fn wir_player_event(kind: PlayerEventKind) -> wir::PlayerEventKind {
1280    match kind {
1281        PlayerEventKind::DealtDamage => wir::PlayerEventKind::DealtDamage,
1282        PlayerEventKind::DealtFinalBlow => wir::PlayerEventKind::DealtFinalBlow,
1283        PlayerEventKind::DealtHealing => wir::PlayerEventKind::DealtHealing,
1284        PlayerEventKind::DealtKnockback => wir::PlayerEventKind::DealtKnockback,
1285        PlayerEventKind::Died => wir::PlayerEventKind::Died,
1286        PlayerEventKind::EarnedElimination => wir::PlayerEventKind::EarnedElimination,
1287        PlayerEventKind::Joined => wir::PlayerEventKind::Joined,
1288        PlayerEventKind::Left => wir::PlayerEventKind::Left,
1289        PlayerEventKind::ReceivedHealing => wir::PlayerEventKind::ReceivedHealing,
1290        PlayerEventKind::ReceivedKnockback => wir::PlayerEventKind::ReceivedKnockback,
1291        PlayerEventKind::TookDamage => wir::PlayerEventKind::TookDamage,
1292    }
1293}
1294
1295fn public_modify(op: wir::ModifyOp) -> ModifyOp {
1296    match op {
1297        wir::ModifyOp::Add => ModifyOp::Add,
1298        wir::ModifyOp::Subtract => ModifyOp::Subtract,
1299        wir::ModifyOp::Multiply => ModifyOp::Multiply,
1300        wir::ModifyOp::Divide => ModifyOp::Divide,
1301        wir::ModifyOp::Modulo => ModifyOp::Modulo,
1302        wir::ModifyOp::Min => ModifyOp::Min,
1303        wir::ModifyOp::Max => ModifyOp::Max,
1304        wir::ModifyOp::RaiseToPower => ModifyOp::RaiseToPower,
1305        wir::ModifyOp::AppendToArray => ModifyOp::AppendToArray,
1306        wir::ModifyOp::RemoveFromArray => ModifyOp::RemoveFromArray,
1307        wir::ModifyOp::RemoveFromArrayByIndex => ModifyOp::RemoveFromArrayByIndex,
1308    }
1309}
1310
1311fn wir_modify(op: ModifyOp) -> wir::ModifyOp {
1312    match op {
1313        ModifyOp::Add => wir::ModifyOp::Add,
1314        ModifyOp::Subtract => wir::ModifyOp::Subtract,
1315        ModifyOp::Multiply => wir::ModifyOp::Multiply,
1316        ModifyOp::Divide => wir::ModifyOp::Divide,
1317        ModifyOp::Modulo => wir::ModifyOp::Modulo,
1318        ModifyOp::Min => wir::ModifyOp::Min,
1319        ModifyOp::Max => wir::ModifyOp::Max,
1320        ModifyOp::RaiseToPower => wir::ModifyOp::RaiseToPower,
1321        ModifyOp::AppendToArray => wir::ModifyOp::AppendToArray,
1322        ModifyOp::RemoveFromArray => wir::ModifyOp::RemoveFromArray,
1323        ModifyOp::RemoveFromArrayByIndex => wir::ModifyOp::RemoveFromArrayByIndex,
1324    }
1325}
1326
1327fn malformed_id(kind: &str, index: usize) -> WorkshopError {
1328    WorkshopError::Malformed {
1329        message: format!("dangling {kind} {index}"),
1330        span: None,
1331    }
1332}
1333
1334fn unknown_name(kind: &str, name: &str) -> WorkshopError {
1335    WorkshopError::Malformed {
1336        message: format!("unknown {kind} '{name}'"),
1337        span: None,
1338    }
1339}
1340
1341/// A Workshop global or player variable declaration.
1342#[derive(Debug, Clone, PartialEq, Eq)]
1343pub struct Variable {
1344    pub name: String,
1345    /// The raw Workshop declaration index, when the declaration has one.
1346    pub index: Option<u32>,
1347}
1348
1349impl Variable {
1350    pub fn new(name: impl Into<String>) -> Self {
1351        Self {
1352            name: name.into(),
1353            index: None,
1354        }
1355    }
1356
1357    pub fn with_index(name: impl Into<String>, index: u32) -> Self {
1358        Self {
1359            name: name.into(),
1360            index: Some(index),
1361        }
1362    }
1363}
1364
1365/// A Workshop subroutine declaration.
1366#[derive(Debug, Clone, PartialEq, Eq)]
1367pub struct Subroutine {
1368    pub name: String,
1369    /// The raw Workshop declaration index, when the declaration has one.
1370    pub index: Option<u32>,
1371}
1372
1373impl Subroutine {
1374    pub fn new(name: impl Into<String>) -> Self {
1375        Self {
1376            name: name.into(),
1377            index: None,
1378        }
1379    }
1380
1381    pub fn with_index(name: impl Into<String>, index: u32) -> Self {
1382        Self {
1383            name: name.into(),
1384            index: Some(index),
1385        }
1386    }
1387}
1388
1389/// A Workshop rule with explicit conditions and a linear action stream.
1390#[derive(Debug, Clone)]
1391pub struct Rule {
1392    pub name: String,
1393    pub disabled: bool,
1394    pub event: Event,
1395    pub conditions: Vec<Condition>,
1396    pub actions: Vec<Action>,
1397}
1398
1399impl Rule {
1400    pub fn new(name: impl Into<String>, event: Event) -> Self {
1401        Self {
1402            name: name.into(),
1403            disabled: false,
1404            event,
1405            conditions: Vec::new(),
1406            actions: Vec::new(),
1407        }
1408    }
1409
1410    pub fn condition(mut self, condition: impl Into<Condition>) -> Self {
1411        self.conditions.push(condition.into());
1412        self
1413    }
1414
1415    pub fn action(mut self, action: Action) -> Self {
1416        self.actions.push(action);
1417        self
1418    }
1419}
1420
1421/// A rule condition. Conditions remain distinct from general value expressions.
1422#[derive(Debug, Clone)]
1423pub struct Condition {
1424    pub value: Value,
1425    pub disabled: bool,
1426}
1427
1428impl Condition {
1429    pub fn new(value: Value) -> Self {
1430        Self {
1431            value,
1432            disabled: false,
1433        }
1434    }
1435
1436    pub fn disabled(value: Value) -> Self {
1437        Self {
1438            value,
1439            disabled: true,
1440        }
1441    }
1442}
1443
1444impl From<Value> for Condition {
1445    fn from(value: Value) -> Self {
1446        Self::new(value)
1447    }
1448}
1449
1450/// A Workshop event identity and its native filters.
1451#[derive(Debug, Clone, PartialEq, Eq)]
1452pub enum Event {
1453    Global,
1454    EachPlayer,
1455    EachPlayerWithFilters {
1456        team: EventTeam,
1457        target: EventTarget,
1458    },
1459    Player {
1460        kind: PlayerEventKind,
1461        team: EventTeam,
1462        target: EventTarget,
1463    },
1464    Subroutine(String),
1465}
1466
1467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1468pub enum EventTeam {
1469    All,
1470    Team1,
1471    Team2,
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Eq)]
1475pub enum EventTarget {
1476    All,
1477    Slot(u8),
1478    Hero(String),
1479}
1480
1481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1482pub enum PlayerEventKind {
1483    DealtDamage,
1484    DealtFinalBlow,
1485    DealtHealing,
1486    DealtKnockback,
1487    Died,
1488    EarnedElimination,
1489    Joined,
1490    Left,
1491    ReceivedHealing,
1492    ReceivedKnockback,
1493    TookDamage,
1494}
1495
1496/// A Workshop action line. Control flow is represented in the same order as
1497/// the Workshop source, including its explicit `End` lines.
1498#[derive(Debug, Clone)]
1499pub enum Action {
1500    SetGlobalVariable {
1501        variable: String,
1502        value: Value,
1503    },
1504    ModifyGlobalVariable {
1505        variable: String,
1506        op: ModifyOp,
1507        value: Value,
1508    },
1509    SetPlayerVariable {
1510        player: Value,
1511        variable: String,
1512        value: Value,
1513    },
1514    ModifyPlayerVariable {
1515        player: Value,
1516        variable: String,
1517        op: ModifyOp,
1518        value: Value,
1519    },
1520    AssignMember {
1521        target: Value,
1522        op: Option<ModifyOp>,
1523        value: Value,
1524    },
1525    CallSubroutine {
1526        subroutine: String,
1527    },
1528    If {
1529        condition: Value,
1530    },
1531    ElseIf {
1532        condition: Value,
1533    },
1534    Else,
1535    While {
1536        condition: Value,
1537    },
1538    ForGlobalVariable {
1539        variable: String,
1540        start: Value,
1541        stop: Value,
1542        step: Value,
1543    },
1544    ForPlayerVariable {
1545        player: Value,
1546        variable: String,
1547        start: Value,
1548        stop: Value,
1549        step: Value,
1550    },
1551    End,
1552    Disabled {
1553        action: Box<Action>,
1554    },
1555    Call {
1556        name: String,
1557        args: Vec<Value>,
1558    },
1559}
1560
1561impl Action {
1562    /// Mark an action as disabled.
1563    pub fn disabled(action: Action) -> Self {
1564        Self::Disabled {
1565            action: Box::new(action),
1566        }
1567    }
1568
1569    /// Construct a dynamic action call by canonical Workshop id.
1570    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Value>) -> Self {
1571        Self::Call {
1572            name: name.into(),
1573            args: args.into_iter().collect(),
1574        }
1575    }
1576}
1577
1578/// The operation used by a Workshop variable modification action.
1579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1580pub enum ModifyOp {
1581    Add,
1582    Subtract,
1583    Multiply,
1584    Divide,
1585    Modulo,
1586    Min,
1587    Max,
1588    RaiseToPower,
1589    AppendToArray,
1590    RemoveFromArray,
1591    RemoveFromArrayByIndex,
1592}
1593
1594/// A composable Workshop value expression.
1595#[derive(Debug, Clone)]
1596pub enum Value {
1597    Number(f64),
1598    String(String),
1599    LocalizedString(String),
1600    Bool(bool),
1601    Null,
1602    Array(Vec<Value>),
1603    Vector {
1604        x: Box<Value>,
1605        y: Box<Value>,
1606        z: Box<Value>,
1607    },
1608    Enum {
1609        value_type: String,
1610        value: String,
1611    },
1612    GlobalVariable(String),
1613    PlayerVariable {
1614        player: Box<Value>,
1615        variable: String,
1616    },
1617    Subroutine(String),
1618    EventPlayer,
1619    Call {
1620        name: String,
1621        args: Vec<Value>,
1622    },
1623}
1624
1625impl Value {
1626    /// Construct a numeric Workshop literal.
1627    pub fn number(value: f64) -> Self {
1628        Self::Number(value)
1629    }
1630
1631    /// Construct a custom Workshop string literal.
1632    pub fn string(value: impl Into<String>) -> Self {
1633        Self::String(value.into())
1634    }
1635
1636    pub fn global_variable(name: impl Into<String>) -> Self {
1637        Self::GlobalVariable(name.into())
1638    }
1639
1640    pub fn player_variable(player: Value, name: impl Into<String>) -> Self {
1641        Self::PlayerVariable {
1642            player: Box::new(player),
1643            variable: name.into(),
1644        }
1645    }
1646
1647    /// Construct a dynamic value call by canonical Workshop id.
1648    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Value>) -> Self {
1649        Self::Call {
1650            name: name.into(),
1651            args: args.into_iter().collect(),
1652        }
1653    }
1654}
1655
1656impl From<bool> for Value {
1657    fn from(value: bool) -> Self {
1658        Self::Bool(value)
1659    }
1660}
1661
1662impl From<f64> for Value {
1663    fn from(value: f64) -> Self {
1664        Self::Number(value)
1665    }
1666}
1667
1668impl From<f32> for Value {
1669    fn from(value: f32) -> Self {
1670        Self::Number(f64::from(value))
1671    }
1672}
1673
1674macro_rules! impl_integer_value {
1675    ($($type:ty),+ $(,)?) => {
1676        $(
1677            impl From<$type> for Value {
1678                fn from(value: $type) -> Self {
1679                    Self::Number(value as f64)
1680                }
1681            }
1682        )+
1683    };
1684}
1685
1686impl_integer_value!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
1687
1688impl From<String> for Value {
1689    fn from(value: String) -> Self {
1690        Self::String(value)
1691    }
1692}
1693
1694impl From<&str> for Value {
1695    fn from(value: &str) -> Self {
1696        Self::String(value.to_string())
1697    }
1698}
1699
1700impl<T: Into<Value>> From<Vec<T>> for Value {
1701    fn from(values: Vec<T>) -> Self {
1702        Self::Array(values.into_iter().map(Into::into).collect())
1703    }
1704}
1705
1706impl<T: Into<Value>, const N: usize> From<[T; N]> for Value {
1707    fn from(values: [T; N]) -> Self {
1708        Self::Array(values.into_iter().map(Into::into).collect())
1709    }
1710}
1711
1712include!(concat!(env!("OUT_DIR"), "/typed_api.rs"));