Skip to main content

workshop_rs/program/
source_map.rs

1//! Canonical Workshop artifact formats and the public [`SourceMap`].
2
3use serde::{Deserialize, Serialize};
4
5use super::{
6    DeclarationProvenance, Program, ProgramProvenance, Value, ValueProvenance,
7    action_argument_count, action_argument_values, fit, value_children,
8};
9use crate::source::{FileId, Position, SourceFile, Span};
10
11/// Identifier of the canonical Workshop text artifact: the Workshop text alone.
12pub const TEXT_V1: &str = "workshop-rs/text-v1";
13
14/// Identifier of the canonical mapped Workshop artifact: Workshop text plus a
15/// [`SourceMap`], serialized by [`MappedText::to_json`].
16pub const MAPPED_TEXT_V1: &str = "workshop-rs/mapped-text-v1";
17
18/// A source mapping detached from a [`Program`].
19///
20/// A source map records the file table, the program shape, and the
21/// position-keyed spans of a span-bearing program. Extract it with
22/// [`SourceMap::extract`], carry it beside the emitted Workshop text, and
23/// [`apply`](Self::apply) it to a program parsed from that text. Spans use
24/// [`Position`] units: 1-based lines and columns counted in Unicode scalar
25/// values.
26///
27/// The mapping granularity is rule, condition, action, direct action argument,
28/// and variable and subroutine declarations. Entries may additionally carry the
29/// identifier span the node names — a rule's name, a rule's subroutine event
30/// binding, an action's target or callee, a value's variable or subroutine
31/// identifier — and condition and action-argument entries carry the provenance
32/// of the value's children, keyed by position in the public [`Value`] tree.
33/// Every field of an entry is optional: a node with only finer-grained
34/// provenance, such as a member read that records just its identifier,
35/// appears as an entry without `span`. Nodes without an authored origin have
36/// no entry, so consumers report evidence on them as unmapped.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SourceMap {
39    files: Vec<String>,
40    shape: Shape,
41    spans: Vec<MappedNode>,
42}
43
44/// Workshop text together with the [`SourceMap`] of its authored origin: the
45/// `workshop-rs/mapped-text-v1` artifact.
46#[derive(Debug, Clone, PartialEq, Eq)]
47#[non_exhaustive]
48pub struct MappedText {
49    /// The Workshop text, itself a `workshop-rs/text-v1` artifact.
50    pub text: String,
51    /// The mapping from the program parsed from [`text`](Self::text) to the
52    /// authored source.
53    pub map: SourceMap,
54}
55
56/// A failure while decoding or applying a [`SourceMap`].
57///
58/// A failed [`SourceMap::apply`] leaves the program unchanged.
59#[derive(Debug, Clone, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum SourceMapError {
62    GlobalVariableCount {
63        expected: usize,
64        found: usize,
65    },
66    PlayerVariableCount {
67        expected: usize,
68        found: usize,
69    },
70    SubroutineCount {
71        expected: usize,
72        found: usize,
73    },
74    RuleCount {
75        expected: usize,
76        found: usize,
77    },
78    ConditionCount {
79        rule: usize,
80        expected: usize,
81        found: usize,
82    },
83    ActionCount {
84        rule: usize,
85        expected: usize,
86        found: usize,
87    },
88    /// An entry addresses a node outside the program shape.
89    InvalidPosition,
90    /// Two entries map the same node.
91    DuplicateEntry,
92    /// An entry carries no position at all.
93    EmptyEntry,
94    /// A span references a file outside the file table.
95    UnknownFile(usize),
96    /// A span is not a valid 1-based interval.
97    InvalidSpan(Span),
98    /// The artifact declares a format other than `workshop-rs/mapped-text-v1`.
99    UnsupportedFormat(String),
100    /// The artifact is not well-formed JSON of the expected structure.
101    Malformed(String),
102}
103
104impl std::fmt::Display for SourceMapError {
105    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        let mismatch = |formatter: &mut std::fmt::Formatter<'_>, what, expected, found| {
107            write!(
108                formatter,
109                "source map shape mismatch: expected {expected} {what}, found {found}"
110            )
111        };
112        match self {
113            Self::GlobalVariableCount { expected, found } => {
114                mismatch(formatter, "global variables", expected, found)
115            }
116            Self::PlayerVariableCount { expected, found } => {
117                mismatch(formatter, "player variables", expected, found)
118            }
119            Self::SubroutineCount { expected, found } => {
120                mismatch(formatter, "subroutines", expected, found)
121            }
122            Self::RuleCount { expected, found } => mismatch(formatter, "rules", expected, found),
123            Self::ConditionCount {
124                rule,
125                expected,
126                found,
127            } => mismatch(
128                formatter,
129                &format!("conditions in rule {rule}"),
130                expected,
131                found,
132            ),
133            Self::ActionCount {
134                rule,
135                expected,
136                found,
137            } => mismatch(
138                formatter,
139                &format!("actions in rule {rule}"),
140                expected,
141                found,
142            ),
143            Self::InvalidPosition => {
144                write!(formatter, "source map entry is outside the program shape")
145            }
146            Self::DuplicateEntry => write!(formatter, "source map maps a node twice"),
147            Self::EmptyEntry => write!(formatter, "source map entry has no position"),
148            Self::UnknownFile(file) => {
149                write!(formatter, "source map span references unknown file {file}")
150            }
151            Self::InvalidSpan(span) => write!(formatter, "invalid source map span {span:?}"),
152            Self::UnsupportedFormat(format) => {
153                write!(formatter, "unsupported mapped text format {format:?}")
154            }
155            Self::Malformed(message) => write!(formatter, "malformed mapped text: {message}"),
156        }
157    }
158}
159
160impl std::error::Error for SourceMapError {}
161
162impl SourceMap {
163    /// Extract the current mapping of a span-bearing program.
164    ///
165    /// Only spans that are still valid for the program's current shape are
166    /// extracted; see [`Program::rule_span`].
167    pub fn extract(program: &Program) -> Self {
168        let mut spans = Vec::new();
169        push_declarations(
170            program,
171            program.global_variables.len(),
172            |provenance| &provenance.global_variables,
173            |index, span, name_span| MappedNode::GlobalVariable {
174                index,
175                span,
176                name_span,
177            },
178            &mut spans,
179        );
180        push_declarations(
181            program,
182            program.player_variables.len(),
183            |provenance| &provenance.player_variables,
184            |index, span, name_span| MappedNode::PlayerVariable {
185                index,
186                span,
187                name_span,
188            },
189            &mut spans,
190        );
191        push_declarations(
192            program,
193            program.subroutines.len(),
194            |provenance| &provenance.subroutines,
195            |index, span, name_span| MappedNode::Subroutine {
196                index,
197                span,
198                name_span,
199            },
200            &mut spans,
201        );
202        for (rule, public) in program.rules.iter().enumerate() {
203            if let Some(provenance) = program.rule_provenance(rule) {
204                if provenance.span.is_some()
205                    || provenance.name.is_some()
206                    || provenance.event_name.is_some()
207                {
208                    spans.push(MappedNode::Rule {
209                        rule,
210                        span: provenance.span.map(WireSpan::from),
211                        name_span: provenance.name.map(WireSpan::from),
212                        event_name_span: provenance.event_name.map(WireSpan::from),
213                    });
214                }
215            }
216            for condition in 0..public.conditions.len() {
217                let Some(provenance) = program.condition_provenance(rule, condition) else {
218                    continue;
219                };
220                let children = wire_children(&provenance.children);
221                if provenance.span.is_some()
222                    || provenance.identifier.is_some()
223                    || !children.is_empty()
224                {
225                    spans.push(MappedNode::Condition {
226                        rule,
227                        condition,
228                        span: provenance.span.map(WireSpan::from),
229                        identifier_span: provenance.identifier.map(WireSpan::from),
230                        children,
231                    });
232                }
233            }
234            for (action, public_action) in public.actions.iter().enumerate() {
235                let Some(provenance) = program.action_provenance(rule, action) else {
236                    continue;
237                };
238                if provenance.span.is_some() || provenance.identifier.is_some() {
239                    spans.push(MappedNode::Action {
240                        rule,
241                        action,
242                        span: provenance.span.map(WireSpan::from),
243                        identifier_span: provenance.identifier.map(WireSpan::from),
244                    });
245                }
246                for argument in 0..action_argument_count(public_action) {
247                    let Some(argument_provenance) = provenance.arguments.get(argument) else {
248                        continue;
249                    };
250                    let children = wire_children(&argument_provenance.children);
251                    if argument_provenance.span.is_some()
252                        || argument_provenance.identifier.is_some()
253                        || !children.is_empty()
254                    {
255                        spans.push(MappedNode::ActionArgument {
256                            rule,
257                            action,
258                            argument,
259                            span: argument_provenance.span.map(WireSpan::from),
260                            identifier_span: argument_provenance.identifier.map(WireSpan::from),
261                            children,
262                        });
263                    }
264                }
265            }
266        }
267        Self {
268            files: program.files.iter().map(|file| file.path.clone()).collect(),
269            shape: Shape::of(program),
270            spans,
271        }
272    }
273
274    /// The paths of the file table that mapped spans refer to by file index.
275    pub fn files(&self) -> &[String] {
276        &self.files
277    }
278
279    /// Replace the source mapping of `program` with this map.
280    ///
281    /// The program's file table becomes this map's file table, and nodes
282    /// without an entry carry no span. The program must have exactly the shape
283    /// the map was extracted from; otherwise the whole mapping is rejected and
284    /// `program` is unchanged.
285    pub fn apply(&self, program: &mut Program) -> Result<(), SourceMapError> {
286        self.shape.check(program)?;
287
288        let mut provenance = ProgramProvenance::default();
289        fit(
290            &mut provenance.global_variables,
291            self.shape.global_variables,
292        );
293        fit(
294            &mut provenance.player_variables,
295            self.shape.player_variables,
296        );
297        fit(&mut provenance.subroutines, self.shape.subroutines);
298        fit(&mut provenance.rules, self.shape.rules.len());
299        for (rule, shape) in provenance.rules.iter_mut().zip(&self.shape.rules) {
300            fit(&mut rule.conditions, shape.conditions);
301            fit(&mut rule.actions, shape.actions);
302        }
303
304        for node in &self.spans {
305            match node {
306                MappedNode::GlobalVariable {
307                    index,
308                    span,
309                    name_span,
310                } => {
311                    let declaration = provenance
312                        .global_variables
313                        .get_mut(*index)
314                        .ok_or(SourceMapError::InvalidPosition)?;
315                    let mapped = self.declaration(*span, *name_span)?;
316                    if declaration.span.is_some() || declaration.name_span.is_some() {
317                        return Err(SourceMapError::DuplicateEntry);
318                    }
319                    *declaration = mapped;
320                }
321                MappedNode::PlayerVariable {
322                    index,
323                    span,
324                    name_span,
325                } => {
326                    let declaration = provenance
327                        .player_variables
328                        .get_mut(*index)
329                        .ok_or(SourceMapError::InvalidPosition)?;
330                    let mapped = self.declaration(*span, *name_span)?;
331                    if declaration.span.is_some() || declaration.name_span.is_some() {
332                        return Err(SourceMapError::DuplicateEntry);
333                    }
334                    *declaration = mapped;
335                }
336                MappedNode::Subroutine {
337                    index,
338                    span,
339                    name_span,
340                } => {
341                    let declaration = provenance
342                        .subroutines
343                        .get_mut(*index)
344                        .ok_or(SourceMapError::InvalidPosition)?;
345                    let mapped = self.declaration(*span, *name_span)?;
346                    if declaration.span.is_some() || declaration.name_span.is_some() {
347                        return Err(SourceMapError::DuplicateEntry);
348                    }
349                    *declaration = mapped;
350                }
351                MappedNode::Rule {
352                    rule,
353                    span,
354                    name_span,
355                    event_name_span,
356                } => {
357                    let span = span.map(|span| self.span(span)).transpose()?;
358                    let name = name_span.map(|span| self.span(span)).transpose()?;
359                    let event_name = event_name_span.map(|span| self.span(span)).transpose()?;
360                    let slot = provenance
361                        .rules
362                        .get_mut(*rule)
363                        .ok_or(SourceMapError::InvalidPosition)?;
364                    if slot.span.is_some() || slot.name.is_some() || slot.event_name.is_some() {
365                        return Err(SourceMapError::DuplicateEntry);
366                    }
367                    if span.is_none() && name.is_none() && event_name.is_none() {
368                        return Err(SourceMapError::EmptyEntry);
369                    }
370                    slot.span = span;
371                    slot.name = name;
372                    slot.event_name = event_name;
373                }
374                MappedNode::Condition {
375                    rule,
376                    condition,
377                    span,
378                    identifier_span,
379                    children,
380                } => {
381                    let span = span.map(|span| self.span(span)).transpose()?;
382                    let identifier = identifier_span.map(|span| self.span(span)).transpose()?;
383                    let has_children = children.iter().any(|child| !wire_value_unmapped(child));
384                    let children = self.mapped_children(
385                        children,
386                        &program
387                            .rules
388                            .get(*rule)
389                            .and_then(|rule| rule.conditions.get(*condition))
390                            .ok_or(SourceMapError::InvalidPosition)?
391                            .value,
392                    )?;
393                    let slot = provenance
394                        .rules
395                        .get_mut(*rule)
396                        .and_then(|rule| rule.conditions.get_mut(*condition))
397                        .ok_or(SourceMapError::InvalidPosition)?;
398                    if slot.span.is_some() || slot.identifier.is_some() || !slot.children.is_empty()
399                    {
400                        return Err(SourceMapError::DuplicateEntry);
401                    }
402                    if span.is_none() && identifier.is_none() && !has_children {
403                        return Err(SourceMapError::EmptyEntry);
404                    }
405                    slot.span = span;
406                    slot.identifier = identifier;
407                    slot.children = children;
408                }
409                MappedNode::Action {
410                    rule,
411                    action,
412                    span,
413                    identifier_span,
414                } => {
415                    let span = span.map(|span| self.span(span)).transpose()?;
416                    let identifier = identifier_span.map(|span| self.span(span)).transpose()?;
417                    let slot = provenance
418                        .rules
419                        .get_mut(*rule)
420                        .and_then(|rule| rule.actions.get_mut(*action))
421                        .ok_or(SourceMapError::InvalidPosition)?;
422                    if slot.span.is_some() || slot.identifier.is_some() {
423                        return Err(SourceMapError::DuplicateEntry);
424                    }
425                    if span.is_none() && identifier.is_none() {
426                        return Err(SourceMapError::EmptyEntry);
427                    }
428                    slot.span = span;
429                    slot.identifier = identifier;
430                }
431                MappedNode::ActionArgument {
432                    rule,
433                    action,
434                    argument,
435                    span,
436                    identifier_span,
437                    children,
438                } => {
439                    let span = span.map(|span| self.span(span)).transpose()?;
440                    let identifier = identifier_span.map(|span| self.span(span)).transpose()?;
441                    let argument_values = program
442                        .rules
443                        .get(*rule)
444                        .and_then(|rule| rule.actions.get(*action))
445                        .map(action_argument_values)
446                        .ok_or(SourceMapError::InvalidPosition)?;
447                    let Some(&value) = argument_values.get(*argument) else {
448                        return Err(SourceMapError::InvalidPosition);
449                    };
450                    let count = argument_values.len();
451                    let has_children = children.iter().any(|child| !wire_value_unmapped(child));
452                    let children = self.mapped_children(children, value)?;
453                    let arguments = &mut provenance
454                        .rules
455                        .get_mut(*rule)
456                        .and_then(|rule| rule.actions.get_mut(*action))
457                        .ok_or(SourceMapError::InvalidPosition)?
458                        .arguments;
459                    fit(arguments, count);
460                    let slot = &mut arguments[*argument];
461                    if slot.span.is_some() || slot.identifier.is_some() || !slot.children.is_empty()
462                    {
463                        return Err(SourceMapError::DuplicateEntry);
464                    }
465                    if span.is_none() && identifier.is_none() && !has_children {
466                        return Err(SourceMapError::EmptyEntry);
467                    }
468                    slot.span = span;
469                    slot.identifier = identifier;
470                    slot.children = children;
471                }
472            }
473        }
474
475        program.files.clear();
476        for path in &self.files {
477            program.add_file(SourceFile::new(path.clone()));
478        }
479        program.provenance = Some(Box::new(provenance));
480        Ok(())
481    }
482
483    fn span(&self, wire: WireSpan) -> Result<Span, SourceMapError> {
484        if wire.file >= self.files.len() {
485            return Err(SourceMapError::UnknownFile(wire.file));
486        }
487        let span = Span::from(wire);
488        if !span.is_valid() {
489            return Err(SourceMapError::InvalidSpan(span));
490        }
491        Ok(span)
492    }
493
494    fn declaration(
495        &self,
496        span: Option<WireSpan>,
497        name_span: Option<WireSpan>,
498    ) -> Result<DeclarationProvenance, SourceMapError> {
499        if span.is_none() && name_span.is_none() {
500            return Err(SourceMapError::EmptyEntry);
501        }
502        Ok(DeclarationProvenance {
503            span: span.map(|span| self.span(span)).transpose()?,
504            name_span: name_span.map(|span| self.span(span)).transpose()?,
505        })
506    }
507
508    /// Map serialized children onto a public value's children by position.
509    ///
510    /// Emission can canonicalize a value into a form with a different public
511    /// arity — `(expr).member` reparse as a variable read drops the member-name
512    /// child, for example — so children are matched positionally: entries
513    /// beyond the value's children are dropped and positions with no entry are
514    /// left unmapped.
515    fn mapped_children(
516        &self,
517        wire: &[WireValue],
518        value: &Value,
519    ) -> Result<Vec<ValueProvenance>, SourceMapError> {
520        let children = value_children(value);
521        let mut mapped = wire
522            .iter()
523            .zip(children.iter().copied())
524            .map(|(wire, value)| self.mapped_value(wire, value))
525            .collect::<Result<Vec<_>, _>>()?;
526        mapped.resize_with(children.len(), ValueProvenance::default);
527        Ok(mapped)
528    }
529
530    fn mapped_value(
531        &self,
532        wire: &WireValue,
533        value: &Value,
534    ) -> Result<ValueProvenance, SourceMapError> {
535        Ok(ValueProvenance {
536            span: wire.span.map(|span| self.span(span)).transpose()?,
537            identifier: wire
538                .identifier_span
539                .map(|span| self.span(span))
540                .transpose()?,
541            children: self.mapped_children(&wire.children, value)?,
542        })
543    }
544}
545
546impl MappedText {
547    pub fn new(text: impl Into<String>, map: SourceMap) -> Self {
548        Self {
549            text: text.into(),
550            map,
551        }
552    }
553
554    /// Serialize as a `workshop-rs/mapped-text-v1` JSON document.
555    pub fn to_json(&self) -> String {
556        let artifact = Artifact {
557            format: MAPPED_TEXT_V1.to_string(),
558            text: self.text.clone(),
559            files: self
560                .map
561                .files
562                .iter()
563                .map(|path| WireFile { path: path.clone() })
564                .collect(),
565            shape: self.map.shape.clone(),
566            spans: self.map.spans.clone(),
567        };
568        serde_json::to_string(&artifact).expect("mapped text serializes to JSON")
569    }
570
571    /// Decode a `workshop-rs/mapped-text-v1` JSON document.
572    ///
573    /// Decoding checks the format and structure only; [`SourceMap::apply`]
574    /// validates the mapping against the program it is applied to.
575    pub fn from_json(json: &str) -> Result<Self, SourceMapError> {
576        let value: serde_json::Value = serde_json::from_str(json)
577            .map_err(|error| SourceMapError::Malformed(error.to_string()))?;
578        match value.get("format").and_then(serde_json::Value::as_str) {
579            Some(MAPPED_TEXT_V1) => {}
580            Some(other) => return Err(SourceMapError::UnsupportedFormat(other.to_string())),
581            None => return Err(SourceMapError::Malformed("missing format".to_string())),
582        }
583        let artifact: Artifact = serde_json::from_value(value)
584            .map_err(|error| SourceMapError::Malformed(error.to_string()))?;
585        Ok(Self {
586            text: artifact.text,
587            map: SourceMap {
588                files: artifact.files.into_iter().map(|file| file.path).collect(),
589                shape: artifact.shape,
590                spans: artifact.spans,
591            },
592        })
593    }
594}
595
596/// The wire form of recorded value-provenance children: children keep their
597/// position up to the last mapped one, and a level with no recorded provenance
598/// at all is omitted.
599fn wire_children(children: &[ValueProvenance]) -> Vec<WireValue> {
600    let Some(last) = children.iter().rposition(|child| !value_unmapped(child)) else {
601        return Vec::new();
602    };
603    children[..=last].iter().map(wire_value).collect()
604}
605
606fn wire_value(provenance: &ValueProvenance) -> WireValue {
607    WireValue {
608        span: provenance.span.map(WireSpan::from),
609        identifier_span: provenance.identifier.map(WireSpan::from),
610        children: wire_children(&provenance.children),
611    }
612}
613
614fn value_unmapped(provenance: &ValueProvenance) -> bool {
615    provenance.span.is_none()
616        && provenance.identifier.is_none()
617        && provenance.children.iter().all(value_unmapped)
618}
619
620/// Whether a serialized value entry contributes any position.
621fn wire_value_unmapped(value: &WireValue) -> bool {
622    value.span.is_none()
623        && value.identifier_span.is_none()
624        && value.children.iter().all(wire_value_unmapped)
625}
626
627fn push_declarations(
628    program: &Program,
629    count: usize,
630    recorded: impl Fn(&ProgramProvenance) -> &[DeclarationProvenance],
631    node: impl Fn(usize, Option<WireSpan>, Option<WireSpan>) -> MappedNode,
632    output: &mut Vec<MappedNode>,
633) {
634    for index in 0..count {
635        let declaration = program.declaration_provenance(&recorded, count, index);
636        if declaration.span.is_some() || declaration.name_span.is_some() {
637            output.push(node(
638                index,
639                declaration.span.map(WireSpan::from),
640                declaration.name_span.map(WireSpan::from),
641            ));
642        }
643    }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647struct Artifact {
648    format: String,
649    text: String,
650    files: Vec<WireFile>,
651    shape: Shape,
652    spans: Vec<MappedNode>,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656struct WireFile {
657    path: String,
658}
659
660#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
661struct Shape {
662    global_variables: usize,
663    player_variables: usize,
664    subroutines: usize,
665    rules: Vec<RuleShape>,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
669struct RuleShape {
670    conditions: usize,
671    actions: usize,
672}
673
674impl Shape {
675    fn of(program: &Program) -> Self {
676        Self {
677            global_variables: program.global_variables.len(),
678            player_variables: program.player_variables.len(),
679            subroutines: program.subroutines.len(),
680            rules: program
681                .rules
682                .iter()
683                .map(|rule| RuleShape {
684                    conditions: rule.conditions.len(),
685                    actions: rule.actions.len(),
686                })
687                .collect(),
688        }
689    }
690
691    fn check(&self, program: &Program) -> Result<(), SourceMapError> {
692        let found = Self::of(program);
693        if self.global_variables != found.global_variables {
694            return Err(SourceMapError::GlobalVariableCount {
695                expected: self.global_variables,
696                found: found.global_variables,
697            });
698        }
699        if self.player_variables != found.player_variables {
700            return Err(SourceMapError::PlayerVariableCount {
701                expected: self.player_variables,
702                found: found.player_variables,
703            });
704        }
705        if self.subroutines != found.subroutines {
706            return Err(SourceMapError::SubroutineCount {
707                expected: self.subroutines,
708                found: found.subroutines,
709            });
710        }
711        if self.rules.len() != found.rules.len() {
712            return Err(SourceMapError::RuleCount {
713                expected: self.rules.len(),
714                found: found.rules.len(),
715            });
716        }
717        for (rule, (expected, found)) in self.rules.iter().zip(&found.rules).enumerate() {
718            if expected.conditions != found.conditions {
719                return Err(SourceMapError::ConditionCount {
720                    rule,
721                    expected: expected.conditions,
722                    found: found.conditions,
723                });
724            }
725            if expected.actions != found.actions {
726                return Err(SourceMapError::ActionCount {
727                    rule,
728                    expected: expected.actions,
729                    found: found.actions,
730                });
731            }
732        }
733        Ok(())
734    }
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
738#[serde(tag = "node", rename_all = "snake_case")]
739enum MappedNode {
740    Rule {
741        rule: usize,
742        #[serde(default, skip_serializing_if = "Option::is_none")]
743        span: Option<WireSpan>,
744        #[serde(default, skip_serializing_if = "Option::is_none")]
745        name_span: Option<WireSpan>,
746        #[serde(default, skip_serializing_if = "Option::is_none")]
747        event_name_span: Option<WireSpan>,
748    },
749    Condition {
750        rule: usize,
751        condition: usize,
752        #[serde(default, skip_serializing_if = "Option::is_none")]
753        span: Option<WireSpan>,
754        #[serde(default, skip_serializing_if = "Option::is_none")]
755        identifier_span: Option<WireSpan>,
756        #[serde(default, skip_serializing_if = "Vec::is_empty")]
757        children: Vec<WireValue>,
758    },
759    Action {
760        rule: usize,
761        action: usize,
762        #[serde(default, skip_serializing_if = "Option::is_none")]
763        span: Option<WireSpan>,
764        #[serde(default, skip_serializing_if = "Option::is_none")]
765        identifier_span: Option<WireSpan>,
766    },
767    ActionArgument {
768        rule: usize,
769        action: usize,
770        argument: usize,
771        #[serde(default, skip_serializing_if = "Option::is_none")]
772        span: Option<WireSpan>,
773        #[serde(default, skip_serializing_if = "Option::is_none")]
774        identifier_span: Option<WireSpan>,
775        #[serde(default, skip_serializing_if = "Vec::is_empty")]
776        children: Vec<WireValue>,
777    },
778    GlobalVariable {
779        index: usize,
780        #[serde(default, skip_serializing_if = "Option::is_none")]
781        span: Option<WireSpan>,
782        #[serde(default, skip_serializing_if = "Option::is_none")]
783        name_span: Option<WireSpan>,
784    },
785    PlayerVariable {
786        index: usize,
787        #[serde(default, skip_serializing_if = "Option::is_none")]
788        span: Option<WireSpan>,
789        #[serde(default, skip_serializing_if = "Option::is_none")]
790        name_span: Option<WireSpan>,
791    },
792    Subroutine {
793        index: usize,
794        #[serde(default, skip_serializing_if = "Option::is_none")]
795        span: Option<WireSpan>,
796        #[serde(default, skip_serializing_if = "Option::is_none")]
797        name_span: Option<WireSpan>,
798    },
799}
800
801/// The mapped provenance of one value node, mirroring the structure of the
802/// public [`Value`] tree: `children` addresses child values by position.
803#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804struct WireValue {
805    #[serde(default, skip_serializing_if = "Option::is_none")]
806    span: Option<WireSpan>,
807    #[serde(default, skip_serializing_if = "Option::is_none")]
808    identifier_span: Option<WireSpan>,
809    #[serde(default, skip_serializing_if = "Vec::is_empty")]
810    children: Vec<WireValue>,
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
814struct WireSpan {
815    file: usize,
816    start: WirePosition,
817    end: WirePosition,
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
821struct WirePosition {
822    line: u32,
823    column: u32,
824}
825
826impl From<Span> for WireSpan {
827    fn from(span: Span) -> Self {
828        Self {
829            file: span.file.index(),
830            start: span.start.into(),
831            end: span.end.into(),
832        }
833    }
834}
835
836impl From<WireSpan> for Span {
837    fn from(wire: WireSpan) -> Self {
838        Span::new(
839            FileId::from_index(wire.file),
840            wire.start.into(),
841            wire.end.into(),
842        )
843    }
844}
845
846impl From<Position> for WirePosition {
847    fn from(position: Position) -> Self {
848        Self {
849            line: position.line,
850            column: position.col,
851        }
852    }
853}
854
855impl From<WirePosition> for Position {
856    fn from(position: WirePosition) -> Self {
857        Position::new(position.line, position.column)
858    }
859}