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                match escaped {
149                    b'"' => value.push('"'),
150                    b'\\' => value.push('\\'),
151                    b'/' => value.push('/'),
152                    b'b' => value.push('\u{8}'),
153                    b'f' => value.push('\u{c}'),
154                    b'n' => value.push('\n'),
155                    b'r' => value.push('\r'),
156                    b't' => value.push('\t'),
157                    b'u' => value.push(decode_unicode_escape(bytes, cursor)?),
158                    _ => return Err("unsupported string escape".to_owned()),
159                }
160            }
161            byte if byte.is_ascii() => value.push(byte as char),
162            _ => {
163                // Raw UTF-8 is valid JSON, and it is exactly what this crate's own writer
164                // emits: rejecting every byte at or above 0x80 made a non-ASCII symbol
165                // (`crate::héllo`, and Rust identifiers may be non-ASCII) impossible to
166                // read back in any encoding — the error told the producer to escape the
167                // string while `\u` was refused too.
168                // 原始 UTF-8 是合法的 JSON,而且正是本 crate 自己的写入器产出的形式:拒绝所有
169                // ≥ 0x80 的字节让非 ASCII 符号(`crate::héllo`,而 Rust 标识符可以是非 ASCII)
170                // 在任何编码下都读不回来——那条错误让生产者去转义,而 `\u` 同样被拒。
171                let start = *cursor - 1;
172                let width = utf8_width(byte);
173                let end = start + width;
174                if width == 0 || end > bytes.len() {
175                    return Err("invalid UTF-8 in string".to_owned());
176                }
177                let text = core::str::from_utf8(&bytes[start..end])
178                    .map_err(|_| "invalid UTF-8 in string".to_owned())?;
179                value.push_str(text);
180                *cursor = end;
181            }
182        }
183    }
184    Err("unterminated string".to_owned())
185}
186
187/// How many bytes the UTF-8 sequence that starts with `byte` occupies, or zero when it
188/// cannot start one.
189/// 以 `byte` 开头的 UTF-8 序列占几个字节;它不能作为起始时为零。
190fn utf8_width(byte: u8) -> usize {
191    match byte {
192        0xC2..=0xDF => 2,
193        0xE0..=0xEF => 3,
194        0xF0..=0xF4 => 4,
195        _ => 0,
196    }
197}
198
199/// Decode a `\uXXXX` escape, pairing a surrogate when JSON wrote one.
200/// 解码 `\uXXXX` 转义;JSON 写成代理对时把一对合起来解。
201fn decode_unicode_escape(bytes: &[u8], cursor: &mut usize) -> Result<char, String> {
202    let code = hex4(bytes, cursor)?;
203    if (0xD800..0xDC00).contains(&code) {
204        // A high surrogate must be followed by a low one: JSON writes an astral code
205        // point as two escapes, and neither half is a `char` on its own.
206        // 高代理后面必须跟一个低代理:JSON 把星平面码点写成两个转义,而任何一半单独都不是 `char`。
207        if bytes.get(*cursor..*cursor + 2) != Some(b"\\u") {
208            return Err("a high surrogate needs a low surrogate".to_owned());
209        }
210        *cursor += 2;
211        let low = hex4(bytes, cursor)?;
212        if !(0xDC00..0xE000).contains(&low) {
213            return Err("a high surrogate needs a low surrogate".to_owned());
214        }
215        let combined = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
216        return char::from_u32(combined).ok_or_else(|| "invalid \\u escape".to_owned());
217    }
218    char::from_u32(code).ok_or_else(|| "invalid \\u escape".to_owned())
219}
220
221/// Read four hex digits at `cursor`.
222/// 在 `cursor` 处读四位十六进制。
223fn hex4(bytes: &[u8], cursor: &mut usize) -> Result<u32, String> {
224    let digits = bytes
225        .get(*cursor..*cursor + 4)
226        .ok_or_else(|| "truncated \\u escape".to_owned())?;
227    let text = core::str::from_utf8(digits).map_err(|_| "invalid \\u escape".to_owned())?;
228    let code = u32::from_str_radix(text, 16).map_err(|_| "invalid \\u escape".to_owned())?;
229    *cursor += 4;
230    Ok(code)
231}
232
233/// Skip the whitespace between two JSON tokens.
234/// 跳过两个 JSON token 之间的空白。
235fn skip_space(bytes: &[u8], cursor: &mut usize) {
236    while bytes
237        .get(*cursor)
238        .is_some_and(|byte| byte.is_ascii_whitespace())
239    {
240        *cursor += 1;
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::MirGraph;
247
248    #[test]
249    fn parses_jsonl_without_json_dependency() {
250        let graph = MirGraph::from_jsonl(
251            "{\"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",
252        )
253        .unwrap();
254        assert!(graph.functions.contains("crate::a"));
255        assert_eq!(graph.calls[0].mir_line, 12);
256        assert_eq!(graph.locals[0].type_name, "f32");
257    }
258    /// A non-ASCII symbol round-trips, and an escaped form decodes.
259    /// 非 ASCII 符号能往返,转义形式也能解码。
260    ///
261    /// The writer emits raw UTF-8; the reader used to reject every byte at or above
262    /// 0x80 *and* every `\u` escape, so no encoding of `crate::héllo` could be read
263    /// back — while the refusal message told the producer to escape the string.
264    /// 写入器产出原始 UTF-8;读取器过去既拒绝所有 ≥ 0x80 的字节,也拒绝每一个 `\u` 转义,
265    /// 因此 `crate::héllo` 在任何编码下都读不回来——而拒绝信息还在让生产者去转义。
266    #[test]
267    fn a_non_ascii_symbol_round_trips() {
268        let line = "{\"kind\":\"function\",\"name\":\"crate::héllo\"}\n";
269        let graph = MirGraph::from_jsonl(line).expect("raw UTF-8 is valid JSON");
270        assert!(
271            graph.functions.iter().any(|name| name == "crate::héllo"),
272            "the raw form reads back: {:?}",
273            graph.functions
274        );
275        let again = MirGraph::from_jsonl(&graph.to_jsonl()).expect("the writer's own output");
276        assert!(
277            again.functions.iter().any(|name| name == "crate::héllo"),
278            "the artifact round-trips: {:?}",
279            again.functions
280        );
281
282        // An escaped form is accepted too, including an astral code point written as a
283        // surrogate pair.
284        // 转义形式同样被接受,包括写成代理对的星平面码点。
285        let escaped = MirGraph::from_jsonl(
286            "{\"kind\":\"function\",\"name\":\"caf\\u00e9 \\ud83d\\ude00\"}\n",
287        )
288        .expect("escapes are accepted");
289        assert!(
290            escaped.functions.iter().any(|name| name == "café 😀"),
291            "escapes decode: {:?}",
292            escaped.functions
293        );
294    }
295}