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    /// Literal text, or the authored expression when `evaluated` is `None`.
303    /// Catalog-enum consumers read this as a member name.
304    pub value: String,
305    /// The evaluated display text, `None` while unevaluated and after a
306    /// failed expression evaluation (#512).
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub evaluated: Option<String>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub span: Option<Span>,
311}
312
313/// A program-scope symbol declaration.
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315#[serde(tag = "kind", rename_all = "camelCase")]
316pub enum Declaration {
317    GlobalVariable {
318        name: String,
319        #[serde(default)]
320        index: Option<u32>,
321        #[serde(skip_serializing_if = "Option::is_none")]
322        span: Option<Span>,
323        /// The exact span of the declared identifier token.
324        #[serde(default, skip_serializing_if = "Option::is_none")]
325        name_span: Option<Span>,
326        #[serde(default)]
327        initializer: Option<Box<Expr>>,
328    },
329    PlayerVariable {
330        name: String,
331        #[serde(default)]
332        index: Option<u32>,
333        #[serde(skip_serializing_if = "Option::is_none")]
334        span: Option<Span>,
335        /// The exact span of the declared identifier token.
336        #[serde(default, skip_serializing_if = "Option::is_none")]
337        name_span: Option<Span>,
338        #[serde(default)]
339        initializer: Option<Box<Expr>>,
340    },
341    Subroutine {
342        name: String,
343        #[serde(default)]
344        index: Option<u32>,
345        #[serde(skip_serializing_if = "Option::is_none")]
346        span: Option<Span>,
347        /// The exact span of the declared identifier token.
348        #[serde(default, skip_serializing_if = "Option::is_none")]
349        name_span: Option<Span>,
350    },
351    Constant {
352        name: String,
353        #[serde(skip_serializing_if = "Option::is_none")]
354        span: Option<Span>,
355        value: Box<Expr>,
356    },
357    Macro {
358        name: String,
359        #[serde(default)]
360        args: Vec<String>,
361        #[serde(skip_serializing_if = "Option::is_none")]
362        span: Option<Span>,
363        #[serde(default)]
364        body: Vec<Stmt>,
365    },
366}
367
368/// An entry in `rules`: a rule or a subroutine definition.
369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
370#[serde(untagged)]
371pub enum RuleEntry {
372    /// A rule: an object without a `kind` tag.
373    Rule(Rule),
374    /// A subroutine definition: `{ "kind": "subroutineDef", ... }`.
375    SubroutineDef {
376        #[serde(rename = "kind")]
377        kind: String,
378        name: String,
379        #[serde(default, skip_serializing_if = "String::is_empty")]
380        source_name: String,
381        #[serde(skip_serializing_if = "Option::is_none")]
382        span: Option<Span>,
383        /// The exact span of the defined identifier token in `def name():`.
384        #[serde(default, skip_serializing_if = "Option::is_none")]
385        name_span: Option<Span>,
386        #[serde(default)]
387        body: Vec<Stmt>,
388        #[serde(default)]
389        annotations: Vec<Annotation>,
390    },
391}
392
393/// A rule with its event, conditions, and actions.
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395pub struct Rule {
396    pub name: String,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub span: Option<Span>,
399    /// The exact span of the rule name inside its string literal.
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub name_span: Option<Span>,
402    #[serde(default)]
403    pub disabled: bool,
404    #[serde(default)]
405    pub delimiter: bool,
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub new_page: Option<String>,
408    #[serde(default)]
409    pub annotations: Vec<Annotation>,
410    pub event: Event,
411    #[serde(default)]
412    pub conditions: Vec<Expr>,
413    #[serde(default)]
414    pub actions: Vec<Stmt>,
415}
416
417/// A source annotation retained on a rule or subroutine definition.
418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
419pub struct Annotation {
420    pub name: String,
421    #[serde(default)]
422    pub args: Vec<AnnotationArg>,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub span: Option<Span>,
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
428pub struct AnnotationArg {
429    pub text: String,
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub span: Option<Span>,
432}
433
434/// A rule event.
435#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
436pub struct Event {
437    pub name: String,
438    #[serde(default)]
439    pub args: Vec<Expr>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub span: Option<Span>,
442}
443
444/// A statement.
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
446#[serde(tag = "kind", rename_all = "camelCase")]
447pub enum Stmt {
448    Expr {
449        expr: Box<Expr>,
450        #[serde(skip_serializing_if = "Option::is_none")]
451        span: Option<Span>,
452    },
453    Assign {
454        target: Box<Expr>,
455        value: Box<Expr>,
456        #[serde(skip_serializing_if = "Option::is_none")]
457        span: Option<Span>,
458    },
459    If {
460        branches: Vec<IfBranch>,
461        #[serde(default)]
462        r#else: Option<Vec<Stmt>>,
463        /// The authored `else` keyword span when the chain has an else branch.
464        #[serde(default, skip_serializing_if = "Option::is_none")]
465        else_span: Option<Span>,
466        /// Zero-width span at the first token past the chain: the position a
467        /// Workshop `End` marker would occupy.
468        #[serde(default, skip_serializing_if = "Option::is_none")]
469        end_span: Option<Span>,
470        #[serde(skip_serializing_if = "Option::is_none")]
471        span: Option<Span>,
472    },
473    For {
474        variable: Box<Expr>,
475        iterable: Box<Expr>,
476        #[serde(default)]
477        body: Vec<Stmt>,
478        #[serde(skip_serializing_if = "Option::is_none")]
479        span: Option<Span>,
480    },
481    While {
482        condition: Box<Expr>,
483        #[serde(default)]
484        body: Vec<Stmt>,
485        #[serde(skip_serializing_if = "Option::is_none")]
486        span: Option<Span>,
487    },
488    DoWhile {
489        condition: Box<Expr>,
490        #[serde(default)]
491        body: Vec<Stmt>,
492        #[serde(skip_serializing_if = "Option::is_none")]
493        span: Option<Span>,
494    },
495    Switch {
496        value: Box<Expr>,
497        #[serde(default)]
498        arms: Vec<SwitchArm>,
499        #[serde(skip_serializing_if = "Option::is_none")]
500        span: Option<Span>,
501    },
502    Delete {
503        target: Box<Expr>,
504        #[serde(skip_serializing_if = "Option::is_none")]
505        span: Option<Span>,
506    },
507    Break {
508        #[serde(skip_serializing_if = "Option::is_none")]
509        span: Option<Span>,
510    },
511    Return {
512        #[serde(skip_serializing_if = "Option::is_none")]
513        span: Option<Span>,
514    },
515    Continue {
516        #[serde(skip_serializing_if = "Option::is_none")]
517        span: Option<Span>,
518    },
519    Goto {
520        #[serde(default, skip_serializing_if = "Option::is_none")]
521        label: Option<String>,
522        #[serde(default, skip_serializing_if = "Option::is_none")]
523        offset: Option<Box<Expr>>,
524        #[serde(default)]
525        rule_start: bool,
526        #[serde(skip_serializing_if = "Option::is_none")]
527        span: Option<Span>,
528    },
529    Label {
530        name: String,
531        #[serde(skip_serializing_if = "Option::is_none")]
532        span: Option<Span>,
533    },
534    CallSubroutine {
535        name: String,
536        #[serde(skip_serializing_if = "Option::is_none")]
537        span: Option<Span>,
538    },
539    Pass {
540        #[serde(skip_serializing_if = "Option::is_none")]
541        span: Option<Span>,
542    },
543}
544
545/// One condition/body pair of an `if` statement.
546#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
547pub struct IfBranch {
548    pub condition: Box<Expr>,
549    #[serde(default)]
550    pub body: Vec<Stmt>,
551    /// The authored `if`/`elif`/`else if` keyword span that opens this branch.
552    #[serde(default, skip_serializing_if = "Option::is_none")]
553    pub marker: Option<Span>,
554}
555
556impl Stmt {
557    /// The source span of this statement, if any.
558    pub fn span(&self) -> Option<&Span> {
559        match self {
560            Stmt::Expr { span, .. }
561            | Stmt::Assign { span, .. }
562            | Stmt::If { span, .. }
563            | Stmt::For { span, .. }
564            | Stmt::While { span, .. }
565            | Stmt::DoWhile { span, .. }
566            | Stmt::Switch { span, .. }
567            | Stmt::Delete { span, .. }
568            | Stmt::Break { span }
569            | Stmt::Return { span }
570            | Stmt::Continue { span }
571            | Stmt::Goto { span, .. }
572            | Stmt::Label { span, .. }
573            | Stmt::CallSubroutine { span, .. }
574            | Stmt::Pass { span } => span.as_ref(),
575        }
576    }
577
578    pub(crate) fn span_mut(&mut self) -> &mut Option<Span> {
579        match self {
580            Stmt::Expr { span, .. }
581            | Stmt::Assign { span, .. }
582            | Stmt::If { span, .. }
583            | Stmt::For { span, .. }
584            | Stmt::While { span, .. }
585            | Stmt::DoWhile { span, .. }
586            | Stmt::Switch { span, .. }
587            | Stmt::Delete { span, .. }
588            | Stmt::Break { span }
589            | Stmt::Return { span }
590            | Stmt::Continue { span }
591            | Stmt::Goto { span, .. }
592            | Stmt::Label { span, .. }
593            | Stmt::CallSubroutine { span, .. }
594            | Stmt::Pass { span } => span,
595        }
596    }
597}
598
599/// One source-ordered arm in the OPY HIR. Arms execute in source order and
600/// fall through to subsequent arms until a `break` statement is encountered.
601#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
602#[serde(tag = "kind", rename_all = "camelCase")]
603pub enum SwitchArm {
604    Case {
605        value: Box<Expr>,
606        #[serde(default)]
607        body: Vec<Stmt>,
608        #[serde(skip_serializing_if = "Option::is_none")]
609        span: Option<Span>,
610    },
611    Default {
612        #[serde(default)]
613        body: Vec<Stmt>,
614        #[serde(skip_serializing_if = "Option::is_none")]
615        span: Option<Span>,
616    },
617}
618
619/// An expression.
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621#[serde(tag = "kind", rename_all = "camelCase")]
622pub enum Expr {
623    Number {
624        value: f64,
625        text: String,
626        #[serde(skip_serializing_if = "Option::is_none")]
627        span: Option<Span>,
628    },
629    String {
630        value: String,
631        #[serde(skip_serializing_if = "Option::is_none")]
632        span: Option<Span>,
633    },
634    Bool {
635        value: bool,
636        #[serde(skip_serializing_if = "Option::is_none")]
637        span: Option<Span>,
638    },
639    Null {
640        #[serde(skip_serializing_if = "Option::is_none")]
641        span: Option<Span>,
642    },
643    Array {
644        #[serde(default)]
645        elements: Vec<Expr>,
646        #[serde(skip_serializing_if = "Option::is_none")]
647        span: Option<Span>,
648    },
649    Dict {
650        #[serde(default)]
651        entries: Vec<DictEntry>,
652        #[serde(skip_serializing_if = "Option::is_none")]
653        span: Option<Span>,
654    },
655    Comprehension {
656        element: Box<Expr>,
657        variable: String,
658        #[serde(skip_serializing_if = "Option::is_none")]
659        variable_span: Option<Span>,
660        #[serde(default, skip_serializing_if = "Option::is_none")]
661        index: Option<String>,
662        #[serde(default, skip_serializing_if = "Option::is_none")]
663        index_span: Option<Span>,
664        iterable: Box<Expr>,
665        #[serde(default, skip_serializing_if = "Option::is_none")]
666        condition: Option<Box<Expr>>,
667        #[serde(skip_serializing_if = "Option::is_none")]
668        span: Option<Span>,
669    },
670    Lambda {
671        #[serde(default)]
672        params: Vec<String>,
673        #[serde(default)]
674        param_spans: Vec<Option<Span>>,
675        body: Box<Expr>,
676        #[serde(skip_serializing_if = "Option::is_none")]
677        span: Option<Span>,
678    },
679    StringModifier {
680        modifier: String,
681        value: String,
682        #[serde(skip_serializing_if = "Option::is_none")]
683        span: Option<Span>,
684    },
685    Local {
686        name: String,
687        #[serde(skip_serializing_if = "Option::is_none")]
688        span: Option<Span>,
689    },
690    Vector {
691        x: Box<Expr>,
692        y: Box<Expr>,
693        z: Box<Expr>,
694        #[serde(skip_serializing_if = "Option::is_none")]
695        span: Option<Span>,
696    },
697    Enum {
698        #[serde(rename = "type")]
699        value_type: String,
700        value: String,
701        /// The member was written through the upstream `*Literal` receiver
702        /// (`HeroLiteral.ANA`, `GamemodeLiteral.ASSAULT`): the reference
703        /// emits the bare display-name lookup, without the canonical
704        /// wrapper (issue #466).
705        #[serde(default)]
706        literal: bool,
707        #[serde(skip_serializing_if = "Option::is_none")]
708        span: Option<Span>,
709    },
710    GlobalVar {
711        name: String,
712        #[serde(skip_serializing_if = "Option::is_none")]
713        span: Option<Span>,
714    },
715    PlayerVar {
716        player: Box<Expr>,
717        name: String,
718        /// The exact span of the member identifier in a source reference
719        /// such as `hostPlayer.I`.
720        #[serde(default, skip_serializing_if = "Option::is_none")]
721        member_span: Option<Span>,
722        #[serde(skip_serializing_if = "Option::is_none")]
723        span: Option<Span>,
724    },
725    HostPlayer {
726        #[serde(skip_serializing_if = "Option::is_none")]
727        span: Option<Span>,
728    },
729    /// An OPY member expression whose canonical Workshop meaning is deferred
730    /// to the integration catalog. The receiver and source member identity
731    /// remain available to tooling and lowering.
732    Member {
733        receiver: Box<Expr>,
734        member: String,
735        #[serde(default, skip_serializing_if = "Option::is_none")]
736        member_span: Option<Span>,
737        #[serde(skip_serializing_if = "Option::is_none")]
738        span: Option<Span>,
739    },
740    EventPlayer {
741        #[serde(skip_serializing_if = "Option::is_none")]
742        span: Option<Span>,
743    },
744    Constant {
745        name: String,
746        #[serde(skip_serializing_if = "Option::is_none")]
747        span: Option<Span>,
748    },
749    Call {
750        name: String,
751        #[serde(default)]
752        args: Vec<Expr>,
753        #[serde(default, skip_serializing_if = "Option::is_none")]
754        debug_source: Option<String>,
755        #[serde(skip_serializing_if = "Option::is_none")]
756        span: Option<Span>,
757    },
758    ReceiverCall {
759        receiver: Box<Expr>,
760        name: String,
761        #[serde(default)]
762        args: Vec<Expr>,
763        #[serde(skip_serializing_if = "Option::is_none")]
764        span: Option<Span>,
765    },
766    MacroCall {
767        name: String,
768        #[serde(default)]
769        args: Vec<Expr>,
770        #[serde(skip_serializing_if = "Option::is_none")]
771        span: Option<Span>,
772    },
773    MacroParam {
774        name: String,
775        #[serde(skip_serializing_if = "Option::is_none")]
776        span: Option<Span>,
777    },
778    /// An OPY source type literal, currently used by
779    /// `createWorkshopSetting` numeric ranges.
780    Type {
781        name: String,
782        #[serde(default)]
783        args: Vec<Expr>,
784        #[serde(skip_serializing_if = "Option::is_none")]
785        span: Option<Span>,
786    },
787    Binary {
788        op: String,
789        left: Box<Expr>,
790        right: Box<Expr>,
791        #[serde(skip_serializing_if = "Option::is_none")]
792        span: Option<Span>,
793    },
794    Conditional {
795        then_value: Box<Expr>,
796        condition: Box<Expr>,
797        else_value: Box<Expr>,
798        #[serde(skip_serializing_if = "Option::is_none")]
799        span: Option<Span>,
800    },
801    Unary {
802        op: String,
803        operand: Box<Expr>,
804        #[serde(skip_serializing_if = "Option::is_none")]
805        span: Option<Span>,
806    },
807    Index {
808        array: Box<Expr>,
809        index: Box<Expr>,
810        #[serde(skip_serializing_if = "Option::is_none")]
811        span: Option<Span>,
812    },
813    Format {
814        text: String,
815        #[serde(default)]
816        args: Vec<Expr>,
817        #[serde(skip_serializing_if = "Option::is_none")]
818        span: Option<Span>,
819    },
820}
821
822/// One key/value pair in an OPY dictionary.
823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
824pub struct DictEntry {
825    pub key: Box<Expr>,
826    pub value: Box<Expr>,
827    #[serde(skip_serializing_if = "Option::is_none")]
828    pub span: Option<Span>,
829}
830
831impl Expr {
832    /// The source span of this expression, if any.
833    pub fn span(&self) -> Option<&Span> {
834        match self {
835            Expr::Number { span, .. }
836            | Expr::String { span, .. }
837            | Expr::Bool { span, .. }
838            | Expr::Null { span }
839            | Expr::Array { span, .. }
840            | Expr::Dict { span, .. }
841            | Expr::Comprehension { span, .. }
842            | Expr::Lambda { span, .. }
843            | Expr::StringModifier { span, .. }
844            | Expr::Local { span, .. }
845            | Expr::Vector { span, .. }
846            | Expr::Enum { span, .. }
847            | Expr::GlobalVar { span, .. }
848            | Expr::PlayerVar { span, .. }
849            | Expr::HostPlayer { span }
850            | Expr::Member { span, .. }
851            | Expr::EventPlayer { span }
852            | Expr::Constant { span, .. }
853            | Expr::Call { span, .. }
854            | Expr::ReceiverCall { span, .. }
855            | Expr::MacroCall { span, .. }
856            | Expr::MacroParam { span, .. }
857            | Expr::Type { span, .. }
858            | Expr::Binary { span, .. }
859            | Expr::Conditional { span, .. }
860            | Expr::Unary { span, .. }
861            | Expr::Index { span, .. }
862            | Expr::Format { span, .. } => span.as_ref(),
863        }
864    }
865
866    pub(crate) fn span_mut(&mut self) -> &mut Option<Span> {
867        match self {
868            Expr::Number { span, .. }
869            | Expr::String { span, .. }
870            | Expr::Bool { span, .. }
871            | Expr::Null { span }
872            | Expr::Array { span, .. }
873            | Expr::Dict { span, .. }
874            | Expr::Comprehension { span, .. }
875            | Expr::Lambda { span, .. }
876            | Expr::StringModifier { span, .. }
877            | Expr::Local { span, .. }
878            | Expr::Vector { span, .. }
879            | Expr::Enum { span, .. }
880            | Expr::GlobalVar { span, .. }
881            | Expr::PlayerVar { span, .. }
882            | Expr::HostPlayer { span }
883            | Expr::Member { span, .. }
884            | Expr::EventPlayer { span }
885            | Expr::Constant { span, .. }
886            | Expr::Call { span, .. }
887            | Expr::ReceiverCall { span, .. }
888            | Expr::MacroCall { span, .. }
889            | Expr::MacroParam { span, .. }
890            | Expr::Type { span, .. }
891            | Expr::Binary { span, .. }
892            | Expr::Conditional { span, .. }
893            | Expr::Unary { span, .. }
894            | Expr::Index { span, .. }
895            | Expr::Format { span, .. } => span,
896        }
897    }
898
899    /// The protocol `kind` of this expression.
900    pub fn kind_name(&self) -> &'static str {
901        match self {
902            Expr::Number { .. } => "number",
903            Expr::String { .. } => "string",
904            Expr::Bool { .. } => "bool",
905            Expr::Null { .. } => "null",
906            Expr::Array { .. } => "array",
907            Expr::Dict { .. } => "dict",
908            Expr::Comprehension { .. } => "comprehension",
909            Expr::Lambda { .. } => "lambda",
910            Expr::StringModifier { .. } => "stringModifier",
911            Expr::Local { .. } => "local",
912            Expr::Vector { .. } => "vector",
913            Expr::Enum { .. } => "enum",
914            Expr::GlobalVar { .. } => "globalVar",
915            Expr::PlayerVar { .. } => "playerVar",
916            Expr::HostPlayer { .. } => "hostPlayer",
917            Expr::Member { .. } => "member",
918            Expr::EventPlayer { .. } => "eventPlayer",
919            Expr::Constant { .. } => "constant",
920            Expr::Call { .. } => "call",
921            Expr::ReceiverCall { .. } => "receiverCall",
922            Expr::MacroCall { .. } => "macroCall",
923            Expr::MacroParam { .. } => "macroParam",
924            Expr::Type { .. } => "type",
925            Expr::Binary { .. } => "binary",
926            Expr::Conditional { .. } => "conditional",
927            Expr::Unary { .. } => "unary",
928            Expr::Index { .. } => "index",
929            Expr::Format { .. } => "format",
930        }
931    }
932}