Skip to main content

onnx_std/text/
de.rs

1//! Parser for the textual representation emitted by [`super::ser`].
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use onnx_runtime_ir::{
7    Attribute, DataType, Dim, Graph, Node, NodeId, Shape, SparseTensorData, TensorData, TypeProto,
8    ValueId, WeightRef,
9};
10use onnx_runtime_loader::ModelMetadata;
11
12use crate::error::{Error, Result};
13use crate::model::Model;
14
15#[derive(Clone, Copy)]
16struct Line<'a> {
17    number: usize,
18    text: &'a str,
19}
20
21struct Parser<'a> {
22    lines: Vec<Line<'a>>,
23    pos: usize,
24    mark_opaque_placeholders: bool,
25}
26
27/// Parse a model rendered by [`crate::text::to_text`].
28///
29/// Binary initializer and tensor-attribute payloads are not present in the
30/// textual format. They are reconstructed as zero-filled placeholders with the
31/// printed dtype and shape; external initializers retain their external kind
32/// but necessarily use an empty path and zero offset.
33pub fn from_text(source: &str) -> Result<Model> {
34    let (source, extension) = super::extensions::split(source)?;
35    let model = Parser::new(source, extension.is_some()).parse()?;
36    let Some(extension) = extension else {
37        return Ok(model);
38    };
39    let native = model.to_proto()?;
40    Model::from_proto(super::extensions::merge(extension, native))
41}
42
43impl<'a> Parser<'a> {
44    fn new(source: &'a str, mark_opaque_placeholders: bool) -> Self {
45        let lines = source
46            .lines()
47            .enumerate()
48            .filter_map(|(index, text)| {
49                let text = text.trim();
50                (!text.is_empty()).then_some(Line {
51                    number: index + 1,
52                    text,
53                })
54            })
55            .collect();
56        Self {
57            lines,
58            pos: 0,
59            mark_opaque_placeholders,
60        }
61    }
62
63    fn parse(mut self) -> Result<Model> {
64        let open = self.next()?;
65        if open.text != "<" {
66            return self.fail(open.number, "expected model header '<'");
67        }
68
69        let mut metadata = ModelMetadata::default();
70        let mut imports = HashMap::new();
71        loop {
72            let line = self.next()?;
73            if line.text == ">" {
74                break;
75            }
76            if let Some(value) = line.text.strip_prefix("ir_version:") {
77                metadata.ir_version = trim_comma(value)
78                    .parse()
79                    .map_err(|_| self.error(line.number, "invalid ir_version"))?;
80            } else if let Some(value) = line.text.strip_prefix("opset_import:") {
81                imports = parse_opset_imports(value.trim(), line.number)?;
82            } else {
83                return self.fail(line.number, "unknown model header field");
84            }
85        }
86
87        let (graph_name, mut graph) = self.parse_graph()?;
88        metadata.graph_name = graph_name;
89        graph.opset_imports = imports;
90        if self.pos != self.lines.len() {
91            return self.fail(
92                self.lines[self.pos].number,
93                "unexpected content after graph",
94            );
95        }
96        Ok(Model::with_metadata(graph, metadata))
97    }
98
99    fn parse_graph(&mut self) -> Result<(String, Graph)> {
100        let signature = self.next()?;
101        if !signature.text.ends_with('{') {
102            let open = self.next()?;
103            if open.text != "{" {
104                return self.fail(open.number, "expected graph body '{'");
105            }
106        }
107        let (name, inputs, outputs) = parse_signature(signature)?;
108        let mut graph = Graph::new();
109        let mut values = HashMap::new();
110
111        for typed in inputs {
112            let id = create_typed_value(&mut graph, &typed, signature.number)?;
113            graph.add_input(id);
114            values.insert(typed.name, id);
115        }
116        for typed in outputs {
117            let id = match values.get(&typed.name) {
118                Some(id) => *id,
119                None => {
120                    let id = create_typed_value(&mut graph, &typed, signature.number)?;
121                    values.insert(typed.name, id);
122                    id
123                }
124            };
125            graph.add_output(id);
126        }
127
128        let mut last_node = None;
129        loop {
130            let line = self.peek()?;
131            if line.text == "}" {
132                self.pos += 1;
133                break;
134            }
135            if line.text == "// initializers" {
136                self.pos += 1;
137                continue;
138            }
139            if let Some(init) = line.text.strip_prefix("// ")
140                && init.contains(" data omitted>")
141            {
142                self.pos += 1;
143                parse_initializer(init, line.number, &mut graph, &mut values)?;
144                continue;
145            }
146            if let Some((attr_name, index)) = parse_subgraph_marker(line.text) {
147                self.pos += 1;
148                let (_, subgraph) = self.parse_graph()?;
149                let node_id = last_node.ok_or_else(|| {
150                    self.error(line.number, "subgraph attribute has no preceding node")
151                })?;
152                attach_subgraph(&mut graph, node_id, attr_name, index, subgraph);
153                continue;
154            }
155
156            self.pos += 1;
157            last_node = Some(parse_node(
158                line,
159                &mut graph,
160                &mut values,
161                self.mark_opaque_placeholders,
162            )?);
163        }
164
165        Ok((name, graph))
166    }
167
168    fn next(&mut self) -> Result<Line<'a>> {
169        let line = self
170            .lines
171            .get(self.pos)
172            .copied()
173            .ok_or_else(|| self.error(self.last_line(), "unexpected end of input"))?;
174        self.pos += 1;
175        Ok(line)
176    }
177
178    fn peek(&self) -> Result<Line<'a>> {
179        self.lines
180            .get(self.pos)
181            .copied()
182            .ok_or_else(|| self.error(self.last_line(), "unexpected end of graph"))
183    }
184
185    fn last_line(&self) -> usize {
186        self.lines.last().map_or(1, |line| line.number)
187    }
188
189    fn error(&self, line: usize, message: impl Into<String>) -> Error {
190        Error::TextParse {
191            line,
192            message: message.into(),
193        }
194    }
195
196    fn fail<T>(&self, line: usize, message: impl Into<String>) -> Result<T> {
197        Err(self.error(line, message))
198    }
199}
200
201#[derive(Debug)]
202struct TypedValue {
203    name: String,
204    dtype: DataType,
205    dims: Vec<String>,
206}
207
208fn parse_signature(line: Line<'_>) -> Result<(String, Vec<TypedValue>, Vec<TypedValue>)> {
209    let arrow = line
210        .text
211        .find("=>")
212        .ok_or_else(|| parse_error(line.number, "graph signature is missing '=>'"))?;
213    let left = line.text[..arrow].trim();
214    let right = line.text[arrow + 2..].trim();
215
216    let left_open = left
217        .find('(')
218        .ok_or_else(|| parse_error(line.number, "graph inputs are missing"))?;
219    let left_close = left
220        .rfind(')')
221        .ok_or_else(|| parse_error(line.number, "graph inputs are not closed"))?;
222    let right = right.strip_suffix('{').unwrap_or(right).trim();
223    let right_open = right
224        .find('(')
225        .ok_or_else(|| parse_error(line.number, "graph outputs are missing"))?;
226    let right_close = right
227        .rfind(')')
228        .ok_or_else(|| parse_error(line.number, "graph outputs are not closed"))?;
229
230    let name = left[..left_open].trim().to_string();
231    let inputs = parse_typed_values(&left[left_open + 1..left_close], line.number)?;
232    let outputs = parse_typed_values(&right[right_open + 1..right_close], line.number)?;
233    Ok((name, inputs, outputs))
234}
235
236fn parse_typed_values(text: &str, line: usize) -> Result<Vec<TypedValue>> {
237    if text.trim().is_empty() {
238        return Ok(Vec::new());
239    }
240    split_top_level(text, ',')
241        .into_iter()
242        .map(|item| parse_typed_value(item.trim(), line))
243        .collect()
244}
245
246fn parse_typed_value(text: &str, line: usize) -> Result<TypedValue> {
247    let open = text
248        .find('[')
249        .ok_or_else(|| parse_error(line, "typed value is missing '['"))?;
250    let close = text
251        .find(']')
252        .ok_or_else(|| parse_error(line, "typed value is missing ']'"))?;
253    let dtype = parse_dtype(text[..open].trim(), line)?;
254    let name = text[close + 1..].trim();
255    if name.is_empty() {
256        return Err(parse_error(line, "typed value is missing a name"));
257    }
258    let dims = if text[open + 1..close].trim().is_empty() {
259        Vec::new()
260    } else {
261        split_top_level(&text[open + 1..close], ',')
262            .into_iter()
263            .map(|dim| dim.trim().to_string())
264            .collect()
265    };
266    Ok(TypedValue {
267        name: name.to_string(),
268        dtype,
269        dims,
270    })
271}
272
273fn create_typed_value(graph: &mut Graph, value: &TypedValue, line: usize) -> Result<ValueId> {
274    let mut shape = Shape::new();
275    for dim in &value.dims {
276        let dim = unquote(dim);
277        match dim.parse::<usize>() {
278            Ok(size) => shape.push(Dim::Static(size)),
279            Err(_) if !dim.is_empty() => {
280                shape.push(Dim::Symbolic(graph.intern_symbol(&dim)));
281            }
282            Err(_) => return Err(parse_error(line, "empty dimension")),
283        }
284    }
285    Ok(graph.create_named_value(&value.name, value.dtype, shape))
286}
287
288fn parse_node(
289    line: Line<'_>,
290    graph: &mut Graph,
291    values: &mut HashMap<String, ValueId>,
292    mark_opaque_placeholders: bool,
293) -> Result<NodeId> {
294    let text = strip_trailing_comment(line.text);
295    let open = find_top_level_char(text, '(')
296        .ok_or_else(|| parse_error(line.number, "node is missing input list"))?;
297    let close = text
298        .rfind(')')
299        .ok_or_else(|| parse_error(line.number, "node input list is not closed"))?;
300    if !text[close + 1..].trim().is_empty() {
301        return Err(parse_error(line.number, "unexpected text after node"));
302    }
303
304    let prefix = text[..open].trim();
305    let (lhs, invocation) = match find_top_level_char(prefix, '=') {
306        Some(eq) => (&prefix[..eq], prefix[eq + 1..].trim()),
307        None => ("", prefix),
308    };
309    let outputs = split_names(lhs);
310    let (op, attrs) = parse_invocation(invocation, line.number, mark_opaque_placeholders)?;
311    let inputs = split_top_level(&text[open + 1..close], ',')
312        .into_iter()
313        .map(|name| {
314            let name = name.trim();
315            if name.is_empty() {
316                Ok(None)
317            } else {
318                Ok(Some(value_id(graph, values, name)))
319            }
320        })
321        .collect::<Result<Vec<_>>>()?;
322    let output_ids = outputs
323        .iter()
324        .map(|name| value_id(graph, values, name))
325        .collect();
326
327    let (domain, op_type) = match op.rsplit_once('.') {
328        Some((domain, op_type)) => (domain.to_string(), op_type.to_string()),
329        None => (String::new(), op.to_string()),
330    };
331    if op_type.is_empty() {
332        return Err(parse_error(line.number, "node is missing an op type"));
333    }
334    let mut node = Node::new(NodeId(0), op_type, inputs, output_ids);
335    node.domain = domain;
336    node.attributes = attrs;
337    Ok(graph.insert_node(node))
338}
339
340fn parse_invocation(
341    text: &str,
342    line: usize,
343    mark_opaque_placeholders: bool,
344) -> Result<(&str, HashMap<String, Attribute>)> {
345    let Some(open) = find_top_level_char(text, '<') else {
346        return Ok((text.trim(), HashMap::new()));
347    };
348    let close = matching_close(text, open, '<', '>')
349        .ok_or_else(|| parse_error(line, "attribute block is not closed"))?;
350    if !text[close + 1..].trim().is_empty() {
351        return Err(parse_error(line, "unexpected text after attributes"));
352    }
353    let mut attrs = HashMap::new();
354    for pair in split_top_level(&text[open + 1..close], ',') {
355        let eq = find_top_level_char(pair, '=')
356            .ok_or_else(|| parse_error(line, "attribute is missing '='"))?;
357        let name = pair[..eq].trim();
358        if name.is_empty() {
359            return Err(parse_error(line, "attribute is missing a name"));
360        }
361        attrs.insert(
362            name.to_string(),
363            parse_attribute(pair[eq + 1..].trim(), line, mark_opaque_placeholders)?,
364        );
365    }
366    Ok((text[..open].trim(), attrs))
367}
368
369fn parse_attribute(text: &str, line: usize, mark_opaque_placeholders: bool) -> Result<Attribute> {
370    match text {
371        "[]:ints" => return Ok(Attribute::Ints(Vec::new())),
372        "[]:floats" => return Ok(Attribute::Floats(Vec::new())),
373        "[]:strings" => return Ok(Attribute::Strings(Vec::new())),
374        "[]:graphs" => return Ok(Attribute::Graphs(Vec::new())),
375        _ => {}
376    }
377    if text.starts_with('"') {
378        let value: String = serde_yaml::from_str(text)
379            .map_err(|error| parse_error(line, format!("invalid string: {error}")))?;
380        return Ok(Attribute::String(value.into_bytes()));
381    }
382    if text.starts_with('[') {
383        if text.contains('"') {
384            let values: Vec<String> = serde_yaml::from_str(text)
385                .map_err(|error| parse_error(line, format!("invalid string list: {error}")))?;
386            return Ok(Attribute::Strings(
387                values.into_iter().map(String::into_bytes).collect(),
388            ));
389        }
390        let body = text
391            .strip_prefix('[')
392            .and_then(|v| v.strip_suffix(']'))
393            .ok_or_else(|| parse_error(line, "attribute list is not closed"))?;
394        if body.trim().is_empty() {
395            return Ok(Attribute::Ints(Vec::new()));
396        }
397        let parts = split_top_level(body, ',');
398        if parts.iter().any(|part| looks_float(part.trim())) {
399            return parts
400                .into_iter()
401                .map(|part| parse_float(part.trim(), line))
402                .collect::<Result<Vec<_>>>()
403                .map(Attribute::Floats);
404        }
405        return parts
406            .into_iter()
407            .map(|part| {
408                part.trim()
409                    .parse::<i64>()
410                    .map_err(|_| parse_error(line, "invalid integer list"))
411            })
412            .collect::<Result<Vec<_>>>()
413            .map(Attribute::Ints);
414    }
415    if let Some(count) = text
416        .strip_prefix('<')
417        .and_then(|v| v.strip_suffix(" strings>"))
418    {
419        let count = count
420            .parse::<usize>()
421            .map_err(|_| parse_error(line, "invalid string-list reference"))?;
422        if count > 1_000_000 {
423            return Err(parse_error(line, "string-list reference is too large"));
424        }
425        let placeholder = if mark_opaque_placeholders {
426            vec![super::extensions::OPAQUE_PLACEHOLDER_BYTE]
427        } else {
428            Vec::new()
429        };
430        return Ok(Attribute::Strings(vec![placeholder; count]));
431    }
432    if let Some(count) = text
433        .strip_prefix('<')
434        .and_then(|value| value.strip_suffix(" sparse tensors>"))
435    {
436        let count = parse_placeholder_count(count, line, "sparse-tensor-list")?;
437        return Ok(Attribute::SparseTensors(
438            (0..count).map(|_| empty_sparse_tensor()).collect(),
439        ));
440    }
441    if let Some(count) = text
442        .strip_prefix('<')
443        .and_then(|value| value.strip_suffix(" tensors>"))
444    {
445        let count = parse_placeholder_count(count, line, "tensor-list")?;
446        return Ok(Attribute::Tensors(
447            (0..count)
448                .map(|_| TensorData::from_raw(DataType::Float32, Vec::new(), Vec::new()))
449                .collect(),
450        ));
451    }
452    if let Some(count) = text
453        .strip_prefix('<')
454        .and_then(|value| value.strip_suffix(" types>"))
455    {
456        let count = parse_placeholder_count(count, line, "type-list")?;
457        return Ok(Attribute::TypeProtos(
458            (0..count)
459                .map(|_| TypeProto::Tensor {
460                    dtype: DataType::Float32,
461                    shape: Vec::new(),
462                })
463                .collect(),
464        ));
465    }
466    if let Some(count) = text
467        .strip_prefix('<')
468        .and_then(|v| v.strip_suffix(" bytes>"))
469    {
470        let count = count
471            .parse::<usize>()
472            .map_err(|_| parse_error(line, "invalid byte-string reference"))?;
473        if count > 64 * 1024 * 1024 {
474            return Err(parse_error(line, "byte-string reference exceeds 64 MiB"));
475        }
476        let byte = if mark_opaque_placeholders {
477            super::extensions::OPAQUE_PLACEHOLDER_BYTE
478        } else {
479            0
480        };
481        return Ok(Attribute::String(vec![byte; count]));
482    }
483    if let Some(body) = text
484        .strip_prefix("<tensor ")
485        .and_then(|v| v.strip_suffix('>'))
486    {
487        let typed = parse_tensor_reference(body, line)?;
488        return Ok(Attribute::Tensor(typed));
489    }
490    if text == "<sparse tensor>" {
491        return Ok(Attribute::SparseTensor(empty_sparse_tensor()));
492    }
493    if text == "<type>" {
494        return Ok(Attribute::TypeProto(TypeProto::Tensor {
495            dtype: DataType::Float32,
496            shape: Vec::new(),
497        }));
498    }
499    if looks_float(text) {
500        return parse_float(text, line).map(Attribute::Float);
501    }
502    text.parse::<i64>()
503        .map(Attribute::Int)
504        .map_err(|_| parse_error(line, "unrecognized attribute value"))
505}
506
507fn parse_placeholder_count(text: &str, line: usize, kind: &str) -> Result<usize> {
508    let count = text
509        .parse::<usize>()
510        .map_err(|_| parse_error(line, format!("invalid {kind} reference")))?;
511    if count > 1_000_000 {
512        return Err(parse_error(line, format!("{kind} reference is too large")));
513    }
514    Ok(count)
515}
516
517fn empty_sparse_tensor() -> SparseTensorData {
518    SparseTensorData {
519        values: TensorData::from_raw(DataType::Float32, vec![0], Vec::new()),
520        indices: TensorData::from_raw(DataType::Int64, vec![0], Vec::new()),
521        dims: Vec::new(),
522    }
523}
524
525fn parse_tensor_reference(text: &str, line: usize) -> Result<TensorData> {
526    let open = text
527        .find('[')
528        .ok_or_else(|| parse_error(line, "tensor reference is missing shape"))?;
529    let dtype = parse_dtype(text[..open].trim(), line)?;
530    let mut dims_text = text[open + 1..]
531        .strip_suffix(']')
532        .ok_or_else(|| parse_error(line, "tensor reference shape is not closed"))?
533        .trim();
534    if dims_text.starts_with('[') && dims_text.ends_with(']') {
535        dims_text = &dims_text[1..dims_text.len() - 1];
536    }
537    let dims = parse_static_dims(dims_text, line)?;
538    placeholder_tensor(dtype, dims, line)
539}
540
541fn parse_initializer(
542    text: &str,
543    line: usize,
544    graph: &mut Graph,
545    values: &mut HashMap<String, ValueId>,
546) -> Result<()> {
547    let eq = text
548        .find('=')
549        .ok_or_else(|| parse_error(line, "initializer reference is missing '='"))?;
550    let typed = parse_typed_value(text[..eq].trim(), line)?;
551    let kind = text[eq + 1..].trim();
552    let dims = typed
553        .dims
554        .iter()
555        .map(|dim| {
556            dim.parse::<usize>()
557                .map_err(|_| parse_error(line, "initializer shape must be static"))
558        })
559        .collect::<Result<Vec<_>>>()?;
560    let id = match values.get(&typed.name) {
561        Some(id) => *id,
562        None => {
563            let id = create_typed_value(graph, &typed, line)?;
564            values.insert(typed.name, id);
565            id
566        }
567    };
568    let weight = if kind == "<inline data omitted>" {
569        WeightRef::Inline(placeholder_tensor(typed.dtype, dims, line)?)
570    } else if kind == "<external data omitted>" {
571        let numel = checked_numel(&dims, line)?;
572        let length = typed
573            .dtype
574            .checked_storage_bytes(numel)
575            .ok_or_else(|| parse_error(line, "initializer byte size overflows"))?;
576        WeightRef::External {
577            path: PathBuf::new(),
578            offset: 0,
579            length,
580            dtype: typed.dtype,
581            dims,
582        }
583    } else {
584        return Err(parse_error(line, "unknown initializer reference kind"));
585    };
586    graph.set_initializer(id, weight);
587    Ok(())
588}
589
590fn placeholder_tensor(dtype: DataType, dims: Vec<usize>, line: usize) -> Result<TensorData> {
591    let numel = checked_numel(&dims, line)?;
592    let bytes = dtype
593        .checked_storage_bytes(numel)
594        .ok_or_else(|| parse_error(line, "tensor byte size overflows"))?;
595    const MAX_PLACEHOLDER_BYTES: usize = 64 * 1024 * 1024;
596    if bytes > MAX_PLACEHOLDER_BYTES {
597        return Err(parse_error(
598            line,
599            "tensor placeholder exceeds 64 MiB safety limit",
600        ));
601    }
602    Ok(TensorData::from_raw(dtype, dims, vec![0; bytes]))
603}
604
605fn checked_numel(dims: &[usize], line: usize) -> Result<usize> {
606    dims.iter().try_fold(1usize, |count, dim| {
607        count
608            .checked_mul(*dim)
609            .ok_or_else(|| parse_error(line, "tensor element count overflows"))
610    })
611}
612
613fn attach_subgraph(
614    graph: &mut Graph,
615    node_id: NodeId,
616    name: String,
617    index: Option<usize>,
618    subgraph: Graph,
619) {
620    let key = match index {
621        Some(index) => format!("{name}[{index}]"),
622        None => name.clone(),
623    };
624    graph.subgraphs.insert((node_id, key), subgraph.clone());
625    let attrs = &mut graph.node_mut(node_id).attributes;
626    match index {
627        None => {
628            attrs.insert(name, Attribute::Graph(Box::new(subgraph)));
629        }
630        Some(index) => {
631            let attr = attrs
632                .entry(name)
633                .or_insert_with(|| Attribute::Graphs(Vec::new()));
634            if let Attribute::Graphs(graphs) = attr {
635                graphs.resize_with(index + 1, Graph::new);
636                graphs[index] = subgraph;
637            }
638        }
639    }
640}
641
642fn parse_subgraph_marker(text: &str) -> Option<(String, Option<usize>)> {
643    let lhs = text.strip_suffix("= graph")?.trim();
644    if let Some(open) = lhs.rfind('[')
645        && lhs.ends_with(']')
646        && let Ok(index) = lhs[open + 1..lhs.len() - 1].parse()
647    {
648        if index <= 65_535 {
649            return Some((lhs[..open].to_string(), Some(index)));
650        }
651        return None;
652    }
653    (!lhs.is_empty()).then(|| (lhs.to_string(), None))
654}
655
656fn value_id(graph: &mut Graph, values: &mut HashMap<String, ValueId>, name: &str) -> ValueId {
657    *values
658        .entry(name.to_string())
659        .or_insert_with(|| graph.create_named_value(name, DataType::Float32, Vec::new()))
660}
661
662fn split_names(text: &str) -> Vec<&str> {
663    if text.trim().is_empty() {
664        Vec::new()
665    } else {
666        text.split(',')
667            .map(str::trim)
668            .filter(|name| !name.is_empty())
669            .collect()
670    }
671}
672
673fn parse_opset_imports(text: &str, line: usize) -> Result<HashMap<String, u64>> {
674    let body = text
675        .strip_prefix('[')
676        .and_then(|v| v.strip_suffix(']'))
677        .ok_or_else(|| parse_error(line, "opset_import must be enclosed in brackets"))?;
678    let mut imports = HashMap::new();
679    if body.trim().is_empty() {
680        return Ok(imports);
681    }
682    for entry in split_top_level(body, ',') {
683        let colon = find_top_level_char(entry, ':')
684            .ok_or_else(|| parse_error(line, "opset import is missing ':'"))?;
685        let domain: String = serde_yaml::from_str(entry[..colon].trim())
686            .map_err(|error| parse_error(line, format!("invalid opset domain: {error}")))?;
687        let version = entry[colon + 1..]
688            .trim()
689            .parse()
690            .map_err(|_| parse_error(line, "invalid opset version"))?;
691        imports.insert(domain, version);
692    }
693    Ok(imports)
694}
695
696fn parse_static_dims(text: &str, line: usize) -> Result<Vec<usize>> {
697    if text.trim().is_empty() {
698        return Ok(Vec::new());
699    }
700    split_top_level(text, ',')
701        .into_iter()
702        .map(|dim| {
703            dim.trim()
704                .parse()
705                .map_err(|_| parse_error(line, "invalid static dimension"))
706        })
707        .collect()
708}
709
710fn parse_dtype(text: &str, line: usize) -> Result<DataType> {
711    let dtype = match text {
712        "undefined" => DataType::Undefined,
713        "float" | "float32" => DataType::Float32,
714        "uint8" => DataType::Uint8,
715        "int8" => DataType::Int8,
716        "uint16" => DataType::Uint16,
717        "int16" => DataType::Int16,
718        "int32" => DataType::Int32,
719        "int64" => DataType::Int64,
720        "string" => DataType::String,
721        "bool" => DataType::Bool,
722        "float16" => DataType::Float16,
723        "float64" => DataType::Float64,
724        "uint32" => DataType::Uint32,
725        "uint64" => DataType::Uint64,
726        "complex64" => DataType::Complex64,
727        "complex128" => DataType::Complex128,
728        "bfloat16" => DataType::BFloat16,
729        "float8e4m3fn" => DataType::Float8E4M3FN,
730        "float8e4m3fnuz" => DataType::Float8E4M3FNUZ,
731        "float8e5m2" => DataType::Float8E5M2,
732        "float8e5m2fnuz" => DataType::Float8E5M2FNUZ,
733        "uint4" => DataType::Uint4,
734        "int4" => DataType::Int4,
735        "float4e2m1" => DataType::Float4E2M1,
736        "float8e8m0" => DataType::Float8E8M0,
737        "uint2" => DataType::Uint2,
738        "int2" => DataType::Int2,
739        _ => return Err(parse_error(line, format!("unknown dtype '{text}'"))),
740    };
741    Ok(dtype)
742}
743
744fn unquote(text: &str) -> String {
745    text.strip_prefix('"')
746        .and_then(|text| text.strip_suffix('"'))
747        .unwrap_or(text)
748        .to_string()
749}
750
751fn parse_float(text: &str, line: usize) -> Result<f32> {
752    match text {
753        "inf" => Ok(f32::INFINITY),
754        "-inf" => Ok(f32::NEG_INFINITY),
755        "NaN" => Ok(f32::NAN),
756        _ => text
757            .parse()
758            .map_err(|_| parse_error(line, "invalid float attribute")),
759    }
760}
761
762fn looks_float(text: &str) -> bool {
763    text.contains(['.', 'e', 'E']) || matches!(text, "inf" | "-inf" | "NaN")
764}
765
766fn trim_comma(text: &str) -> &str {
767    text.trim().strip_suffix(',').unwrap_or(text.trim()).trim()
768}
769
770fn strip_trailing_comment(text: &str) -> &str {
771    let mut quoted = false;
772    let mut escaped = false;
773    for (index, ch) in text.char_indices() {
774        if escaped {
775            escaped = false;
776        } else if ch == '\\' && quoted {
777            escaped = true;
778        } else if ch == '"' {
779            quoted = !quoted;
780        } else if ch == '/' && !quoted && text[index..].starts_with("//") {
781            return text[..index].trim_end();
782        }
783    }
784    text
785}
786
787fn split_top_level(text: &str, separator: char) -> Vec<&str> {
788    let mut parts = Vec::new();
789    let mut start = 0;
790    let mut stack = Vec::new();
791    let mut quoted = false;
792    let mut escaped = false;
793    for (index, ch) in text.char_indices() {
794        if escaped {
795            escaped = false;
796            continue;
797        }
798        if ch == '\\' && quoted {
799            escaped = true;
800            continue;
801        }
802        if ch == '"' {
803            quoted = !quoted;
804            continue;
805        }
806        if quoted {
807            continue;
808        }
809        match ch {
810            '(' | '[' | '<' => stack.push(ch),
811            ')' | ']' | '>' => {
812                stack.pop();
813            }
814            _ if ch == separator && stack.is_empty() => {
815                parts.push(&text[start..index]);
816                start = index + ch.len_utf8();
817            }
818            _ => {}
819        }
820    }
821    parts.push(&text[start..]);
822    parts
823}
824
825fn find_top_level_char(text: &str, needle: char) -> Option<usize> {
826    let mut stack = Vec::new();
827    let mut quoted = false;
828    let mut escaped = false;
829    for (index, ch) in text.char_indices() {
830        if escaped {
831            escaped = false;
832            continue;
833        }
834        if ch == '\\' && quoted {
835            escaped = true;
836            continue;
837        }
838        if ch == '"' {
839            quoted = !quoted;
840            continue;
841        }
842        if quoted {
843            continue;
844        }
845        if ch == needle && stack.is_empty() {
846            return Some(index);
847        }
848        match ch {
849            '(' | '[' | '<' => stack.push(ch),
850            ')' | ']' | '>' => {
851                stack.pop();
852            }
853            _ => {}
854        }
855    }
856    None
857}
858
859fn matching_close(text: &str, open: usize, left: char, right: char) -> Option<usize> {
860    let mut depth = 0;
861    let mut quoted = false;
862    let mut escaped = false;
863    for (offset, ch) in text[open..].char_indices() {
864        if escaped {
865            escaped = false;
866            continue;
867        }
868        if ch == '\\' && quoted {
869            escaped = true;
870            continue;
871        }
872        if ch == '"' {
873            quoted = !quoted;
874        } else if !quoted && ch == left {
875            depth += 1;
876        } else if !quoted && ch == right {
877            depth -= 1;
878            if depth == 0 {
879                return Some(open + offset);
880            }
881        }
882    }
883    None
884}
885
886fn parse_error(line: usize, message: impl Into<String>) -> Error {
887    Error::TextParse {
888        line,
889        message: message.into(),
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896    use crate::text::to_text;
897    use onnx_runtime_ir::{Node, static_shape};
898
899    fn node_by_op<'a>(graph: &'a Graph, op: &str) -> &'a Node {
900        graph
901            .nodes
902            .values()
903            .find(|node| node.op_type == op)
904            .unwrap()
905    }
906
907    #[test]
908    fn round_trips_simple_graph_and_domain_imports() {
909        let mut graph = Graph::new();
910        graph.opset_imports.insert(String::new(), 21);
911        graph.opset_imports.insert("com.acme".into(), 3);
912        let x = graph.create_named_value("X", DataType::Float32, static_shape([2, 3]));
913        let y = graph.create_named_value("Y", DataType::Float32, static_shape([2, 3]));
914        graph.add_input(x);
915        let mut node = Node::new(NodeId(0), "CustomAdd", vec![Some(x)], vec![y]);
916        node.domain = "com.acme".into();
917        graph.insert_node(node);
918        graph.add_output(y);
919        let metadata = ModelMetadata {
920            graph_name: "compute".into(),
921            ir_version: 9,
922            ..ModelMetadata::default()
923        };
924        let model = Model::with_metadata(graph, metadata);
925
926        let parsed = from_text(&to_text(&model)).unwrap();
927        assert_eq!(parsed.metadata.ir_version, 9);
928        assert_eq!(parsed.metadata.graph_name, "compute");
929        assert_eq!(parsed.graph.opset_imports, model.graph.opset_imports);
930        assert_eq!(parsed.graph.inputs.len(), 1);
931        assert_eq!(parsed.graph.outputs.len(), 1);
932        let node = node_by_op(&parsed.graph, "CustomAdd");
933        assert_eq!(node.domain, "com.acme");
934        assert_eq!(node.inputs.len(), 1);
935        assert_eq!(node.outputs.len(), 1);
936    }
937
938    #[test]
939    fn round_trips_lists_tensor_attribute_and_initializer_reference() {
940        let mut graph = Graph::new();
941        graph.opset_imports.insert(String::new(), 21);
942        let x = graph.create_named_value("X", DataType::Float32, static_shape([2]));
943        let w = graph.create_named_value("W", DataType::Float32, static_shape([2]));
944        let y = graph.create_named_value("Y", DataType::Float32, static_shape([2]));
945        graph.add_input(x);
946        graph.set_initializer(
947            w,
948            WeightRef::Inline(TensorData::from_raw(DataType::Float32, vec![2], vec![0; 8])),
949        );
950        let mut node = Node::new(NodeId(0), "Decorated", vec![Some(x), Some(w)], vec![y]);
951        node.attributes.insert("axis".into(), Attribute::Int(-1));
952        node.attributes
953            .insert("alpha".into(), Attribute::Float(0.25));
954        node.attributes
955            .insert("label".into(), Attribute::String(b"hello".to_vec()));
956        node.attributes
957            .insert("axes".into(), Attribute::Ints(vec![1, 2]));
958        node.attributes
959            .insert("scales".into(), Attribute::Floats(vec![0.5, 2.0]));
960        node.attributes.insert(
961            "labels".into(),
962            Attribute::Strings(vec![b"a".to_vec(), b"b".to_vec()]),
963        );
964        node.attributes.insert(
965            "value".into(),
966            Attribute::Tensor(TensorData::from_raw(DataType::Int64, vec![2], vec![0; 16])),
967        );
968        graph.insert_node(node);
969        graph.add_output(y);
970
971        let parsed = from_text(&to_text(&Model::new(graph))).unwrap();
972        let node = node_by_op(&parsed.graph, "Decorated");
973        assert!(matches!(node.attributes["axis"], Attribute::Int(-1)));
974        assert!(matches!(node.attributes["alpha"], Attribute::Float(0.25)));
975        assert_eq!(node.attributes["label"].as_str(), Some("hello"));
976        assert!(matches!(&node.attributes["axes"], Attribute::Ints(v) if v == &[1, 2]));
977        assert!(matches!(&node.attributes["scales"], Attribute::Floats(v) if v == &[0.5, 2.0]));
978        assert!(matches!(
979            &node.attributes["labels"],
980            Attribute::Strings(v) if v == &[b"a".to_vec(), b"b".to_vec()]
981        ));
982        assert!(
983            matches!(&node.attributes["value"], Attribute::Tensor(t) if t.dtype == DataType::Int64 && t.dims == [2])
984        );
985        let initializer = parsed.graph.initializers.values().next().unwrap();
986        assert_eq!(initializer.dtype(), DataType::Float32);
987        assert_eq!(initializer.dims(), &[2]);
988    }
989
990    #[test]
991    fn round_trips_empty_typed_attribute_lists() {
992        let mut graph = Graph::new();
993        graph.opset_imports.insert(String::new(), 21);
994        let x = graph.create_named_value("X", DataType::Float32, static_shape([2]));
995        let y = graph.create_named_value("Y", DataType::Float32, static_shape([2]));
996        graph.add_input(x);
997        let mut node = Node::new(NodeId(0), "EmptyLists", vec![Some(x)], vec![y]);
998        node.attributes
999            .insert("ints".into(), Attribute::Ints(Vec::new()));
1000        node.attributes
1001            .insert("floats".into(), Attribute::Floats(Vec::new()));
1002        node.attributes
1003            .insert("strings".into(), Attribute::Strings(Vec::new()));
1004        node.attributes
1005            .insert("graphs".into(), Attribute::Graphs(Vec::new()));
1006        graph.insert_node(node);
1007        graph.add_output(y);
1008
1009        let text = to_text(&Model::new(graph));
1010        assert!(text.contains("ints = []:ints"), "{text}");
1011        assert!(text.contains("floats = []:floats"), "{text}");
1012        assert!(text.contains("strings = []:strings"), "{text}");
1013        assert!(text.contains("graphs = []:graphs"), "{text}");
1014
1015        let parsed = from_text(&text).unwrap();
1016        let node = node_by_op(&parsed.graph, "EmptyLists");
1017        assert!(matches!(&node.attributes["ints"], Attribute::Ints(v) if v.is_empty()));
1018        assert!(matches!(&node.attributes["floats"], Attribute::Floats(v) if v.is_empty()));
1019        assert!(matches!(&node.attributes["strings"], Attribute::Strings(v) if v.is_empty()));
1020        assert!(matches!(&node.attributes["graphs"], Attribute::Graphs(v) if v.is_empty()));
1021    }
1022
1023    fn branch(input: &str, output: &str, op: &str) -> Graph {
1024        let mut graph = Graph::new();
1025        let x = graph.create_named_value(input, DataType::Float32, static_shape([2]));
1026        let y = graph.create_named_value(output, DataType::Float32, static_shape([2]));
1027        graph.add_input(x);
1028        graph.insert_node(Node::new(NodeId(0), op, vec![Some(x)], vec![y]));
1029        graph.add_output(y);
1030        graph
1031    }
1032
1033    #[test]
1034    fn round_trips_if_subgraphs() {
1035        let then_branch = branch("then_in", "then_out", "Relu");
1036        let else_branch = branch("else_in", "else_out", "Neg");
1037        let case_zero = branch("case0_in", "case0_out", "Identity");
1038        let case_one = branch("case1_in", "case1_out", "Abs");
1039        let mut graph = Graph::new();
1040        graph.opset_imports.insert(String::new(), 21);
1041        let cond = graph.create_named_value("cond", DataType::Bool, static_shape([]));
1042        let out = graph.create_named_value("out", DataType::Float32, static_shape([2]));
1043        graph.add_input(cond);
1044        let mut node = Node::new(NodeId(0), "If", vec![Some(cond)], vec![out]);
1045        node.attributes.insert(
1046            "then_branch".into(),
1047            Attribute::Graph(Box::new(then_branch.clone())),
1048        );
1049        node.attributes.insert(
1050            "else_branch".into(),
1051            Attribute::Graph(Box::new(else_branch.clone())),
1052        );
1053        node.attributes.insert(
1054            "cases".into(),
1055            Attribute::Graphs(vec![case_zero.clone(), case_one.clone()]),
1056        );
1057        let node_id = graph.insert_node(node);
1058        graph
1059            .subgraphs
1060            .insert((node_id, "then_branch".into()), then_branch);
1061        graph
1062            .subgraphs
1063            .insert((node_id, "else_branch".into()), else_branch);
1064        graph
1065            .subgraphs
1066            .insert((node_id, "cases[0]".into()), case_zero);
1067        graph
1068            .subgraphs
1069            .insert((node_id, "cases[1]".into()), case_one);
1070        graph.add_output(out);
1071
1072        let parsed = from_text(&to_text(&Model::new(graph))).unwrap();
1073        let if_node = node_by_op(&parsed.graph, "If");
1074        assert!(matches!(
1075            &if_node.attributes["then_branch"],
1076            Attribute::Graph(graph) if node_by_op(graph, "Relu").op_type == "Relu"
1077        ));
1078        assert!(matches!(
1079            &if_node.attributes["else_branch"],
1080            Attribute::Graph(graph) if node_by_op(graph, "Neg").op_type == "Neg"
1081        ));
1082        assert!(matches!(
1083            &if_node.attributes["cases"],
1084            Attribute::Graphs(graphs)
1085                if node_by_op(&graphs[0], "Identity").op_type == "Identity"
1086                    && node_by_op(&graphs[1], "Abs").op_type == "Abs"
1087        ));
1088        assert_eq!(parsed.graph.subgraphs.len(), 4);
1089    }
1090
1091    #[test]
1092    fn malformed_text_returns_error() {
1093        let malformed = "<\n  ir_version: nope,\n>\nmain () => () {\n";
1094        assert!(matches!(from_text(malformed), Err(Error::TextParse { .. })));
1095    }
1096}