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