nichlink/registry_core/mir/
jsonl.rs1use std::collections::HashMap;
10
11use super::model::{MirCall, MirGraph, MirLocal, MirParseError};
12
13impl MirGraph {
14 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}