Skip to main content

meta_language/
lino_serialization.rs

1//! Lossless serialization of a [`LinkNetwork`] to and from links-notation text.
2//!
3//! [`LinkNetwork::to_lino`] projects every link in the network onto a single
4//! canonical links-notation statement, keyed by the link's numeric identifier
5//! (a doublets-style id discipline shared by text and future binary storage).
6//! [`LinkNetwork::from_lino`] reconstructs the exact same network from that
7//! text. The pair forms a round-trip: `from_lino(to_lino(n))` is isomorphic to
8//! `n` for any network, covering references, names, types, terms, definitions,
9//! languages, source spans, parse flags, and term registration.
10//!
11//! The emitted dialect is plain links-notation accepted by the
12//! [`links_notation`] 0.22 crate, so other ecosystem parsers can consume the
13//! output. Each statement has the shape:
14//!
15//! ```text
16//! (<id>: <ref> ... (meta: (t: <type>) (n: <0|1>) (term: <pct>) ...))
17//! ```
18//!
19//! where references are decimal link ids and the trailing `meta` sublink
20//! carries metadata. String payloads (`term`, `def`, `lang`) are
21//! percent-encoded so they always form a single escape-free reference token,
22//! sidestepping the crate's quote-escaping edge cases. The `meta` keys are
23//! non-numeric, so they never collide with numeric reference ids, and the
24//! references (`Ref` nodes) are structurally distinct from the `meta` sublink
25//! (a `Link` node) in the parsed AST.
26//!
27//! This is distinct from [`LinkNetwork::parse`] with the `"LiNo"` language,
28//! which interprets human-authored links-notation into a fresh semantic
29//! network. `to_lino`/`from_lino` are an exact serialization pair for an
30//! already-built network.
31
32use std::error::Error;
33use std::fmt;
34use std::fmt::Write as _;
35use std::sync::Arc;
36
37use links_notation::{LiNo, ParserConfig, parse_lino_to_links_with_config};
38
39use crate::link_flags::LinkFlags;
40use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
41use crate::source::{ByteRange, Point, SourceSpan};
42
43/// Error returned when [`LinkNetwork::from_lino`] cannot reconstruct a network.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum LinoSerializationError {
46    /// The text could not be parsed as links-notation.
47    Parse(String),
48    /// The text parsed but did not match the serialization schema.
49    Structure(String),
50}
51
52impl fmt::Display for LinoSerializationError {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Parse(message) => write!(formatter, "links-notation parse error: {message}"),
56            Self::Structure(message) => {
57                write!(formatter, "serialization structure error: {message}")
58            }
59        }
60    }
61}
62
63impl Error for LinoSerializationError {}
64
65impl LinkNetwork {
66    /// Serializes the entire network to canonical links-notation text.
67    ///
68    /// Every link becomes one statement keyed by its numeric id. The output is
69    /// accepted by the [`links_notation`] crate parser and round-trips back
70    /// through [`LinkNetwork::from_lino`].
71    #[must_use]
72    pub fn to_lino(&self) -> String {
73        let registered: std::collections::BTreeSet<u64> =
74            self.terms.values().map(|id| id.0).collect();
75        let mut output = String::new();
76        for link in self.links.values() {
77            encode_link(link, registered.contains(&link.id.0), &mut output);
78            output.push('\n');
79        }
80        output
81    }
82
83    /// Reconstructs a network from text produced by [`LinkNetwork::to_lino`].
84    ///
85    /// # Errors
86    ///
87    /// Returns [`LinoSerializationError`] when the text is not valid
88    /// links-notation or does not match the serialization schema.
89    pub fn from_lino(text: &str) -> Result<Self, LinoSerializationError> {
90        let statements = parse_lino_to_links_with_config(text, &ParserConfig::without_comments())
91            .map_err(|error| LinoSerializationError::Parse(error.to_string()))?;
92        let mut network = Self::new();
93        for statement in &statements {
94            let LiNo::Link {
95                id: Some(id),
96                values,
97            } = statement
98            else {
99                return Err(LinoSerializationError::Structure(
100                    "top-level statement must be an identified link".to_string(),
101                ));
102            };
103            let link_id = LinkId(parse_u64(id)?);
104            let mut references = Vec::new();
105            let mut meta_values: Option<&Vec<LiNo<String>>> = None;
106            for value in values {
107                match value {
108                    LiNo::Ref(reference) => references.push(LinkId(parse_u64(reference)?)),
109                    LiNo::Link {
110                        id: Some(key),
111                        values: fields,
112                    } if key == "meta" => meta_values = Some(fields),
113                    LiNo::Link { .. } => {
114                        return Err(LinoSerializationError::Structure(
115                            "statement values must be references or a meta sublink".to_string(),
116                        ));
117                    }
118                }
119            }
120            let meta_values = meta_values.ok_or_else(|| {
121                LinoSerializationError::Structure(
122                    "statement is missing its meta sublink".to_string(),
123                )
124            })?;
125            let (metadata, registered) = decode_meta(meta_values)?;
126            if registered && let Some(term) = metadata.term() {
127                network.terms.insert(Arc::from(term), link_id);
128            }
129            network.next_id = network.next_id.max(link_id.0 + 1);
130            network.links.insert(
131                link_id,
132                Arc::new(Link {
133                    id: link_id,
134                    references: Arc::from(references),
135                    metadata,
136                }),
137            );
138        }
139        Ok(network)
140    }
141}
142
143/// Writes one `(<id>: <refs> (meta: ...))` statement into `output`.
144fn encode_link(link: &Link, registered: bool, output: &mut String) {
145    write!(output, "({}:", link.id.0).expect("writing to a String never fails");
146    for reference in link.references.iter() {
147        write!(output, " {}", reference.0).expect("writing to a String never fails");
148    }
149    output.push_str(" (meta:");
150    let metadata = &link.metadata;
151    if let Some(link_type) = metadata.link_type() {
152        write!(output, " (t: {link_type})").expect("writing to a String never fails");
153    }
154    write!(output, " (n: {})", u8::from(metadata.is_named()))
155        .expect("writing to a String never fails");
156    if let Some(term) = metadata.term() {
157        write!(output, " (term: {})", percent_encode(term))
158            .expect("writing to a String never fails");
159    }
160    if let Some(definition) = metadata.definition() {
161        write!(output, " (def: {})", percent_encode(definition))
162            .expect("writing to a String never fails");
163    }
164    if let Some(language) = metadata.language() {
165        write!(output, " (lang: {})", percent_encode(language))
166            .expect("writing to a String never fails");
167    }
168    if let Some(span) = metadata.span() {
169        let byte_range = span.byte_range();
170        let start = span.start_point();
171        let end = span.end_point();
172        write!(
173            output,
174            " (span: {} {} {} {} {} {})",
175            byte_range.start(),
176            byte_range.end(),
177            start.row(),
178            start.column(),
179            end.row(),
180            end.column(),
181        )
182        .expect("writing to a String never fails");
183    }
184    let bits = flag_bits(metadata.flags());
185    if bits != 0 {
186        write!(output, " (flags: {bits})").expect("writing to a String never fails");
187    }
188    if registered {
189        output.push_str(" (reg: 1)");
190    }
191    output.push_str("))");
192}
193
194/// Decodes a `meta` sublink's fields into metadata and a registration flag.
195fn decode_meta(fields: &[LiNo<String>]) -> Result<(LinkMetadata, bool), LinoSerializationError> {
196    let mut metadata = LinkMetadata::new();
197    let mut registered = false;
198    let mut flag_bits = 0u8;
199    for field in fields {
200        let LiNo::Link {
201            id: Some(key),
202            values,
203        } = field
204        else {
205            return Err(LinoSerializationError::Structure(
206                "meta field must be an identified link".to_string(),
207            ));
208        };
209        match key.as_str() {
210            "t" => metadata = metadata.with_link_type(parse_link_type(single_ref(values)?)?),
211            "n" => metadata = metadata.with_named(single_ref(values)? == "1"),
212            "term" => metadata = metadata.with_term(percent_decode(single_ref(values)?)?),
213            "def" => metadata = metadata.with_definition(percent_decode(single_ref(values)?)?),
214            "lang" => metadata = metadata.with_language(percent_decode(single_ref(values)?)?),
215            "span" => metadata = metadata.with_span(parse_span(values)?),
216            "flags" => flag_bits = parse_u8(single_ref(values)?)?,
217            "reg" => registered = true,
218            other => {
219                return Err(LinoSerializationError::Structure(format!(
220                    "unknown meta field `{other}`"
221                )));
222            }
223        }
224    }
225    if flag_bits != 0 {
226        let mut flags = LinkFlags::clean();
227        if flag_bits & 0b0001 != 0 {
228            flags = flags.with_error();
229        }
230        if flag_bits & 0b0010 != 0 {
231            flags = flags.with_containing_error();
232        }
233        if flag_bits & 0b0100 != 0 {
234            flags = flags.with_missing();
235        }
236        if flag_bits & 0b1000 != 0 {
237            flags = flags.with_extra();
238        }
239        metadata = metadata.with_flags(flags);
240    }
241    Ok((metadata, registered))
242}
243
244/// Packs the four parse-status bits into a single byte.
245fn flag_bits(flags: LinkFlags) -> u8 {
246    u8::from(flags.is_error())
247        | (u8::from(flags.has_error()) << 1)
248        | (u8::from(flags.is_missing()) << 2)
249        | (u8::from(flags.is_extra()) << 3)
250}
251
252/// Builds a [`SourceSpan`] from the six decimal values of a `span` field.
253fn parse_span(values: &[LiNo<String>]) -> Result<SourceSpan, LinoSerializationError> {
254    if values.len() != 6 {
255        return Err(LinoSerializationError::Structure(
256            "span field requires six numbers".to_string(),
257        ));
258    }
259    let mut numbers = [0usize; 6];
260    for (slot, value) in numbers.iter_mut().zip(values) {
261        let LiNo::Ref(reference) = value else {
262            return Err(LinoSerializationError::Structure(
263                "span field values must be numbers".to_string(),
264            ));
265        };
266        *slot = reference.parse().map_err(|_| {
267            LinoSerializationError::Structure(format!("invalid span number `{reference}`"))
268        })?;
269    }
270    Ok(SourceSpan::new(
271        ByteRange::new(numbers[0], numbers[1]),
272        Point::new(numbers[2], numbers[3]),
273        Point::new(numbers[4], numbers[5]),
274    ))
275}
276
277/// Returns the single reference held by a one-value meta field.
278fn single_ref(values: &[LiNo<String>]) -> Result<&str, LinoSerializationError> {
279    match values {
280        [LiNo::Ref(reference)] => Ok(reference),
281        _ => Err(LinoSerializationError::Structure(
282            "meta field must hold exactly one reference".to_string(),
283        )),
284    }
285}
286
287/// Maps a link-type token back to its [`LinkType`] variant.
288fn parse_link_type(token: &str) -> Result<LinkType, LinoSerializationError> {
289    Ok(match token {
290        "link" => LinkType::Link,
291        "reference" => LinkType::Reference,
292        "relation" => LinkType::Relation,
293        "language" => LinkType::Language,
294        "grammar" => LinkType::Grammar,
295        "type" => LinkType::Type,
296        "concept" => LinkType::Concept,
297        "syntax" => LinkType::Syntax,
298        "field" => LinkType::Field,
299        "trivia" => LinkType::Trivia,
300        "token" => LinkType::Token,
301        "document" => LinkType::Document,
302        "semantic" => LinkType::Semantic,
303        "region" => LinkType::Region,
304        "object" => LinkType::Object,
305        other => {
306            return Err(LinoSerializationError::Structure(format!(
307                "unknown link type `{other}`"
308            )));
309        }
310    })
311}
312
313fn parse_u64(value: &str) -> Result<u64, LinoSerializationError> {
314    value
315        .parse()
316        .map_err(|_| LinoSerializationError::Structure(format!("invalid link id `{value}`")))
317}
318
319fn parse_u8(value: &str) -> Result<u8, LinoSerializationError> {
320    value
321        .parse()
322        .map_err(|_| LinoSerializationError::Structure(format!("invalid flags value `{value}`")))
323}
324
325/// Percent-encodes a string into a single escape-free links-notation token.
326///
327/// The empty string maps to the sentinel `%`, which never results from
328/// encoding a non-empty string (a literal `%` byte becomes `%25`).
329fn percent_encode(value: &str) -> String {
330    if value.is_empty() {
331        return "%".to_string();
332    }
333    let mut encoded = String::with_capacity(value.len());
334    for &byte in value.as_bytes() {
335        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.') {
336            encoded.push(byte as char);
337        } else {
338            write!(encoded, "%{byte:02X}").expect("writing to a String never fails");
339        }
340    }
341    encoded
342}
343
344/// Reverses [`percent_encode`].
345fn percent_decode(value: &str) -> Result<String, LinoSerializationError> {
346    if value == "%" {
347        return Ok(String::new());
348    }
349    let bytes = value.as_bytes();
350    let mut decoded = Vec::with_capacity(bytes.len());
351    let mut index = 0;
352    while index < bytes.len() {
353        if bytes[index] == b'%' {
354            if index + 2 >= bytes.len() {
355                return Err(LinoSerializationError::Structure(
356                    "truncated percent escape".to_string(),
357                ));
358            }
359            let high = hex_value(bytes[index + 1])?;
360            let low = hex_value(bytes[index + 2])?;
361            decoded.push((high << 4) | low);
362            index += 3;
363        } else {
364            decoded.push(bytes[index]);
365            index += 1;
366        }
367    }
368    String::from_utf8(decoded).map_err(|_| {
369        LinoSerializationError::Structure("percent escape is not valid UTF-8".to_string())
370    })
371}
372
373fn hex_value(byte: u8) -> Result<u8, LinoSerializationError> {
374    match byte {
375        b'0'..=b'9' => Ok(byte - b'0'),
376        b'a'..=b'f' => Ok(byte - b'a' + 10),
377        b'A'..=b'F' => Ok(byte - b'A' + 10),
378        _ => Err(LinoSerializationError::Structure(
379            "invalid percent escape digit".to_string(),
380        )),
381    }
382}