Skip to main content

meta_language/program_representation/
snapshot.rs

1use serde_json::{Value, json};
2
3use super::{
4    LinkType, ProgramProjectContext, ProgramProjectSource, ProgramRepresentation,
5    ProgramRepresentationError, construct_program,
6};
7
8/// Schema revision for source-buffer-independent program snapshots.
9pub const PROGRAM_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
10
11impl ProgramRepresentation {
12    /// Serializes exact retained token fragments without a duplicate source field.
13    #[must_use]
14    pub fn serialize_snapshot(&self) -> String {
15        let fragments = retained_fragments(self);
16        let mut project = json!({
17            "root": self.project.root(),
18            "files": self.project.files(),
19            "dependencies": self.project.dependencies(),
20            "extensions": self.project.extensions(),
21        });
22        // A project analyzed from an entry file also carries its sources.
23        if let Some(entry) = self.project.entry() {
24            project["entry"] = json!(entry);
25            project["sources"] = self
26                .project
27                .sources()
28                .iter()
29                .map(|file| json!({ "path": file.path(), "source": file.source() }))
30                .collect();
31        }
32        json!({
33            "schemaVersion": PROGRAM_SNAPSHOT_SCHEMA_VERSION,
34            "language": self.language,
35            "project": project,
36            "fragments": fragments,
37        })
38        .to_string()
39    }
40
41    /// Reloads a validated snapshot without access to its original source buffer.
42    pub fn from_snapshot(snapshot: &str) -> Result<Self, ProgramRepresentationError> {
43        let value: Value = serde_json::from_str(snapshot).map_err(|error| {
44            ProgramRepresentationError::InvalidSnapshot(format!("invalid JSON: {error}"))
45        })?;
46        let object = value.as_object().ok_or_else(|| {
47            ProgramRepresentationError::InvalidSnapshot("expected an object".to_string())
48        })?;
49        let version = object.get("schemaVersion").and_then(Value::as_u64);
50        if version != Some(u64::from(PROGRAM_SNAPSHOT_SCHEMA_VERSION)) {
51            return Err(ProgramRepresentationError::InvalidSnapshot(format!(
52                "unsupported schema version {version:?}"
53            )));
54        }
55        let language = required_string(object.get("language"), "language")?;
56        let project = read_project(object.get("project"))?;
57        let fragments = object
58            .get("fragments")
59            .and_then(Value::as_array)
60            .ok_or_else(|| {
61                ProgramRepresentationError::InvalidSnapshot(
62                    "fragments must be an array".to_string(),
63                )
64            })?;
65        let mut source = String::new();
66        let mut expected_start = 0_u64;
67        for fragment in fragments {
68            let fragment = fragment.as_object().ok_or_else(|| {
69                ProgramRepresentationError::InvalidSnapshot(
70                    "fragment must be an object".to_string(),
71                )
72            })?;
73            let byte_start = fragment.get("byteStart").and_then(Value::as_u64);
74            let byte_end = fragment.get("byteEnd").and_then(Value::as_u64);
75            let text = fragment.get("text").and_then(Value::as_str);
76            let (Some(byte_start), Some(byte_end), Some(text)) = (byte_start, byte_end, text)
77            else {
78                return Err(ProgramRepresentationError::InvalidSnapshot(
79                    "fragment fields have invalid types".to_string(),
80                ));
81            };
82            let text_length = u64::try_from(text.len()).map_err(|_| {
83                ProgramRepresentationError::InvalidSnapshot(
84                    "fragment text length does not fit u64".to_string(),
85                )
86            })?;
87            if byte_start != expected_start
88                || byte_end < byte_start
89                || text_length != byte_end - byte_start
90            {
91                return Err(ProgramRepresentationError::InvalidSnapshot(format!(
92                    "invalid fragment span at byte {expected_start}"
93                )));
94            }
95            source.push_str(text);
96            expected_start = byte_end;
97        }
98        construct_program(&source, language, project)
99    }
100}
101
102/// Constructs a clean program from ordered structured source fragments.
103pub fn construct_program_from_fragments<S: AsRef<str>>(
104    fragments: &[S],
105    language: &str,
106    project: ProgramProjectContext,
107) -> Result<ProgramRepresentation, ProgramRepresentationError> {
108    let source = fragments
109        .iter()
110        .fold(String::new(), |mut source, fragment| {
111            source.push_str(fragment.as_ref());
112            source
113        });
114    construct_program(&source, language, project)
115}
116
117fn retained_fragments(program: &ProgramRepresentation) -> Vec<Value> {
118    let mut tokens = program
119        .network
120        .links()
121        .filter(|link| link.metadata().link_type() == Some(LinkType::Token))
122        .filter(|link| !link.metadata().flags().is_missing())
123        .filter_map(|link| {
124            let range = link.metadata().span()?.byte_range();
125            Some((
126                range.start(),
127                link.id().as_u64(),
128                range.end(),
129                link.metadata().term()?.to_string(),
130            ))
131        })
132        .collect::<Vec<_>>();
133    tokens.sort_by_key(|(start, id, _end, _text)| (*start, *id));
134    let mut covered_until = 0;
135    let mut fragments = Vec::new();
136    for (start, _id, end, text) in tokens {
137        if start < covered_until {
138            continue;
139        }
140        debug_assert_eq!(
141            start, covered_until,
142            "source-token spans must be contiguous"
143        );
144        debug_assert_eq!(text.len(), end - start, "source-token span must match text");
145        fragments.push(json!({ "byteStart": start, "byteEnd": end, "text": text }));
146        covered_until = end;
147    }
148    debug_assert_eq!(
149        fragments
150            .iter()
151            .filter_map(|fragment| fragment["text"].as_str())
152            .collect::<String>(),
153        program.source
154    );
155    fragments
156}
157
158fn read_project(
159    value: Option<&Value>,
160) -> Result<ProgramProjectContext, ProgramRepresentationError> {
161    let project = value.and_then(Value::as_object).ok_or_else(|| {
162        ProgramRepresentationError::InvalidSnapshot("project must be an object".to_string())
163    })?;
164    let context = ProgramProjectContext::new(
165        required_string(project.get("root"), "project.root")?,
166        string_array(project.get("files"), "project.files")?,
167        string_array(project.get("dependencies"), "project.dependencies")?,
168    )
169    .with_extensions(string_array(
170        project.get("extensions"),
171        "project.extensions",
172    )?);
173    if project.get("entry").is_none() && project.get("sources").is_none() {
174        return Ok(context);
175    }
176    let entry = required_string(project.get("entry"), "project.entry")?;
177    let sources = project
178        .get("sources")
179        .and_then(Value::as_array)
180        .ok_or_else(|| {
181            ProgramRepresentationError::InvalidSnapshot(
182                "project.sources must be an array".to_string(),
183            )
184        })?
185        .iter()
186        .map(|file| {
187            match (
188                file.get("path").and_then(Value::as_str),
189                file.get("source").and_then(Value::as_str),
190            ) {
191                (Some(path), Some(source)) => Ok(ProgramProjectSource::new(path, source)),
192                _ => Err(ProgramRepresentationError::InvalidSnapshot(
193                    "project.sources entries must have a path and a source".to_string(),
194                )),
195            }
196        })
197        .collect::<Result<Vec<_>, _>>()?;
198    Ok(context.with_entry(entry, sources))
199}
200
201fn required_string<'a>(
202    value: Option<&'a Value>,
203    name: &str,
204) -> Result<&'a str, ProgramRepresentationError> {
205    value.and_then(Value::as_str).ok_or_else(|| {
206        ProgramRepresentationError::InvalidSnapshot(format!("{name} must be a string"))
207    })
208}
209
210fn string_array(
211    value: Option<&Value>,
212    name: &str,
213) -> Result<Vec<String>, ProgramRepresentationError> {
214    value
215        .and_then(Value::as_array)
216        .ok_or_else(|| {
217            ProgramRepresentationError::InvalidSnapshot(format!("{name} must be an array"))
218        })?
219        .iter()
220        .map(|entry| {
221            entry.as_str().map(ToString::to_string).ok_or_else(|| {
222                ProgramRepresentationError::InvalidSnapshot(format!(
223                    "{name} entries must be strings"
224                ))
225            })
226        })
227        .collect()
228}