Skip to main content

opy_rs/hir/
types.rs

1//! Serde protocol types for the `wright/opy-hir` protocol, major version 2.
2//!
3//! These types mirror the Opy HIR v2 specification (`docs/hir/opy-hir-v2.md`,
4//! `wright/opy-hir` v2.0.0 wire payloads). Unknown
5//! fields on known nodes are tolerated so an additive producer change inside
6//! the same major version does not break the consumer; unknown node *kinds*
7//! are rejected during validation (see [`super::validate`]).
8
9use std::collections::BTreeMap;
10
11use serde::{Deserialize, Serialize};
12
13/// The `wright/opy-hir` protocol name.
14pub const PROTOCOL_NAME: &str = "wright/opy-hir";
15/// The protocol major version this consumer understands.
16pub const PROTOCOL_MAJOR: u32 = 2;
17/// The protocol version emitted by this producer.
18pub const PROTOCOL_VERSION: &str = "2.0.0";
19
20/// The number of Workshop variable slots per variable set.
21///
22/// OverPy's `defaultVarNames` table covers exactly these slots: the 128
23/// uppercase letter spellings `A`–`Z`, `AA`–`AZ`, …, `DA`–`DX` (bijective
24/// base-26, Excel-style, zero-based). The pinned OverPy 9.7.10 reference
25/// accepts these names as *implicit* global variables anywhere a variable may
26/// appear — including as a `for ... in range(...)` loop binder — and as player
27/// variables, without declarations, assigning each namespace its fixed slot.
28/// Names outside the table (lowercase, mixed case, longer spellings) stay
29/// ordinary unresolved identifiers (see `docs/language-support.md`).
30const DEFAULT_VAR_SLOTS: u32 = 128;
31
32/// The fixed Workshop slot for an OverPy default variable name (`A`–`Z`,
33/// `AA`–`AZ`, …, `DA`–`DX`), or `None` for any other spelling.
34///
35/// The index is the zero-based bijective base-26 value of the uppercase
36/// spelling (`A` = 0, `Z` = 25, `AA` = 26, `DX` = 127); spellings beyond the
37/// 128-slot table (`DY`, `EA`, …, three-letter names, lowercase) return
38/// `None`, matching the pinned reference's `defaultVarNames` table exactly.
39pub fn default_var_index(name: &str) -> Option<u32> {
40    if name.is_empty() || name.len() > 2 {
41        return None;
42    }
43    let mut value: u32 = 0;
44    for byte in name.bytes() {
45        if !byte.is_ascii_uppercase() {
46            return None;
47        }
48        value = value * 26 + u32::from(byte - b'A' + 1);
49    }
50    let index = value - 1;
51    (index < DEFAULT_VAR_SLOTS).then_some(index)
52}
53
54/// Protocol envelope identity.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct Protocol {
57    pub name: String,
58    pub version: String,
59}
60
61/// Producer identity.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct Generator {
64    pub name: String,
65    pub version: String,
66    pub frontend: String,
67}
68
69/// A source file in the protocol's file registry.
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct SourceFile {
72    pub id: u32,
73    pub path: String,
74}
75
76/// A preprocessing define recorded for provenance.
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct Define {
79    pub name: String,
80    #[serde(default)]
81    pub is_function: bool,
82    /// Whether the definition used OverPy's `#!defineMember` surface.
83    #[serde(default)]
84    pub is_member: bool,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub span: Option<Span>,
87}
88
89/// A 1-based, half-open source interval.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Span {
92    pub file: u32,
93    pub start: Position,
94    pub end: Position,
95}
96
97/// A 1-based line/column position.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99pub struct Position {
100    pub line: u32,
101    pub col: u32,
102}
103
104/// A top-level program payload.
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct Program {
107    pub protocol: Protocol,
108    pub generator: Generator,
109    pub files: Vec<SourceFile>,
110    #[serde(default)]
111    pub defines: Vec<Define>,
112    #[serde(default)]
113    pub declarations: Vec<Declaration>,
114    #[serde(default)]
115    pub rules: Vec<RuleEntry>,
116    /// The typed custom-game-settings block, when the source had one (#86).
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub settings: Option<Settings>,
119    /// Frontend preprocessing state. Strict optimization is honored by the
120    /// native lowering path; other optimizer, translation, and replacement
121    /// choices remain lowering-dependent.
122    #[serde(default)]
123    pub preprocessing: PreprocessingState,
124}
125
126/// The source-level preprocessing state observed by the frontend.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
128pub struct PreprocessingState {
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub main_file: Option<DirectiveValue>,
131    pub allow_macro_redeclaration: bool,
132    #[serde(default)]
133    pub rule_prefix: Option<DirectiveValue>,
134    #[serde(default)]
135    pub rule_prefix_template: Option<DirectiveValue>,
136    #[serde(default)]
137    pub translations: Option<TranslationState>,
138    #[serde(default)]
139    pub optimization: OptimizationState,
140    #[serde(default)]
141    pub replacements: Vec<DirectiveValue>,
142    #[serde(default)]
143    pub suppressed_warnings: Vec<String>,
144    #[serde(default)]
145    pub directives: Vec<DirectiveRecord>,
146    /// Initial optimization state for each source file before its local
147    /// directives are processed. This is compiler-internal provenance and is
148    /// not part of the serialized HIR protocol.
149    #[serde(skip)]
150    pub(crate) source_file_initial_optimization: BTreeMap<u32, OptimizationState>,
151}
152
153/// A directive value plus its source provenance.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct DirectiveValue {
156    pub value: String,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub span: Option<Span>,
159}
160
161/// One source message and its optional localized PO values.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct TranslationEntry {
164    pub msgid: String,
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub context: Option<String>,
167    #[serde(default)]
168    pub translations: BTreeMap<String, String>,
169}
170
171/// Translation language selection and the loaded project catalog.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct TranslationState {
174    pub languages: Vec<String>,
175    #[serde(default)]
176    pub entries: Vec<TranslationEntry>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub span: Option<Span>,
179}
180
181/// Frontend-visible optimization controls consumed by lowering-dependent rules.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct OptimizationState {
184    pub enabled: bool,
185    pub for_size: bool,
186    pub for_size_aggressive: bool,
187    pub strict: bool,
188}
189
190impl Default for OptimizationState {
191    fn default() -> Self {
192        Self {
193            enabled: true,
194            for_size: false,
195            for_size_aggressive: false,
196            strict: false,
197        }
198    }
199}
200
201/// One preprocessing event, retained so block-scoped state transitions remain
202/// inspectable without executing a backend optimizer.
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct DirectiveRecord {
205    pub name: String,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub value: Option<String>,
208    pub scope_col: u32,
209    #[serde(default)]
210    pub scope_depth: u32,
211    #[serde(default)]
212    pub state: PreprocessingSnapshot,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub span: Option<Span>,
215}
216
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
218pub struct PreprocessingSnapshot {
219    pub allow_macro_redeclaration: bool,
220    pub optimization: OptimizationState,
221    #[serde(default)]
222    pub rule_prefix: Option<String>,
223    #[serde(default)]
224    pub rule_prefix_template: Option<String>,
225    #[serde(default)]
226    pub translations: Option<Vec<String>>,
227    #[serde(default)]
228    pub replacements: Vec<String>,
229}
230
231/// A custom-game-settings block (`settings { ... }`, #86).
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct Settings {
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub span: Option<Span>,
236    #[serde(default)]
237    pub children: Vec<SettingsNode>,
238}
239
240/// One member of a settings group.
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[serde(tag = "kind", rename_all = "camelCase")]
243pub enum SettingsNode {
244    Group {
245        name: String,
246        #[serde(default)]
247        children: Vec<SettingsNode>,
248        #[serde(skip_serializing_if = "Option::is_none")]
249        span: Option<Span>,
250    },
251    Number {
252        name: String,
253        value: f64,
254        #[serde(skip_serializing_if = "Option::is_none")]
255        span: Option<Span>,
256    },
257    Bool {
258        name: String,
259        value: bool,
260        #[serde(skip_serializing_if = "Option::is_none")]
261        span: Option<Span>,
262    },
263    String {
264        name: String,
265        value: String,
266        #[serde(skip_serializing_if = "Option::is_none")]
267        span: Option<Span>,
268    },
269    /// A settings expression preserved without assigning it a literal type.
270    Raw {
271        name: String,
272        value: String,
273        #[serde(skip_serializing_if = "Option::is_none")]
274        span: Option<Span>,
275    },
276    List {
277        name: String,
278        #[serde(default)]
279        elements: Vec<SettingsListElement>,
280        #[serde(skip_serializing_if = "Option::is_none")]
281        span: Option<Span>,
282    },
283}
284
285impl SettingsNode {
286    /// The source span of this node, if any.
287    pub fn span(&self) -> Option<&Span> {
288        match self {
289            SettingsNode::Group { span, .. }
290            | SettingsNode::Number { span, .. }
291            | SettingsNode::Bool { span, .. }
292            | SettingsNode::String { span, .. }
293            | SettingsNode::Raw { span, .. }
294            | SettingsNode::List { span, .. } => span.as_ref(),
295        }
296    }
297}
298
299/// One element of a settings list (corpus lists are all strings).
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301pub struct SettingsListElement {
302    pub value: String,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub span: Option<Span>,
305}
306
307/// A program-scope symbol declaration.
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309#[serde(tag = "kind", rename_all = "camelCase")]
310pub enum Declaration {
311    GlobalVariable {
312        name: String,
313        #[serde(default)]
314        index: Option<u32>,
315        #[serde(skip_serializing_if = "Option::is_none")]
316        span: Option<Span>,
317        /// The exact span of the declared identifier token.
318        #[serde(default, skip_serializing_if = "Option::is_none")]
319        name_span: Option<Span>,
320        #[serde(default)]
321        initializer: Option<Box<Expr>>,
322    },
323    PlayerVariable {
324        name: String,
325        #[serde(default)]
326        index: Option<u32>,
327        #[serde(skip_serializing_if = "Option::is_none")]
328        span: Option<Span>,
329        /// The exact span of the declared identifier token.
330        #[serde(default, skip_serializing_if = "Option::is_none")]
331        name_span: Option<Span>,
332        #[serde(default)]
333        initializer: Option<Box<Expr>>,
334    },
335    Subroutine {
336        name: String,
337        #[serde(default)]
338        index: Option<u32>,
339        #[serde(skip_serializing_if = "Option::is_none")]
340        span: Option<Span>,
341        /// The exact span of the declared identifier token.
342        #[serde(default, skip_serializing_if = "Option::is_none")]
343        name_span: Option<Span>,
344    },
345    Constant {
346        name: String,
347        #[serde(skip_serializing_if = "Option::is_none")]
348        span: Option<Span>,
349        value: Box<Expr>,
350    },
351    Macro {
352        name: String,
353        #[serde(default)]
354        args: Vec<String>,
355        #[serde(skip_serializing_if = "Option::is_none")]
356        span: Option<Span>,
357        #[serde(default)]
358        body: Vec<Stmt>,
359    },
360}
361
362/// An entry in `rules`: a rule or a subroutine definition.
363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
364#[serde(untagged)]
365pub enum RuleEntry {
366    /// A rule: an object without a `kind` tag.
367    Rule(Rule),
368    /// A subroutine definition: `{ "kind": "subroutineDef", ... }`.
369    SubroutineDef {
370        #[serde(rename = "kind")]
371        kind: String,
372        name: String,
373        #[serde(default, skip_serializing_if = "String::is_empty")]
374        source_name: String,
375        #[serde(skip_serializing_if = "Option::is_none")]
376        span: Option<Span>,
377        /// The exact span of the defined identifier token in `def name():`.
378        #[serde(default, skip_serializing_if = "Option::is_none")]
379        name_span: Option<Span>,
380        #[serde(default)]
381        body: Vec<Stmt>,
382        #[serde(default)]
383        annotations: Vec<Annotation>,
384    },
385}
386
387/// A rule with its event, conditions, and actions.
388#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
389pub struct Rule {
390    pub name: String,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub span: Option<Span>,
393    /// The exact span of the rule name inside its string literal.
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub name_span: Option<Span>,
396    #[serde(default)]
397    pub disabled: bool,
398    #[serde(default)]
399    pub delimiter: bool,
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub new_page: Option<String>,
402    #[serde(default)]
403    pub annotations: Vec<Annotation>,
404    pub event: Event,
405    #[serde(default)]
406    pub conditions: Vec<Expr>,
407    #[serde(default)]
408    pub actions: Vec<Stmt>,
409}
410
411/// A source annotation retained on a rule or subroutine definition.
412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct Annotation {
414    pub name: String,
415    #[serde(default)]
416    pub args: Vec<AnnotationArg>,
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub span: Option<Span>,
419}
420
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422pub struct AnnotationArg {
423    pub text: String,
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub span: Option<Span>,
426}
427
428/// A rule event.
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
430pub struct Event {
431    pub name: String,
432    #[serde(default)]
433    pub args: Vec<Expr>,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub span: Option<Span>,
436}
437
438/// A statement.
439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440#[serde(tag = "kind", rename_all = "camelCase")]
441pub enum Stmt {
442    Expr {
443        expr: Box<Expr>,
444        #[serde(skip_serializing_if = "Option::is_none")]
445        span: Option<Span>,
446    },
447    Assign {
448        target: Box<Expr>,
449        value: Box<Expr>,
450        #[serde(skip_serializing_if = "Option::is_none")]
451        span: Option<Span>,
452    },
453    If {
454        branches: Vec<IfBranch>,
455        #[serde(default)]
456        r#else: Option<Vec<Stmt>>,
457        /// The authored `else` keyword span when the chain has an else branch.
458        #[serde(default, skip_serializing_if = "Option::is_none")]
459        else_span: Option<Span>,
460        /// Zero-width span at the first token past the chain: the position a
461        /// Workshop `End` marker would occupy.
462        #[serde(default, skip_serializing_if = "Option::is_none")]
463        end_span: Option<Span>,
464        #[serde(skip_serializing_if = "Option::is_none")]
465        span: Option<Span>,
466    },
467    For {
468        variable: Box<Expr>,
469        iterable: Box<Expr>,
470        #[serde(default)]
471        body: Vec<Stmt>,
472        #[serde(skip_serializing_if = "Option::is_none")]
473        span: Option<Span>,
474    },
475    While {
476        condition: Box<Expr>,
477        #[serde(default)]
478        body: Vec<Stmt>,
479        #[serde(skip_serializing_if = "Option::is_none")]
480        span: Option<Span>,
481    },
482    DoWhile {
483        condition: Box<Expr>,
484        #[serde(default)]
485        body: Vec<Stmt>,
486        #[serde(skip_serializing_if = "Option::is_none")]
487        span: Option<Span>,
488    },
489    Switch {
490        value: Box<Expr>,
491        #[serde(default)]
492        arms: Vec<SwitchArm>,
493        #[serde(skip_serializing_if = "Option::is_none")]
494        span: Option<Span>,
495    },
496    Delete {
497        target: Box<Expr>,
498        #[serde(skip_serializing_if = "Option::is_none")]
499        span: Option<Span>,
500    },
501    Break {
502        #[serde(skip_serializing_if = "Option::is_none")]
503        span: Option<Span>,
504    },
505    Return {
506        #[serde(skip_serializing_if = "Option::is_none")]
507        span: Option<Span>,
508    },
509    Continue {
510        #[serde(skip_serializing_if = "Option::is_none")]
511        span: Option<Span>,
512    },
513    Goto {
514        #[serde(default, skip_serializing_if = "Option::is_none")]
515        label: Option<String>,
516        #[serde(default, skip_serializing_if = "Option::is_none")]
517        offset: Option<Box<Expr>>,
518        #[serde(default)]
519        rule_start: bool,
520        #[serde(skip_serializing_if = "Option::is_none")]
521        span: Option<Span>,
522    },
523    Label {
524        name: String,
525        #[serde(skip_serializing_if = "Option::is_none")]
526        span: Option<Span>,
527    },
528    CallSubroutine {
529        name: String,
530        #[serde(skip_serializing_if = "Option::is_none")]
531        span: Option<Span>,
532    },
533    Pass {
534        #[serde(skip_serializing_if = "Option::is_none")]
535        span: Option<Span>,
536    },
537}
538
539/// One condition/body pair of an `if` statement.
540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
541pub struct IfBranch {
542    pub condition: Box<Expr>,
543    #[serde(default)]
544    pub body: Vec<Stmt>,
545    /// The authored `if`/`elif`/`else if` keyword span that opens this branch.
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub marker: Option<Span>,
548}
549
550impl Stmt {
551    /// The source span of this statement, if any.
552    pub fn span(&self) -> Option<&Span> {
553        match self {
554            Stmt::Expr { span, .. }
555            | Stmt::Assign { span, .. }
556            | Stmt::If { span, .. }
557            | Stmt::For { span, .. }
558            | Stmt::While { span, .. }
559            | Stmt::DoWhile { span, .. }
560            | Stmt::Switch { span, .. }
561            | Stmt::Delete { span, .. }
562            | Stmt::Break { span }
563            | Stmt::Return { span }
564            | Stmt::Continue { span }
565            | Stmt::Goto { span, .. }
566            | Stmt::Label { span, .. }
567            | Stmt::CallSubroutine { span, .. }
568            | Stmt::Pass { span } => span.as_ref(),
569        }
570    }
571
572    pub(crate) fn span_mut(&mut self) -> &mut Option<Span> {
573        match self {
574            Stmt::Expr { span, .. }
575            | Stmt::Assign { span, .. }
576            | Stmt::If { span, .. }
577            | Stmt::For { span, .. }
578            | Stmt::While { span, .. }
579            | Stmt::DoWhile { span, .. }
580            | Stmt::Switch { span, .. }
581            | Stmt::Delete { span, .. }
582            | Stmt::Break { span }
583            | Stmt::Return { span }
584            | Stmt::Continue { span }
585            | Stmt::Goto { span, .. }
586            | Stmt::Label { span, .. }
587            | Stmt::CallSubroutine { span, .. }
588            | Stmt::Pass { span } => span,
589        }
590    }
591}
592
593/// One source-ordered arm in the OPY HIR. Arms execute in source order and
594/// fall through to subsequent arms until a `break` statement is encountered.
595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
596#[serde(tag = "kind", rename_all = "camelCase")]
597pub enum SwitchArm {
598    Case {
599        value: Box<Expr>,
600        #[serde(default)]
601        body: Vec<Stmt>,
602        #[serde(skip_serializing_if = "Option::is_none")]
603        span: Option<Span>,
604    },
605    Default {
606        #[serde(default)]
607        body: Vec<Stmt>,
608        #[serde(skip_serializing_if = "Option::is_none")]
609        span: Option<Span>,
610    },
611}
612
613/// An expression.
614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
615#[serde(tag = "kind", rename_all = "camelCase")]
616pub enum Expr {
617    Number {
618        value: f64,
619        text: String,
620        #[serde(skip_serializing_if = "Option::is_none")]
621        span: Option<Span>,
622    },
623    String {
624        value: String,
625        #[serde(skip_serializing_if = "Option::is_none")]
626        span: Option<Span>,
627    },
628    Bool {
629        value: bool,
630        #[serde(skip_serializing_if = "Option::is_none")]
631        span: Option<Span>,
632    },
633    Null {
634        #[serde(skip_serializing_if = "Option::is_none")]
635        span: Option<Span>,
636    },
637    Array {
638        #[serde(default)]
639        elements: Vec<Expr>,
640        #[serde(skip_serializing_if = "Option::is_none")]
641        span: Option<Span>,
642    },
643    Dict {
644        #[serde(default)]
645        entries: Vec<DictEntry>,
646        #[serde(skip_serializing_if = "Option::is_none")]
647        span: Option<Span>,
648    },
649    Comprehension {
650        element: Box<Expr>,
651        variable: String,
652        #[serde(skip_serializing_if = "Option::is_none")]
653        variable_span: Option<Span>,
654        #[serde(default, skip_serializing_if = "Option::is_none")]
655        index: Option<String>,
656        #[serde(default, skip_serializing_if = "Option::is_none")]
657        index_span: Option<Span>,
658        iterable: Box<Expr>,
659        #[serde(default, skip_serializing_if = "Option::is_none")]
660        condition: Option<Box<Expr>>,
661        #[serde(skip_serializing_if = "Option::is_none")]
662        span: Option<Span>,
663    },
664    Lambda {
665        #[serde(default)]
666        params: Vec<String>,
667        #[serde(default)]
668        param_spans: Vec<Option<Span>>,
669        body: Box<Expr>,
670        #[serde(skip_serializing_if = "Option::is_none")]
671        span: Option<Span>,
672    },
673    StringModifier {
674        modifier: String,
675        value: String,
676        #[serde(skip_serializing_if = "Option::is_none")]
677        span: Option<Span>,
678    },
679    Local {
680        name: String,
681        #[serde(skip_serializing_if = "Option::is_none")]
682        span: Option<Span>,
683    },
684    Vector {
685        x: Box<Expr>,
686        y: Box<Expr>,
687        z: Box<Expr>,
688        #[serde(skip_serializing_if = "Option::is_none")]
689        span: Option<Span>,
690    },
691    Enum {
692        #[serde(rename = "type")]
693        value_type: String,
694        value: String,
695        /// The member was written through the upstream `*Literal` receiver
696        /// (`HeroLiteral.ANA`, `GamemodeLiteral.ASSAULT`): the reference
697        /// emits the bare display-name lookup, without the canonical
698        /// wrapper (issue #466).
699        #[serde(default)]
700        literal: bool,
701        #[serde(skip_serializing_if = "Option::is_none")]
702        span: Option<Span>,
703    },
704    GlobalVar {
705        name: String,
706        #[serde(skip_serializing_if = "Option::is_none")]
707        span: Option<Span>,
708    },
709    PlayerVar {
710        player: Box<Expr>,
711        name: String,
712        /// The exact span of the member identifier in a source reference
713        /// such as `hostPlayer.I`.
714        #[serde(default, skip_serializing_if = "Option::is_none")]
715        member_span: Option<Span>,
716        #[serde(skip_serializing_if = "Option::is_none")]
717        span: Option<Span>,
718    },
719    HostPlayer {
720        #[serde(skip_serializing_if = "Option::is_none")]
721        span: Option<Span>,
722    },
723    /// An OPY member expression whose canonical Workshop meaning is deferred
724    /// to the integration catalog. The receiver and source member identity
725    /// remain available to tooling and lowering.
726    Member {
727        receiver: Box<Expr>,
728        member: String,
729        #[serde(default, skip_serializing_if = "Option::is_none")]
730        member_span: Option<Span>,
731        #[serde(skip_serializing_if = "Option::is_none")]
732        span: Option<Span>,
733    },
734    EventPlayer {
735        #[serde(skip_serializing_if = "Option::is_none")]
736        span: Option<Span>,
737    },
738    Constant {
739        name: String,
740        #[serde(skip_serializing_if = "Option::is_none")]
741        span: Option<Span>,
742    },
743    Call {
744        name: String,
745        #[serde(default)]
746        args: Vec<Expr>,
747        #[serde(default, skip_serializing_if = "Option::is_none")]
748        debug_source: Option<String>,
749        #[serde(skip_serializing_if = "Option::is_none")]
750        span: Option<Span>,
751    },
752    ReceiverCall {
753        receiver: Box<Expr>,
754        name: String,
755        #[serde(default)]
756        args: Vec<Expr>,
757        #[serde(skip_serializing_if = "Option::is_none")]
758        span: Option<Span>,
759    },
760    MacroCall {
761        name: String,
762        #[serde(default)]
763        args: Vec<Expr>,
764        #[serde(skip_serializing_if = "Option::is_none")]
765        span: Option<Span>,
766    },
767    MacroParam {
768        name: String,
769        #[serde(skip_serializing_if = "Option::is_none")]
770        span: Option<Span>,
771    },
772    /// An OPY source type literal, currently used by
773    /// `createWorkshopSetting` numeric ranges.
774    Type {
775        name: String,
776        #[serde(default)]
777        args: Vec<Expr>,
778        #[serde(skip_serializing_if = "Option::is_none")]
779        span: Option<Span>,
780    },
781    Binary {
782        op: String,
783        left: Box<Expr>,
784        right: Box<Expr>,
785        #[serde(skip_serializing_if = "Option::is_none")]
786        span: Option<Span>,
787    },
788    Conditional {
789        then_value: Box<Expr>,
790        condition: Box<Expr>,
791        else_value: Box<Expr>,
792        #[serde(skip_serializing_if = "Option::is_none")]
793        span: Option<Span>,
794    },
795    Unary {
796        op: String,
797        operand: Box<Expr>,
798        #[serde(skip_serializing_if = "Option::is_none")]
799        span: Option<Span>,
800    },
801    Index {
802        array: Box<Expr>,
803        index: Box<Expr>,
804        #[serde(skip_serializing_if = "Option::is_none")]
805        span: Option<Span>,
806    },
807    Format {
808        text: String,
809        #[serde(default)]
810        args: Vec<Expr>,
811        #[serde(skip_serializing_if = "Option::is_none")]
812        span: Option<Span>,
813    },
814}
815
816/// One key/value pair in an OPY dictionary.
817#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
818pub struct DictEntry {
819    pub key: Box<Expr>,
820    pub value: Box<Expr>,
821    #[serde(skip_serializing_if = "Option::is_none")]
822    pub span: Option<Span>,
823}
824
825impl Expr {
826    /// The source span of this expression, if any.
827    pub fn span(&self) -> Option<&Span> {
828        match self {
829            Expr::Number { span, .. }
830            | Expr::String { span, .. }
831            | Expr::Bool { span, .. }
832            | Expr::Null { span }
833            | Expr::Array { span, .. }
834            | Expr::Dict { span, .. }
835            | Expr::Comprehension { span, .. }
836            | Expr::Lambda { span, .. }
837            | Expr::StringModifier { span, .. }
838            | Expr::Local { span, .. }
839            | Expr::Vector { span, .. }
840            | Expr::Enum { span, .. }
841            | Expr::GlobalVar { span, .. }
842            | Expr::PlayerVar { span, .. }
843            | Expr::HostPlayer { span }
844            | Expr::Member { span, .. }
845            | Expr::EventPlayer { span }
846            | Expr::Constant { span, .. }
847            | Expr::Call { span, .. }
848            | Expr::ReceiverCall { span, .. }
849            | Expr::MacroCall { span, .. }
850            | Expr::MacroParam { span, .. }
851            | Expr::Type { span, .. }
852            | Expr::Binary { span, .. }
853            | Expr::Conditional { span, .. }
854            | Expr::Unary { span, .. }
855            | Expr::Index { span, .. }
856            | Expr::Format { span, .. } => span.as_ref(),
857        }
858    }
859
860    pub(crate) fn span_mut(&mut self) -> &mut Option<Span> {
861        match self {
862            Expr::Number { span, .. }
863            | Expr::String { span, .. }
864            | Expr::Bool { span, .. }
865            | Expr::Null { span }
866            | Expr::Array { span, .. }
867            | Expr::Dict { span, .. }
868            | Expr::Comprehension { span, .. }
869            | Expr::Lambda { span, .. }
870            | Expr::StringModifier { span, .. }
871            | Expr::Local { span, .. }
872            | Expr::Vector { span, .. }
873            | Expr::Enum { span, .. }
874            | Expr::GlobalVar { span, .. }
875            | Expr::PlayerVar { span, .. }
876            | Expr::HostPlayer { span }
877            | Expr::Member { span, .. }
878            | Expr::EventPlayer { span }
879            | Expr::Constant { span, .. }
880            | Expr::Call { span, .. }
881            | Expr::ReceiverCall { span, .. }
882            | Expr::MacroCall { span, .. }
883            | Expr::MacroParam { span, .. }
884            | Expr::Type { span, .. }
885            | Expr::Binary { span, .. }
886            | Expr::Conditional { span, .. }
887            | Expr::Unary { span, .. }
888            | Expr::Index { span, .. }
889            | Expr::Format { span, .. } => span,
890        }
891    }
892
893    /// The protocol `kind` of this expression.
894    pub fn kind_name(&self) -> &'static str {
895        match self {
896            Expr::Number { .. } => "number",
897            Expr::String { .. } => "string",
898            Expr::Bool { .. } => "bool",
899            Expr::Null { .. } => "null",
900            Expr::Array { .. } => "array",
901            Expr::Dict { .. } => "dict",
902            Expr::Comprehension { .. } => "comprehension",
903            Expr::Lambda { .. } => "lambda",
904            Expr::StringModifier { .. } => "stringModifier",
905            Expr::Local { .. } => "local",
906            Expr::Vector { .. } => "vector",
907            Expr::Enum { .. } => "enum",
908            Expr::GlobalVar { .. } => "globalVar",
909            Expr::PlayerVar { .. } => "playerVar",
910            Expr::HostPlayer { .. } => "hostPlayer",
911            Expr::Member { .. } => "member",
912            Expr::EventPlayer { .. } => "eventPlayer",
913            Expr::Constant { .. } => "constant",
914            Expr::Call { .. } => "call",
915            Expr::ReceiverCall { .. } => "receiverCall",
916            Expr::MacroCall { .. } => "macroCall",
917            Expr::MacroParam { .. } => "macroParam",
918            Expr::Type { .. } => "type",
919            Expr::Binary { .. } => "binary",
920            Expr::Conditional { .. } => "conditional",
921            Expr::Unary { .. } => "unary",
922            Expr::Index { .. } => "index",
923            Expr::Format { .. } => "format",
924        }
925    }
926}