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