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