Skip to main content

workshop_rs/wir/
mod.rs

1//! The Workshop IR model.
2//!
3//! Workshop IR models the lower-level workshop program structure: variables
4//! with indexes, subroutines with indexes, and rules with events, conditions,
5//! actions, and values. It is locale-independent (canonical catalog ids only,
6//! never localized spellings) and protocol-agnostic.
7//!
8//! Name policy: call/value `name` fields keep the canonical catalog ids
9//! (`countOf`, `wait`, `createBeamEffect`); mapping those to localized
10//! Workshop presentation spellings is an emission concern.
11//!
12//! Extracted from the Wright-authored `wright-ir` crate (the `wir`,
13//! `settings`, and `source` modules); see
14//! [`docs/provenance.md`](https://github.com/wrightkit/workshop-rs/blob/main/docs/provenance.md).
15
16mod dump;
17mod validate;
18
19pub mod error;
20
21/// The WIR-owned capability surface used by the canonical census. Providers
22/// do not contribute source-language inventories to this registry.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CensusCapabilityKind {
25    Variable,
26    PlayerVariable,
27    Subroutine,
28    ControlFlow,
29    String,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct CensusCapability {
34    pub kind: CensusCapabilityKind,
35    pub name: &'static str,
36}
37
38pub const CENSUS_CAPABILITIES: &[CensusCapability] = &[
39    CensusCapability {
40        kind: CensusCapabilityKind::Variable,
41        name: "global",
42    },
43    CensusCapability {
44        kind: CensusCapabilityKind::PlayerVariable,
45        name: "player",
46    },
47    CensusCapability {
48        kind: CensusCapabilityKind::Subroutine,
49        name: "declaration-and-call",
50    },
51    CensusCapability {
52        kind: CensusCapabilityKind::ControlFlow,
53        name: "if",
54    },
55    CensusCapability {
56        kind: CensusCapabilityKind::ControlFlow,
57        name: "else-if",
58    },
59    CensusCapability {
60        kind: CensusCapabilityKind::ControlFlow,
61        name: "else",
62    },
63    CensusCapability {
64        kind: CensusCapabilityKind::ControlFlow,
65        name: "while",
66    },
67    CensusCapability {
68        kind: CensusCapabilityKind::ControlFlow,
69        name: "for-global-variable",
70    },
71    CensusCapability {
72        kind: CensusCapabilityKind::String,
73        name: "custom-string",
74    },
75];
76
77use crate::arena::Arena;
78use crate::ids::Id;
79use crate::source::{SourceFile, Span};
80
81/// A typed ID referencing a [`WorkshopVariable`] in the global table.
82pub type GlobalVarId = Id<WorkshopVariable>;
83/// A typed ID referencing a [`WorkshopVariable`] in the player table.
84pub type PlayerVarId = Id<WorkshopVariable>;
85/// A typed ID referencing a [`WorkshopSubroutine`].
86pub type SubroutineId = Id<WorkshopSubroutine>;
87/// A typed ID referencing a [`Rule`].
88pub type RuleId = Id<Rule>;
89/// A typed ID referencing an [`Action`] in the action arena.
90pub type ActionId = Id<Action>;
91/// A typed ID referencing a [`ValueNode`] in the value arena.
92pub type ValueId = Id<ValueNode>;
93
94/// The Workshop IR program: tables and arenas produced by lowering.
95#[derive(Debug, Clone)]
96pub struct Program {
97    /// The source-file registry, copied from the source HIR so spans remain
98    /// resolvable for diagnostics.
99    pub files: Arena<SourceFile>,
100    /// The custom-game-settings carrier, copied inertly from the source HIR
101    /// (emitted verbatim, never lowered, #86).
102    pub settings: Option<crate::settings::Settings>,
103    pub global_variables: Arena<WorkshopVariable>,
104    pub player_variables: Arena<WorkshopVariable>,
105    pub subroutines: Arena<WorkshopSubroutine>,
106    pub rules: Arena<Rule>,
107    pub values: Arena<ValueNode>,
108    pub actions: Arena<Action>,
109}
110
111impl Default for Program {
112    fn default() -> Self {
113        Program {
114            files: Arena::new(),
115            settings: None,
116            global_variables: Arena::new(),
117            player_variables: Arena::new(),
118            subroutines: Arena::new(),
119            rules: Arena::new(),
120            values: Arena::new(),
121            actions: Arena::new(),
122        }
123    }
124}
125
126impl Program {
127    /// Validate structural invariants: every ID resolves and every span is
128    /// valid. Returns the first violation as a structured [`IrError`].
129    ///
130    /// [`IrError`]: crate::wir::error::IrError
131    pub fn validate(&self) -> Result<(), error::IrError> {
132        validate::validate(self)
133    }
134
135    /// Report preserved or unknown constructs separately from structural
136    /// validation so consumers cannot present analysis as definitive.
137    pub fn semantic_issues(
138        &self,
139        catalog: &crate::catalog::Catalog,
140    ) -> Vec<crate::semantic::SemanticIssue> {
141        crate::semantic::inspect(self, catalog)
142    }
143
144    /// Render a deterministic debug dump of the workshop program.
145    pub fn dump(&self) -> String {
146        dump::dump(self)
147    }
148}
149
150/// A workshop variable (global or player) with its assigned index.
151///
152/// Declaration initializers are lowered into synthetic "Initialize global
153/// variables" / "Initialize player variables" rules during HIR → WIR lowering
154/// (#112); the variable tables carry no initializer field, so the Initialize
155/// rules are the single source of truth.
156#[derive(Debug, Clone)]
157pub struct WorkshopVariable {
158    pub name: String,
159    /// The workshop variable index assigned during lowering.
160    pub index: u32,
161    pub span: Option<Span>,
162    /// The exact span of the declared identifier token.
163    pub name_span: Option<Span>,
164}
165
166/// A workshop subroutine with its assigned index.
167#[derive(Debug, Clone)]
168pub struct WorkshopSubroutine {
169    pub name: String,
170    pub index: u32,
171    pub span: Option<Span>,
172    /// The exact span of the declared identifier token.
173    pub name_span: Option<Span>,
174}
175
176/// A workshop rule.
177#[derive(Debug, Clone)]
178pub struct Rule {
179    pub name: String,
180    pub span: Option<Span>,
181    /// The exact span of the rule name inside its string literal.
182    pub name_span: Option<Span>,
183    pub disabled: bool,
184    pub event: Event,
185    pub conditions: Vec<ValueId>,
186    pub actions: Vec<ActionId>,
187}
188
189/// The team filter attached to a player-scoped Workshop event.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum EventTeam {
192    All,
193    Team1,
194    Team2,
195}
196
197/// The player filter attached to a player-scoped Workshop event.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum EventTarget {
200    All,
201    Slot(u8),
202    Hero(String),
203}
204
205/// A non-ongoing player event identity.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum PlayerEventKind {
208    DealtDamage,
209    DealtFinalBlow,
210    DealtHealing,
211    DealtKnockback,
212    Died,
213    EarnedElimination,
214    Joined,
215    Left,
216    ReceivedHealing,
217    ReceivedKnockback,
218    TookDamage,
219}
220
221impl PlayerEventKind {
222    /// The locale-independent catalog identity for this event.
223    pub fn catalog_id(self) -> &'static str {
224        match self {
225            PlayerEventKind::DealtDamage => "playerDealtDamage",
226            PlayerEventKind::DealtFinalBlow => "playerDealtFinalBlow",
227            PlayerEventKind::DealtHealing => "playerDealtHealing",
228            PlayerEventKind::DealtKnockback => "playerDealtKnockback",
229            PlayerEventKind::Died => "playerDied",
230            PlayerEventKind::EarnedElimination => "playerEarnedElimination",
231            PlayerEventKind::Joined => "playerJoined",
232            PlayerEventKind::Left => "playerLeft",
233            PlayerEventKind::ReceivedHealing => "playerReceivedHealing",
234            PlayerEventKind::ReceivedKnockback => "playerReceivedKnockback",
235            PlayerEventKind::TookDamage => "playerTookDamage",
236        }
237    }
238}
239
240/// A workshop event.
241#[derive(Debug, Clone)]
242pub enum Event {
243    /// `Ongoing - Global` (from `@Event global`).
244    Global,
245    /// `Ongoing - Each Player` (from `@Event eachPlayer`).
246    EachPlayer,
247    /// `Ongoing - Each Player` with its canonical team/player filters.
248    EachPlayerWithFilters {
249        team: EventTeam,
250        target: EventTarget,
251    },
252    /// A player-scoped Workshop event with canonical filters.
253    Player {
254        kind: PlayerEventKind,
255        team: EventTeam,
256        target: EventTarget,
257    },
258    /// A subroutine body (`def name():`), referencing the subroutine.
259    Subroutine(SubroutineId),
260}
261
262/// A workshop value (expression) node with its source span.
263#[derive(Debug, Clone)]
264pub struct ValueNode {
265    pub value: Value,
266    pub span: Option<Span>,
267}
268
269/// A workshop value (expression).
270#[derive(Debug, Clone)]
271pub enum Value {
272    /// A numeric literal with its source spelling (`5`, `0.0`, `-22.05`);
273    /// computed values (constant folding) carry the formatted spelling.
274    Number {
275        value: f64,
276        text: String,
277    },
278    String(String),
279    /// A reviewed localized Workshop preset-string identity.
280    LocalizedString(String),
281    Bool(bool),
282    Null,
283    Array(Vec<ValueId>),
284    Vector {
285        x: ValueId,
286        y: ValueId,
287        z: ValueId,
288    },
289    /// A built-in enumerated value, e.g. `Team.ALL`.
290    Enum {
291        value_type: String,
292        value: String,
293    },
294    GlobalVariable(GlobalVarId),
295    PlayerVariable {
296        player: ValueId,
297        variable: PlayerVarId,
298    },
299    /// A declared Workshop subroutine referenced by a generic action such as
300    /// `Start Rule`. The identity is source-owned, not a catalog builtin.
301    Subroutine(SubroutineId),
302    EventPlayer,
303    /// A function call over workshop values.
304    Call {
305        name: String,
306        args: Vec<ValueId>,
307    },
308}
309
310impl ValueNode {
311    /// Build a value node with a source span.
312    pub fn new(value: Value, span: Option<Span>) -> Self {
313        ValueNode { value, span }
314    }
315}
316
317/// A workshop action.
318#[derive(Debug, Clone)]
319pub enum Action {
320    SetGlobalVariable {
321        variable: GlobalVarId,
322        value: ValueId,
323        span: Option<Span>,
324        /// The exact span of the assigned variable identifier.
325        target_span: Option<Span>,
326    },
327    ModifyGlobalVariable {
328        variable: GlobalVarId,
329        op: ModifyOp,
330        value: ValueId,
331        span: Option<Span>,
332        /// The exact span of the modified variable identifier.
333        target_span: Option<Span>,
334    },
335    SetPlayerVariable {
336        player: ValueId,
337        variable: PlayerVarId,
338        value: ValueId,
339        span: Option<Span>,
340        /// The exact span of the assigned variable identifier.
341        target_span: Option<Span>,
342    },
343    ModifyPlayerVariable {
344        player: ValueId,
345        variable: PlayerVarId,
346        op: ModifyOp,
347        value: ValueId,
348        span: Option<Span>,
349        /// The exact span of the modified variable identifier.
350        target_span: Option<Span>,
351    },
352    /// Assignment to a canonical Workshop member-access target, optionally
353    /// indexed. This is not a builtin catalog action; the emitter preserves
354    /// the native member-assignment syntax.
355    AssignMember {
356        target: ValueId,
357        op: Option<ModifyOp>,
358        value: ValueId,
359        span: Option<Span>,
360    },
361    CallSubroutine {
362        subroutine: SubroutineId,
363        span: Option<Span>,
364        /// The exact span of the callee identifier occurrence.
365        callee_span: Option<Span>,
366    },
367    If {
368        branches: Vec<IfBranch>,
369        else_body: Option<Vec<ActionId>>,
370        span: Option<Span>,
371    },
372    While {
373        condition: ValueId,
374        body: Vec<ActionId>,
375        span: Option<Span>,
376    },
377    ForGlobalVariable {
378        variable: GlobalVarId,
379        start: ValueId,
380        stop: ValueId,
381        step: ValueId,
382        body: Vec<ActionId>,
383        span: Option<Span>,
384        /// The exact span of the loop variable identifier.
385        target_span: Option<Span>,
386    },
387    /// `For Player Variable(player, name, start, stop, step)`: the
388    /// per-player loop form (frontend-neutral; parsed from reference
389    /// evidence, not emitted by Wright's own lowering, which models
390    /// foreach counters as globals under the declared #119 contract).
391    ForPlayerVariable {
392        player: ValueId,
393        variable: PlayerVarId,
394        start: ValueId,
395        stop: ValueId,
396        step: ValueId,
397        body: Vec<ActionId>,
398        span: Option<Span>,
399    },
400    /// Any other action call with side effects.
401    Call {
402        name: String,
403        args: Vec<ValueId>,
404        span: Option<Span>,
405    },
406}
407
408impl Action {
409    /// The source span of this action, if any.
410    pub fn span(&self) -> Option<Span> {
411        match self {
412            Action::SetGlobalVariable { span, .. }
413            | Action::ModifyGlobalVariable { span, .. }
414            | Action::SetPlayerVariable { span, .. }
415            | Action::ModifyPlayerVariable { span, .. }
416            | Action::AssignMember { span, .. }
417            | Action::CallSubroutine { span, .. }
418            | Action::If { span, .. }
419            | Action::While { span, .. }
420            | Action::ForGlobalVariable { span, .. }
421            | Action::ForPlayerVariable { span, .. }
422            | Action::Call { span, .. } => *span,
423        }
424    }
425}
426
427/// One condition/body pair of an `If` action.
428#[derive(Debug, Clone)]
429pub struct IfBranch {
430    pub condition: ValueId,
431    pub body: Vec<ActionId>,
432}
433
434/// The modify operators of the v0.1 surface.
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436pub enum ModifyOp {
437    Add,
438    Subtract,
439    Multiply,
440    Divide,
441    Modulo,
442    Min,
443    Max,
444    RaiseToPower,
445    AppendToArray,
446    RemoveFromArray,
447    RemoveFromArrayByIndex,
448}
449
450impl ModifyOp {
451    /// A short canonical name for dumps and diagnostics.
452    pub fn as_str(self) -> &'static str {
453        match self {
454            ModifyOp::Add => "Add",
455            ModifyOp::Subtract => "Subtract",
456            ModifyOp::Multiply => "Multiply",
457            ModifyOp::Divide => "Divide",
458            ModifyOp::Modulo => "Modulo",
459            ModifyOp::Min => "Min",
460            ModifyOp::Max => "Max",
461            ModifyOp::RaiseToPower => "RaiseToPower",
462            ModifyOp::AppendToArray => "AppendToArray",
463            ModifyOp::RemoveFromArray => "RemoveFromArray",
464            ModifyOp::RemoveFromArrayByIndex => "RemoveFromArrayByIndex",
465        }
466    }
467
468    /// The canonical catalog identity for this modification operation.
469    pub fn catalog_id(self) -> &'static str {
470        match self {
471            ModifyOp::Add => "add",
472            ModifyOp::Subtract => "subtract",
473            ModifyOp::Multiply => "multiply",
474            ModifyOp::Divide => "divide",
475            ModifyOp::Modulo => "modulo",
476            ModifyOp::Min => "min",
477            ModifyOp::Max => "max",
478            ModifyOp::RaiseToPower => "raiseToPower",
479            ModifyOp::AppendToArray => "appendToArray",
480            ModifyOp::RemoveFromArray => "removeFromArray",
481            ModifyOp::RemoveFromArrayByIndex => "removeFromArrayByIndex",
482        }
483    }
484}