Skip to main content

nichlink/registry_core/mir/
jsonl.rs

1//! JSONL MIR artifact parser.
2//! JSONL MIR artifact 解析器。
3//!
4//! The compact artifact is hand-parsed so the kernel keeps zero JSON
5//! dependency: every line is one object with a `kind` discriminator.
6//! 紧凑 artifact 采用手写解析,kernel 因此保持零 JSON 依赖:每行是一个带 `kind`
7//! 判别字段的对象。
8
9use std::collections::HashMap;
10
11use super::model::{MirCall, MirGraph, MirLocal, MirParseError};
12
13impl MirGraph {
14    /// Parse the compact JSONL artifact, one record per non-blank line.
15    /// 解析紧凑 JSONL artifact,每个非空行一条记录。
16    ///
17    /// A malformed line fails the whole parse with its 1-based line number;
18    /// blank lines are skipped. Each record needs a `kind` of `function`,
19    /// `call`, or `local`, and any other kind is rejected.
20    /// 任一行格式错误都会以使整个解析失败,并带上其以 1 起始的行号;空行跳过。
21    /// 每条记录需要 `kind` 为 `function`、`call` 或 `local`,其他类型一律拒绝。
22    pub fn from_jsonl(input: &str) -> Result<Self, MirParseError> {
23        let mut graph = Self::default();
24        for (index, raw) in input.lines().enumerate() {
25            let line = index + 1;
26            if raw.trim().is_empty() {
27                continue;
28            }
29            let fields = parse_object(raw).map_err(|message| MirParseError { line, message })?;
30            let kind = required(&fields, "kind", line)?;
31            match kind {
32                "function" => {
33                    graph
34                        .functions
35                        .insert(required(&fields, "name", line)?.to_owned());
36                }
37                "call" => graph.calls.push(MirCall {
38                    caller: required(&fields, "caller", line)?.to_owned(),
39                    callee: required(&fields, "callee", line)?.to_owned(),
40                    mir_line: number(&fields, "mir_line", line)?,
41                }),
42                "local" => graph.locals.push(MirLocal {
43                    function: required(&fields, "function", line)?.to_owned(),
44                    name: required(&fields, "name", line)?.to_owned(),
45                    type_name: required(&fields, "type", line)?.to_owned(),
46                    mir_line: number(&fields, "mir_line", line)?,
47                }),
48                other => {
49                    return Err(MirParseError {
50                        line,
51                        message: format!("unsupported record kind `{other}`"),
52                    });
53                }
54            }
55        }
56        Ok(graph)
57    }
58}
59
60fn required<'a>(
61    fields: &'a HashMap<String, String>,
62    key: &str,
63    line: usize,
64) -> Result<&'a str, MirParseError> {
65    fields
66        .get(key)
67        .map(String::as_str)
68        .ok_or_else(|| MirParseError {
69            line,
70            message: format!("missing `{key}`"),
71        })
72}
73
74fn number(
75    fields: &HashMap<String, String>,
76    key: &str,
77    line: usize,
78) -> Result<usize, MirParseError> {
79    required(fields, key, line)?
80        .parse()
81        .map_err(|_| MirParseError {
82            line,
83            message: format!("`{key}` is not an integer"),
84        })
85}
86
87fn parse_object(input: &str) -> Result<HashMap<String, String>, String> {
88    let bytes = input.as_bytes();
89    let mut cursor = 0;
90    skip_space(bytes, &mut cursor);
91    if bytes.get(cursor) != Some(&b'{') {
92        return Err("record must start with `{`".to_owned());
93    }
94    cursor += 1;
95    let mut fields = HashMap::new();
96    loop {
97        skip_space(bytes, &mut cursor);
98        if bytes.get(cursor) == Some(&b'}') {
99            return Ok(fields);
100        }
101        let key = quoted(bytes, &mut cursor)?;
102        skip_space(bytes, &mut cursor);
103        if bytes.get(cursor) != Some(&b':') {
104            return Err("expected `:` after key".to_owned());
105        }
106        cursor += 1;
107        skip_space(bytes, &mut cursor);
108        let value = if bytes.get(cursor) == Some(&b'"') {
109            quoted(bytes, &mut cursor)?
110        } else {
111            let start = cursor;
112            while cursor < bytes.len()
113                && !matches!(bytes[cursor], b',' | b'}' | b' ' | b'\n' | b'\r' | b'\t')
114            {
115                cursor += 1;
116            }
117            if start == cursor {
118                return Err("expected a value".to_owned());
119            }
120            String::from_utf8(bytes[start..cursor].to_vec())
121                .map_err(|_| "value is not UTF-8".to_owned())?
122        };
123        if fields.insert(key, value).is_some() {
124            return Err("duplicate field".to_owned());
125        }
126        skip_space(bytes, &mut cursor);
127        match bytes.get(cursor) {
128            Some(b',') => cursor += 1,
129            Some(b'}') => return Ok(fields),
130            _ => return Err("expected `,` or `}`".to_owned()),
131        }
132    }
133}
134
135fn quoted(bytes: &[u8], cursor: &mut usize) -> Result<String, String> {
136    if bytes.get(*cursor) != Some(&b'"') {
137        return Err("expected a quoted string".to_owned());
138    }
139    *cursor += 1;
140    let mut value = String::new();
141    while let Some(byte) = bytes.get(*cursor).copied() {
142        *cursor += 1;
143        match byte {
144            b'"' => return Ok(value),
145            b'\\' => {
146                let escaped = bytes.get(*cursor).copied().ok_or("unterminated escape")?;
147                *cursor += 1;
148                value.push(match escaped {
149                    b'"' => '"',
150                    b'\\' => '\\',
151                    b'n' => '\n',
152                    b'r' => '\r',
153                    b't' => '\t',
154                    _ => return Err("unsupported string escape".to_owned()),
155                });
156            }
157            byte if byte.is_ascii() => value.push(byte as char),
158            _ => return Err("non-ASCII strings must be JSON escaped".to_owned()),
159        }
160    }
161    Err("unterminated string".to_owned())
162}
163
164fn skip_space(bytes: &[u8], cursor: &mut usize) {
165    while bytes
166        .get(*cursor)
167        .is_some_and(|byte| byte.is_ascii_whitespace())
168    {
169        *cursor += 1;
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::MirGraph;
176
177    #[test]
178    fn parses_jsonl_without_json_dependency() {
179        let graph = MirGraph::from_jsonl(
180            "{\"kind\":\"function\",\"name\":\"crate::a\"}\n{\"kind\":\"call\",\"caller\":\"crate::a\",\"callee\":\"crate::b\",\"mir_line\":12}\n{\"kind\":\"local\",\"function\":\"crate::a\",\"name\":\"_1\",\"type\":\"f32\",\"mir_line\":14}\n",
181        )
182        .unwrap();
183        assert!(graph.functions.contains("crate::a"));
184        assert_eq!(graph.calls[0].mir_line, 12);
185        assert_eq!(graph.locals[0].type_name, "f32");
186    }
187}