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    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 dynamic Workshop object member, optionally through an
353    /// indexed `memberAccess` value. This is source semantics, not a builtin
354    /// catalog action; the emitter preserves the 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    /// The `debug(value)` HUD debug effect.
401    Debug { value: ValueId, span: Option<Span> },
402    /// The `print(message)` HUD message effect.
403    Print {
404        message: ValueId,
405        span: Option<Span>,
406    },
407    /// Any other action call with side effects.
408    Call {
409        name: String,
410        args: Vec<ValueId>,
411        span: Option<Span>,
412    },
413}
414
415impl Action {
416    /// The source span of this action, if any.
417    pub fn span(&self) -> Option<Span> {
418        match self {
419            Action::SetGlobalVariable { span, .. }
420            | Action::ModifyGlobalVariable { span, .. }
421            | Action::SetPlayerVariable { span, .. }
422            | Action::ModifyPlayerVariable { span, .. }
423            | Action::AssignMember { span, .. }
424            | Action::CallSubroutine { span, .. }
425            | Action::If { span, .. }
426            | Action::While { span, .. }
427            | Action::ForGlobalVariable { span, .. }
428            | Action::ForPlayerVariable { span, .. }
429            | Action::Debug { span, .. }
430            | Action::Print { span, .. }
431            | Action::Call { span, .. } => *span,
432        }
433    }
434}
435
436/// One condition/body pair of an `If` action.
437#[derive(Debug, Clone)]
438pub struct IfBranch {
439    pub condition: ValueId,
440    pub body: Vec<ActionId>,
441}
442
443/// The modify operators of the v0.1 surface.
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub enum ModifyOp {
446    Add,
447    Subtract,
448    Multiply,
449    Divide,
450    Modulo,
451    RaiseToPower,
452    AppendToArray,
453    RemoveFromArray,
454    RemoveFromArrayByIndex,
455}
456
457impl ModifyOp {
458    /// A short canonical name for dumps and diagnostics.
459    pub fn as_str(self) -> &'static str {
460        match self {
461            ModifyOp::Add => "Add",
462            ModifyOp::Subtract => "Subtract",
463            ModifyOp::Multiply => "Multiply",
464            ModifyOp::Divide => "Divide",
465            ModifyOp::Modulo => "Modulo",
466            ModifyOp::RaiseToPower => "RaiseToPower",
467            ModifyOp::AppendToArray => "AppendToArray",
468            ModifyOp::RemoveFromArray => "RemoveFromArray",
469            ModifyOp::RemoveFromArrayByIndex => "RemoveFromArrayByIndex",
470        }
471    }
472}