1use 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
27pub 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}