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