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::{DeclarationProvenance, Program, ProgramProvenance, action_argument_count, fit};
6use crate::source::{FileId, Position, SourceFile, Span};
7
8/// Identifier of the canonical Workshop text artifact: the Workshop text alone.
9pub const TEXT_V1: &str = "workshop-rs/text-v1";
10
11/// Identifier of the canonical mapped Workshop artifact: Workshop text plus a
12/// [`SourceMap`], serialized by [`MappedText::to_json`].
13pub const MAPPED_TEXT_V1: &str = "workshop-rs/mapped-text-v1";
14
15/// A source mapping detached from a [`Program`].
16///
17/// A source map records the file table, the program shape, and the
18/// position-keyed spans of a span-bearing program. Extract it with
19/// [`SourceMap::extract`], carry it beside the emitted Workshop text, and
20/// [`apply`](Self::apply) it to a program parsed from that text. Spans use
21/// [`Position`] units: 1-based lines and columns counted in Unicode scalar
22/// values.
23///
24/// The mapping granularity is rule, condition, action, direct action argument,
25/// and variable and subroutine declarations. Nodes without an authored origin
26/// have no entry, so consumers report evidence on them as unmapped.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SourceMap {
29    files: Vec<String>,
30    shape: Shape,
31    spans: Vec<MappedNode>,
32}
33
34/// Workshop text together with the [`SourceMap`] of its authored origin: the
35/// `workshop-rs/mapped-text-v1` artifact.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct MappedText {
39    /// The Workshop text, itself a `workshop-rs/text-v1` artifact.
40    pub text: String,
41    /// The mapping from the program parsed from [`text`](Self::text) to the
42    /// authored source.
43    pub map: SourceMap,
44}
45
46/// A failure while decoding or applying a [`SourceMap`].
47///
48/// A failed [`SourceMap::apply`] leaves the program unchanged.
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum SourceMapError {
52    GlobalVariableCount {
53        expected: usize,
54        found: usize,
55    },
56    PlayerVariableCount {
57        expected: usize,
58        found: usize,
59    },
60    SubroutineCount {
61        expected: usize,
62        found: usize,
63    },
64    RuleCount {
65        expected: usize,
66        found: usize,
67    },
68    ConditionCount {
69        rule: usize,
70        expected: usize,
71        found: usize,
72    },
73    ActionCount {
74        rule: usize,
75        expected: usize,
76        found: usize,
77    },
78    /// An entry addresses a node outside the program shape.
79    InvalidPosition,
80    /// Two entries map the same node.
81    DuplicateEntry,
82    /// A declaration entry carries neither a span nor a name span.
83    EmptyEntry,
84    /// A span references a file outside the file table.
85    UnknownFile(usize),
86    /// A span is not a valid 1-based interval.
87    InvalidSpan(Span),
88    /// The artifact declares a format other than `workshop-rs/mapped-text-v1`.
89    UnsupportedFormat(String),
90    /// The artifact is not well-formed JSON of the expected structure.
91    Malformed(String),
92}
93
94impl std::fmt::Display for SourceMapError {
95    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        let mismatch = |formatter: &mut std::fmt::Formatter<'_>, what, expected, found| {
97            write!(
98                formatter,
99                "source map shape mismatch: expected {expected} {what}, found {found}"
100            )
101        };
102        match self {
103            Self::GlobalVariableCount { expected, found } => {
104                mismatch(formatter, "global variables", expected, found)
105            }
106            Self::PlayerVariableCount { expected, found } => {
107                mismatch(formatter, "player variables", expected, found)
108            }
109            Self::SubroutineCount { expected, found } => {
110                mismatch(formatter, "subroutines", expected, found)
111            }
112            Self::RuleCount { expected, found } => mismatch(formatter, "rules", expected, found),
113            Self::ConditionCount {
114                rule,
115                expected,
116                found,
117            } => mismatch(
118                formatter,
119                &format!("conditions in rule {rule}"),
120                expected,
121                found,
122            ),
123            Self::ActionCount {
124                rule,
125                expected,
126                found,
127            } => mismatch(
128                formatter,
129                &format!("actions in rule {rule}"),
130                expected,
131                found,
132            ),
133            Self::InvalidPosition => {
134                write!(formatter, "source map entry is outside the program shape")
135            }
136            Self::DuplicateEntry => write!(formatter, "source map maps a node twice"),
137            Self::EmptyEntry => write!(formatter, "source map declaration entry has no span"),
138            Self::UnknownFile(file) => {
139                write!(formatter, "source map span references unknown file {file}")
140            }
141            Self::InvalidSpan(span) => write!(formatter, "invalid source map span {span:?}"),
142            Self::UnsupportedFormat(format) => {
143                write!(formatter, "unsupported mapped text format {format:?}")
144            }
145            Self::Malformed(message) => write!(formatter, "malformed mapped text: {message}"),
146        }
147    }
148}
149
150impl std::error::Error for SourceMapError {}
151
152impl SourceMap {
153    /// Extract the current mapping of a span-bearing program.
154    ///
155    /// Only spans that are still valid for the program's current shape are
156    /// extracted; see [`Program::rule_span`].
157    pub fn extract(program: &Program) -> Self {
158        let mut spans = Vec::new();
159        push_declarations(
160            program,
161            program.global_variables.len(),
162            |provenance| &provenance.global_variables,
163            |index, span, name_span| MappedNode::GlobalVariable {
164                index,
165                span,
166                name_span,
167            },
168            &mut spans,
169        );
170        push_declarations(
171            program,
172            program.player_variables.len(),
173            |provenance| &provenance.player_variables,
174            |index, span, name_span| MappedNode::PlayerVariable {
175                index,
176                span,
177                name_span,
178            },
179            &mut spans,
180        );
181        push_declarations(
182            program,
183            program.subroutines.len(),
184            |provenance| &provenance.subroutines,
185            |index, span, name_span| MappedNode::Subroutine {
186                index,
187                span,
188                name_span,
189            },
190            &mut spans,
191        );
192        for (rule, public) in program.rules.iter().enumerate() {
193            if let Some(span) = program.rule_span(rule) {
194                spans.push(MappedNode::Rule {
195                    rule,
196                    span: span.into(),
197                });
198            }
199            for condition in 0..public.conditions.len() {
200                if let Some(span) = program.condition_span(rule, condition) {
201                    spans.push(MappedNode::Condition {
202                        rule,
203                        condition,
204                        span: span.into(),
205                    });
206                }
207            }
208            for (action, public_action) in public.actions.iter().enumerate() {
209                if let Some(span) = program.action_span(rule, action) {
210                    spans.push(MappedNode::Action {
211                        rule,
212                        action,
213                        span: span.into(),
214                    });
215                }
216                for argument in 0..action_argument_count(public_action) {
217                    if let Some(span) = program.action_argument_span(rule, action, argument) {
218                        spans.push(MappedNode::ActionArgument {
219                            rule,
220                            action,
221                            argument,
222                            span: span.into(),
223                        });
224                    }
225                }
226            }
227        }
228        Self {
229            files: program.files.iter().map(|file| file.path.clone()).collect(),
230            shape: Shape::of(program),
231            spans,
232        }
233    }
234
235    /// The paths of the file table that mapped spans refer to by file index.
236    pub fn files(&self) -> &[String] {
237        &self.files
238    }
239
240    /// Replace the source mapping of `program` with this map.
241    ///
242    /// The program's file table becomes this map's file table, and nodes
243    /// without an entry carry no span. The program must have exactly the shape
244    /// the map was extracted from; otherwise the whole mapping is rejected and
245    /// `program` is unchanged.
246    pub fn apply(&self, program: &mut Program) -> Result<(), SourceMapError> {
247        self.shape.check(program)?;
248
249        let mut provenance = ProgramProvenance::default();
250        fit(
251            &mut provenance.global_variables,
252            self.shape.global_variables,
253        );
254        fit(
255            &mut provenance.player_variables,
256            self.shape.player_variables,
257        );
258        fit(&mut provenance.subroutines, self.shape.subroutines);
259        fit(&mut provenance.rules, self.shape.rules.len());
260        for (rule, shape) in provenance.rules.iter_mut().zip(&self.shape.rules) {
261            fit(&mut rule.conditions, shape.conditions);
262            fit(&mut rule.actions, shape.actions);
263        }
264
265        for node in &self.spans {
266            match node {
267                MappedNode::GlobalVariable {
268                    index,
269                    span,
270                    name_span,
271                } => {
272                    let declaration = provenance
273                        .global_variables
274                        .get_mut(*index)
275                        .ok_or(SourceMapError::InvalidPosition)?;
276                    let mapped = self.declaration(*span, *name_span)?;
277                    if declaration.span.is_some() || declaration.name_span.is_some() {
278                        return Err(SourceMapError::DuplicateEntry);
279                    }
280                    *declaration = mapped;
281                }
282                MappedNode::PlayerVariable {
283                    index,
284                    span,
285                    name_span,
286                } => {
287                    let declaration = provenance
288                        .player_variables
289                        .get_mut(*index)
290                        .ok_or(SourceMapError::InvalidPosition)?;
291                    let mapped = self.declaration(*span, *name_span)?;
292                    if declaration.span.is_some() || declaration.name_span.is_some() {
293                        return Err(SourceMapError::DuplicateEntry);
294                    }
295                    *declaration = mapped;
296                }
297                MappedNode::Subroutine {
298                    index,
299                    span,
300                    name_span,
301                } => {
302                    let declaration = provenance
303                        .subroutines
304                        .get_mut(*index)
305                        .ok_or(SourceMapError::InvalidPosition)?;
306                    let mapped = self.declaration(*span, *name_span)?;
307                    if declaration.span.is_some() || declaration.name_span.is_some() {
308                        return Err(SourceMapError::DuplicateEntry);
309                    }
310                    *declaration = mapped;
311                }
312                MappedNode::Rule { rule, span } => {
313                    let span = self.span(*span)?;
314                    let slot = &mut provenance
315                        .rules
316                        .get_mut(*rule)
317                        .ok_or(SourceMapError::InvalidPosition)?
318                        .span;
319                    if slot.replace(span).is_some() {
320                        return Err(SourceMapError::DuplicateEntry);
321                    }
322                }
323                MappedNode::Condition {
324                    rule,
325                    condition,
326                    span,
327                } => {
328                    let span = self.span(*span)?;
329                    let slot = provenance
330                        .rules
331                        .get_mut(*rule)
332                        .and_then(|rule| rule.conditions.get_mut(*condition))
333                        .ok_or(SourceMapError::InvalidPosition)?;
334                    if slot.span.replace(span).is_some() {
335                        return Err(SourceMapError::DuplicateEntry);
336                    }
337                }
338                MappedNode::Action { rule, action, span } => {
339                    let span = self.span(*span)?;
340                    let slot = &mut provenance
341                        .rules
342                        .get_mut(*rule)
343                        .and_then(|rule| rule.actions.get_mut(*action))
344                        .ok_or(SourceMapError::InvalidPosition)?
345                        .span;
346                    if slot.replace(span).is_some() {
347                        return Err(SourceMapError::DuplicateEntry);
348                    }
349                }
350                MappedNode::ActionArgument {
351                    rule,
352                    action,
353                    argument,
354                    span,
355                } => {
356                    let span = self.span(*span)?;
357                    let count = program
358                        .rules
359                        .get(*rule)
360                        .and_then(|rule| rule.actions.get(*action))
361                        .map(action_argument_count)
362                        .ok_or(SourceMapError::InvalidPosition)?;
363                    if *argument >= count {
364                        return Err(SourceMapError::InvalidPosition);
365                    }
366                    let arguments = &mut provenance
367                        .rules
368                        .get_mut(*rule)
369                        .and_then(|rule| rule.actions.get_mut(*action))
370                        .ok_or(SourceMapError::InvalidPosition)?
371                        .arguments;
372                    fit(arguments, count);
373                    if arguments[*argument].span.replace(span).is_some() {
374                        return Err(SourceMapError::DuplicateEntry);
375                    }
376                }
377            }
378        }
379
380        program.files.clear();
381        for path in &self.files {
382            program.add_file(SourceFile::new(path.clone()));
383        }
384        program.provenance = Some(Box::new(provenance));
385        Ok(())
386    }
387
388    fn span(&self, wire: WireSpan) -> Result<Span, SourceMapError> {
389        if wire.file >= self.files.len() {
390            return Err(SourceMapError::UnknownFile(wire.file));
391        }
392        let span = Span::from(wire);
393        if !span.is_valid() {
394            return Err(SourceMapError::InvalidSpan(span));
395        }
396        Ok(span)
397    }
398
399    fn declaration(
400        &self,
401        span: Option<WireSpan>,
402        name_span: Option<WireSpan>,
403    ) -> Result<DeclarationProvenance, SourceMapError> {
404        if span.is_none() && name_span.is_none() {
405            return Err(SourceMapError::EmptyEntry);
406        }
407        Ok(DeclarationProvenance {
408            span: span.map(|span| self.span(span)).transpose()?,
409            name_span: name_span.map(|span| self.span(span)).transpose()?,
410        })
411    }
412}
413
414impl MappedText {
415    pub fn new(text: impl Into<String>, map: SourceMap) -> Self {
416        Self {
417            text: text.into(),
418            map,
419        }
420    }
421
422    /// Serialize as a `workshop-rs/mapped-text-v1` JSON document.
423    pub fn to_json(&self) -> String {
424        let artifact = Artifact {
425            format: MAPPED_TEXT_V1.to_string(),
426            text: self.text.clone(),
427            files: self
428                .map
429                .files
430                .iter()
431                .map(|path| WireFile { path: path.clone() })
432                .collect(),
433            shape: self.map.shape.clone(),
434            spans: self.map.spans.clone(),
435        };
436        serde_json::to_string(&artifact).expect("mapped text serializes to JSON")
437    }
438
439    /// Decode a `workshop-rs/mapped-text-v1` JSON document.
440    ///
441    /// Decoding checks the format and structure only; [`SourceMap::apply`]
442    /// validates the mapping against the program it is applied to.
443    pub fn from_json(json: &str) -> Result<Self, SourceMapError> {
444        let value: serde_json::Value = serde_json::from_str(json)
445            .map_err(|error| SourceMapError::Malformed(error.to_string()))?;
446        match value.get("format").and_then(serde_json::Value::as_str) {
447            Some(MAPPED_TEXT_V1) => {}
448            Some(other) => return Err(SourceMapError::UnsupportedFormat(other.to_string())),
449            None => return Err(SourceMapError::Malformed("missing format".to_string())),
450        }
451        let artifact: Artifact = serde_json::from_value(value)
452            .map_err(|error| SourceMapError::Malformed(error.to_string()))?;
453        Ok(Self {
454            text: artifact.text,
455            map: SourceMap {
456                files: artifact.files.into_iter().map(|file| file.path).collect(),
457                shape: artifact.shape,
458                spans: artifact.spans,
459            },
460        })
461    }
462}
463
464fn push_declarations(
465    program: &Program,
466    count: usize,
467    recorded: impl Fn(&ProgramProvenance) -> &[DeclarationProvenance],
468    node: impl Fn(usize, Option<WireSpan>, Option<WireSpan>) -> MappedNode,
469    output: &mut Vec<MappedNode>,
470) {
471    for index in 0..count {
472        let declaration = program.declaration_provenance(&recorded, count, index);
473        if declaration.span.is_some() || declaration.name_span.is_some() {
474            output.push(node(
475                index,
476                declaration.span.map(WireSpan::from),
477                declaration.name_span.map(WireSpan::from),
478            ));
479        }
480    }
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484struct Artifact {
485    format: String,
486    text: String,
487    files: Vec<WireFile>,
488    shape: Shape,
489    spans: Vec<MappedNode>,
490}
491
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493struct WireFile {
494    path: String,
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498struct Shape {
499    global_variables: usize,
500    player_variables: usize,
501    subroutines: usize,
502    rules: Vec<RuleShape>,
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506struct RuleShape {
507    conditions: usize,
508    actions: usize,
509}
510
511impl Shape {
512    fn of(program: &Program) -> Self {
513        Self {
514            global_variables: program.global_variables.len(),
515            player_variables: program.player_variables.len(),
516            subroutines: program.subroutines.len(),
517            rules: program
518                .rules
519                .iter()
520                .map(|rule| RuleShape {
521                    conditions: rule.conditions.len(),
522                    actions: rule.actions.len(),
523                })
524                .collect(),
525        }
526    }
527
528    fn check(&self, program: &Program) -> Result<(), SourceMapError> {
529        let found = Self::of(program);
530        if self.global_variables != found.global_variables {
531            return Err(SourceMapError::GlobalVariableCount {
532                expected: self.global_variables,
533                found: found.global_variables,
534            });
535        }
536        if self.player_variables != found.player_variables {
537            return Err(SourceMapError::PlayerVariableCount {
538                expected: self.player_variables,
539                found: found.player_variables,
540            });
541        }
542        if self.subroutines != found.subroutines {
543            return Err(SourceMapError::SubroutineCount {
544                expected: self.subroutines,
545                found: found.subroutines,
546            });
547        }
548        if self.rules.len() != found.rules.len() {
549            return Err(SourceMapError::RuleCount {
550                expected: self.rules.len(),
551                found: found.rules.len(),
552            });
553        }
554        for (rule, (expected, found)) in self.rules.iter().zip(&found.rules).enumerate() {
555            if expected.conditions != found.conditions {
556                return Err(SourceMapError::ConditionCount {
557                    rule,
558                    expected: expected.conditions,
559                    found: found.conditions,
560                });
561            }
562            if expected.actions != found.actions {
563                return Err(SourceMapError::ActionCount {
564                    rule,
565                    expected: expected.actions,
566                    found: found.actions,
567                });
568            }
569        }
570        Ok(())
571    }
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(tag = "node", rename_all = "snake_case")]
576enum MappedNode {
577    Rule {
578        rule: usize,
579        span: WireSpan,
580    },
581    Condition {
582        rule: usize,
583        condition: usize,
584        span: WireSpan,
585    },
586    Action {
587        rule: usize,
588        action: usize,
589        span: WireSpan,
590    },
591    ActionArgument {
592        rule: usize,
593        action: usize,
594        argument: usize,
595        span: WireSpan,
596    },
597    GlobalVariable {
598        index: usize,
599        #[serde(default, skip_serializing_if = "Option::is_none")]
600        span: Option<WireSpan>,
601        #[serde(default, skip_serializing_if = "Option::is_none")]
602        name_span: Option<WireSpan>,
603    },
604    PlayerVariable {
605        index: usize,
606        #[serde(default, skip_serializing_if = "Option::is_none")]
607        span: Option<WireSpan>,
608        #[serde(default, skip_serializing_if = "Option::is_none")]
609        name_span: Option<WireSpan>,
610    },
611    Subroutine {
612        index: usize,
613        #[serde(default, skip_serializing_if = "Option::is_none")]
614        span: Option<WireSpan>,
615        #[serde(default, skip_serializing_if = "Option::is_none")]
616        name_span: Option<WireSpan>,
617    },
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
621struct WireSpan {
622    file: usize,
623    start: WirePosition,
624    end: WirePosition,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
628struct WirePosition {
629    line: u32,
630    column: u32,
631}
632
633impl From<Span> for WireSpan {
634    fn from(span: Span) -> Self {
635        Self {
636            file: span.file.index(),
637            start: span.start.into(),
638            end: span.end.into(),
639        }
640    }
641}
642
643impl From<WireSpan> for Span {
644    fn from(wire: WireSpan) -> Self {
645        Span::new(
646            FileId::from_index(wire.file),
647            wire.start.into(),
648            wire.end.into(),
649        )
650    }
651}
652
653impl From<Position> for WirePosition {
654    fn from(position: Position) -> Self {
655        Self {
656            line: position.line,
657            column: position.col,
658        }
659    }
660}
661
662impl From<WirePosition> for Position {
663    fn from(position: WirePosition) -> Self {
664        Position::new(position.line, position.column)
665    }
666}