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        #[serde(skip_serializing_if = "Option::is_none")]
458        span: Option<Span>,
459    },
460    For {
461        variable: Box<Expr>,
462        iterable: Box<Expr>,
463        #[serde(default)]
464        body: Vec<Stmt>,
465        #[serde(skip_serializing_if = "Option::is_none")]
466        span: Option<Span>,
467    },
468    While {
469        condition: Box<Expr>,
470        #[serde(default)]
471        body: Vec<Stmt>,
472        #[serde(skip_serializing_if = "Option::is_none")]
473        span: Option<Span>,
474    },
475    DoWhile {
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    Switch {
483        value: Box<Expr>,
484        #[serde(default)]
485        arms: Vec<SwitchArm>,
486        #[serde(skip_serializing_if = "Option::is_none")]
487        span: Option<Span>,
488    },
489    Delete {
490        target: Box<Expr>,
491        #[serde(skip_serializing_if = "Option::is_none")]
492        span: Option<Span>,
493    },
494    Break {
495        #[serde(skip_serializing_if = "Option::is_none")]
496        span: Option<Span>,
497    },
498    Return {
499        #[serde(skip_serializing_if = "Option::is_none")]
500        span: Option<Span>,
501    },
502    Continue {
503        #[serde(skip_serializing_if = "Option::is_none")]
504        span: Option<Span>,
505    },
506    Goto {
507        #[serde(default, skip_serializing_if = "Option::is_none")]
508        label: Option<String>,
509        #[serde(default, skip_serializing_if = "Option::is_none")]
510        offset: Option<Box<Expr>>,
511        #[serde(default)]
512        rule_start: bool,
513        #[serde(skip_serializing_if = "Option::is_none")]
514        span: Option<Span>,
515    },
516    Label {
517        name: String,
518        #[serde(skip_serializing_if = "Option::is_none")]
519        span: Option<Span>,
520    },
521    CallSubroutine {
522        name: String,
523        #[serde(skip_serializing_if = "Option::is_none")]
524        span: Option<Span>,
525    },
526    Pass {
527        #[serde(skip_serializing_if = "Option::is_none")]
528        span: Option<Span>,
529    },
530}
531
532/// One condition/body pair of an `if` statement.
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
534pub struct IfBranch {
535    pub condition: Box<Expr>,
536    #[serde(default)]
537    pub body: Vec<Stmt>,
538}
539
540impl Stmt {
541    /// The source span of this statement, if any.
542    pub fn span(&self) -> Option<&Span> {
543        match self {
544            Stmt::Expr { span, .. }
545            | Stmt::Assign { span, .. }
546            | Stmt::If { span, .. }
547            | Stmt::For { span, .. }
548            | Stmt::While { span, .. }
549            | Stmt::DoWhile { span, .. }
550            | Stmt::Switch { span, .. }
551            | Stmt::Delete { span, .. }
552            | Stmt::Break { span }
553            | Stmt::Return { span }
554            | Stmt::Continue { span }
555            | Stmt::Goto { span, .. }
556            | Stmt::Label { span, .. }
557            | Stmt::CallSubroutine { span, .. }
558            | Stmt::Pass { span } => span.as_ref(),
559        }
560    }
561
562    pub(crate) fn span_mut(&mut self) -> &mut Option<Span> {
563        match self {
564            Stmt::Expr { span, .. }
565            | Stmt::Assign { span, .. }
566            | Stmt::If { span, .. }
567            | Stmt::For { span, .. }
568            | Stmt::While { span, .. }
569            | Stmt::DoWhile { span, .. }
570            | Stmt::Switch { span, .. }
571            | Stmt::Delete { span, .. }
572            | Stmt::Break { span }
573            | Stmt::Return { span }
574            | Stmt::Continue { span }
575            | Stmt::Goto { span, .. }
576            | Stmt::Label { span, .. }
577            | Stmt::CallSubroutine { span, .. }
578            | Stmt::Pass { span } => span,
579        }
580    }
581}
582
583/// One source-ordered arm in the OPY HIR. Arms execute in source order and
584/// fall through to subsequent arms until a `break` statement is encountered.
585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
586#[serde(tag = "kind", rename_all = "camelCase")]
587pub enum SwitchArm {
588    Case {
589        value: Box<Expr>,
590        #[serde(default)]
591        body: Vec<Stmt>,
592        #[serde(skip_serializing_if = "Option::is_none")]
593        span: Option<Span>,
594    },
595    Default {
596        #[serde(default)]
597        body: Vec<Stmt>,
598        #[serde(skip_serializing_if = "Option::is_none")]
599        span: Option<Span>,
600    },
601}
602
603/// An expression.
604#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
605#[serde(tag = "kind", rename_all = "camelCase")]
606pub enum Expr {
607    Number {
608        value: f64,
609        text: String,
610        #[serde(skip_serializing_if = "Option::is_none")]
611        span: Option<Span>,
612    },
613    String {
614        value: String,
615        #[serde(skip_serializing_if = "Option::is_none")]
616        span: Option<Span>,
617    },
618    Bool {
619        value: bool,
620        #[serde(skip_serializing_if = "Option::is_none")]
621        span: Option<Span>,
622    },
623    Null {
624        #[serde(skip_serializing_if = "Option::is_none")]
625        span: Option<Span>,
626    },
627    Array {
628        #[serde(default)]
629        elements: Vec<Expr>,
630        #[serde(skip_serializing_if = "Option::is_none")]
631        span: Option<Span>,
632    },
633    Dict {
634        #[serde(default)]
635        entries: Vec<DictEntry>,
636        #[serde(skip_serializing_if = "Option::is_none")]
637        span: Option<Span>,
638    },
639    Comprehension {
640        element: Box<Expr>,
641        variable: String,
642        #[serde(skip_serializing_if = "Option::is_none")]
643        variable_span: Option<Span>,
644        #[serde(default, skip_serializing_if = "Option::is_none")]
645        index: Option<String>,
646        #[serde(default, skip_serializing_if = "Option::is_none")]
647        index_span: Option<Span>,
648        iterable: Box<Expr>,
649        #[serde(default, skip_serializing_if = "Option::is_none")]
650        condition: Option<Box<Expr>>,
651        #[serde(skip_serializing_if = "Option::is_none")]
652        span: Option<Span>,
653    },
654    Lambda {
655        #[serde(default)]
656        params: Vec<String>,
657        #[serde(default)]
658        param_spans: Vec<Option<Span>>,
659        body: Box<Expr>,
660        #[serde(skip_serializing_if = "Option::is_none")]
661        span: Option<Span>,
662    },
663    StringModifier {
664        modifier: String,
665        value: String,
666        #[serde(skip_serializing_if = "Option::is_none")]
667        span: Option<Span>,
668    },
669    Local {
670        name: String,
671        #[serde(skip_serializing_if = "Option::is_none")]
672        span: Option<Span>,
673    },
674    Vector {
675        x: Box<Expr>,
676        y: Box<Expr>,
677        z: Box<Expr>,
678        #[serde(skip_serializing_if = "Option::is_none")]
679        span: Option<Span>,
680    },
681    Enum {
682        #[serde(rename = "type")]
683        value_type: String,
684        value: String,
685        /// The member was written through the upstream `*Literal` receiver
686        /// (`HeroLiteral.ANA`, `GamemodeLiteral.ASSAULT`): the reference
687        /// emits the bare display-name lookup, without the canonical
688        /// wrapper (issue #466).
689        #[serde(default)]
690        literal: bool,
691        #[serde(skip_serializing_if = "Option::is_none")]
692        span: Option<Span>,
693    },
694    GlobalVar {
695        name: String,
696        #[serde(skip_serializing_if = "Option::is_none")]
697        span: Option<Span>,
698    },
699    PlayerVar {
700        player: Box<Expr>,
701        name: String,
702        /// The exact span of the member identifier in a source reference
703        /// such as `hostPlayer.I`.
704        #[serde(default, skip_serializing_if = "Option::is_none")]
705        member_span: Option<Span>,
706        #[serde(skip_serializing_if = "Option::is_none")]
707        span: Option<Span>,
708    },
709    HostPlayer {
710        #[serde(skip_serializing_if = "Option::is_none")]
711        span: Option<Span>,
712    },
713    /// An OPY member expression whose canonical Workshop meaning is deferred
714    /// to the integration catalog. The receiver and source member identity
715    /// remain available to tooling and lowering.
716    Member {
717        receiver: Box<Expr>,
718        member: String,
719        #[serde(default, skip_serializing_if = "Option::is_none")]
720        member_span: Option<Span>,
721        #[serde(skip_serializing_if = "Option::is_none")]
722        span: Option<Span>,
723    },
724    EventPlayer {
725        #[serde(skip_serializing_if = "Option::is_none")]
726        span: Option<Span>,
727    },
728    Constant {
729        name: String,
730        #[serde(skip_serializing_if = "Option::is_none")]
731        span: Option<Span>,
732    },
733    Call {
734        name: String,
735        #[serde(default)]
736        args: Vec<Expr>,
737        #[serde(default, skip_serializing_if = "Option::is_none")]
738        debug_source: Option<String>,
739        #[serde(skip_serializing_if = "Option::is_none")]
740        span: Option<Span>,
741    },
742    ReceiverCall {
743        receiver: Box<Expr>,
744        name: String,
745        #[serde(default)]
746        args: Vec<Expr>,
747        #[serde(skip_serializing_if = "Option::is_none")]
748        span: Option<Span>,
749    },
750    MacroCall {
751        name: String,
752        #[serde(default)]
753        args: Vec<Expr>,
754        #[serde(skip_serializing_if = "Option::is_none")]
755        span: Option<Span>,
756    },
757    MacroParam {
758        name: String,
759        #[serde(skip_serializing_if = "Option::is_none")]
760        span: Option<Span>,
761    },
762    /// An OPY source type literal, currently used by
763    /// `createWorkshopSetting` numeric ranges.
764    Type {
765        name: String,
766        #[serde(default)]
767        args: Vec<Expr>,
768        #[serde(skip_serializing_if = "Option::is_none")]
769        span: Option<Span>,
770    },
771    Binary {
772        op: String,
773        left: Box<Expr>,
774        right: Box<Expr>,
775        #[serde(skip_serializing_if = "Option::is_none")]
776        span: Option<Span>,
777    },
778    Conditional {
779        then_value: Box<Expr>,
780        condition: Box<Expr>,
781        else_value: Box<Expr>,
782        #[serde(skip_serializing_if = "Option::is_none")]
783        span: Option<Span>,
784    },
785    Unary {
786        op: String,
787        operand: Box<Expr>,
788        #[serde(skip_serializing_if = "Option::is_none")]
789        span: Option<Span>,
790    },
791    Index {
792        array: Box<Expr>,
793        index: Box<Expr>,
794        #[serde(skip_serializing_if = "Option::is_none")]
795        span: Option<Span>,
796    },
797    Format {
798        text: String,
799        #[serde(default)]
800        args: Vec<Expr>,
801        #[serde(skip_serializing_if = "Option::is_none")]
802        span: Option<Span>,
803    },
804}
805
806/// One key/value pair in an OPY dictionary.
807#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
808pub struct DictEntry {
809    pub key: Box<Expr>,
810    pub value: Box<Expr>,
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub span: Option<Span>,
813}
814
815impl Expr {
816    /// The source span of this expression, if any.
817    pub fn span(&self) -> Option<&Span> {
818        match self {
819            Expr::Number { span, .. }
820            | Expr::String { span, .. }
821            | Expr::Bool { span, .. }
822            | Expr::Null { span }
823            | Expr::Array { span, .. }
824            | Expr::Dict { span, .. }
825            | Expr::Comprehension { span, .. }
826            | Expr::Lambda { span, .. }
827            | Expr::StringModifier { span, .. }
828            | Expr::Local { span, .. }
829            | Expr::Vector { span, .. }
830            | Expr::Enum { span, .. }
831            | Expr::GlobalVar { span, .. }
832            | Expr::PlayerVar { span, .. }
833            | Expr::HostPlayer { span }
834            | Expr::Member { span, .. }
835            | Expr::EventPlayer { span }
836            | Expr::Constant { span, .. }
837            | Expr::Call { span, .. }
838            | Expr::ReceiverCall { span, .. }
839            | Expr::MacroCall { span, .. }
840            | Expr::MacroParam { span, .. }
841            | Expr::Type { span, .. }
842            | Expr::Binary { span, .. }
843            | Expr::Conditional { span, .. }
844            | Expr::Unary { span, .. }
845            | Expr::Index { span, .. }
846            | Expr::Format { span, .. } => span.as_ref(),
847        }
848    }
849
850    pub(crate) fn span_mut(&mut self) -> &mut Option<Span> {
851        match self {
852            Expr::Number { span, .. }
853            | Expr::String { span, .. }
854            | Expr::Bool { span, .. }
855            | Expr::Null { span }
856            | Expr::Array { span, .. }
857            | Expr::Dict { span, .. }
858            | Expr::Comprehension { span, .. }
859            | Expr::Lambda { span, .. }
860            | Expr::StringModifier { span, .. }
861            | Expr::Local { span, .. }
862            | Expr::Vector { span, .. }
863            | Expr::Enum { span, .. }
864            | Expr::GlobalVar { span, .. }
865            | Expr::PlayerVar { span, .. }
866            | Expr::HostPlayer { span }
867            | Expr::Member { span, .. }
868            | Expr::EventPlayer { span }
869            | Expr::Constant { span, .. }
870            | Expr::Call { span, .. }
871            | Expr::ReceiverCall { span, .. }
872            | Expr::MacroCall { span, .. }
873            | Expr::MacroParam { span, .. }
874            | Expr::Type { span, .. }
875            | Expr::Binary { span, .. }
876            | Expr::Conditional { span, .. }
877            | Expr::Unary { span, .. }
878            | Expr::Index { span, .. }
879            | Expr::Format { span, .. } => span,
880        }
881    }
882
883    /// The protocol `kind` of this expression.
884    pub fn kind_name(&self) -> &'static str {
885        match self {
886            Expr::Number { .. } => "number",
887            Expr::String { .. } => "string",
888            Expr::Bool { .. } => "bool",
889            Expr::Null { .. } => "null",
890            Expr::Array { .. } => "array",
891            Expr::Dict { .. } => "dict",
892            Expr::Comprehension { .. } => "comprehension",
893            Expr::Lambda { .. } => "lambda",
894            Expr::StringModifier { .. } => "stringModifier",
895            Expr::Local { .. } => "local",
896            Expr::Vector { .. } => "vector",
897            Expr::Enum { .. } => "enum",
898            Expr::GlobalVar { .. } => "globalVar",
899            Expr::PlayerVar { .. } => "playerVar",
900            Expr::HostPlayer { .. } => "hostPlayer",
901            Expr::Member { .. } => "member",
902            Expr::EventPlayer { .. } => "eventPlayer",
903            Expr::Constant { .. } => "constant",
904            Expr::Call { .. } => "call",
905            Expr::ReceiverCall { .. } => "receiverCall",
906            Expr::MacroCall { .. } => "macroCall",
907            Expr::MacroParam { .. } => "macroParam",
908            Expr::Type { .. } => "type",
909            Expr::Binary { .. } => "binary",
910            Expr::Conditional { .. } => "conditional",
911            Expr::Unary { .. } => "unary",
912            Expr::Index { .. } => "index",
913            Expr::Format { .. } => "format",
914        }
915    }
916}