hugr_model/v0/mod.rs
1//! Version 0 (unstable).
2//!
3//! **Warning**: This module is still under development and is expected to change.
4//! It is included in the library to allow for early experimentation, and for
5//! the core and model to converge incrementally.
6//!
7//! This module defines representations of the hugr IR as plain data, designed
8//! to be as independent of implementation details as feasible. It can be used
9//! by the core compiler, alternative implementations or tooling that does not
10//! need the power/complexity of the full compiler. We provide the following
11//! in-memory representations:
12//!
13//! - [Table]: Efficient intermediate data structure to facilitate conversions.
14//! - [AST]: Abstract syntax tree that uses direct references rather than table indices.
15//!
16//! The table and AST format are interconvertible and can be serialised to
17//! a binary and text format, respectively:
18//!
19//! - [Binary]: Binary serialisation format optimised for performance and size.
20//! - [Text]: Human readable s-expression based text format.
21//!
22//! # Logical Format
23//!
24//! The hugr IR is a hierarchical graph data structure. __Nodes__ represent both
25//! __instructions__ that manipulate runtime values and __symbols__ which
26//! represent named language objects. Instructions have __input__ and __output__ ports
27//! and runtime values flow between ports when they are connected by a __link__.
28//!
29//! Nodes are organised into __regions__ and do not have any explicit ordering
30//! between them; any schedule that respects the data dependencies between nodes
31//! is valid. Regions come in three different kinds. __Module regions__ form the
32//! top level of a module and can only contain symbols. __Dataflow regions__
33//! describe how data flows from the region's __source__ ports to the region's
34//! __target__ ports. __Controlflow regions__ are control flow graphs containing
35//! dataflow __blocks__, with control flow originating from the region's source
36//! ports and ending in the region's target ports.
37//!
38//! __Terms__ form a meta language that is used to describe types, parameters and metadata that
39//! are known statically. To allow types to be parameterized by values, types and values
40//! are treated uniformly as terms, enabling a restricted form of dependent typing.
41//! Terms are extensible declaratively via __constructors__.
42//! __Constraints__ can be used to express more complex validation rules.
43//!
44//! # Remaining Mismatch with `hugr-core`
45//!
46//! This data model was designed to encode as much of `hugr-core` as possible while also
47//! filling in conceptual gaps and providing a forward-compatible foundation for future
48//! development. However, there are still some mismatches with `hugr-core` that are not
49//! addressed by conversions in import/export:
50//!
51//! - Some static types can not yet be represented in `hugr-core` although they should be.
52//! - The model does not have types with a copy bound as `hugr-core` does, and instead uses
53//! a more general form of type constraints ([#1556]). Similarly, the model does not have
54//! bounded naturals. We perform a conversion for compatibility where possible, but this does
55//! not fully cover all potential cases of bounds.
56//! - `hugr-model` allows to declare term constructors that serve as blueprints for constructing
57//! runtime values. This allows constants to have potentially multiple representations,
58//! which can be essential in case of very large constants that require efficient encodings.
59//! `hugr-core` is more restricted, requiring a canonical representation for constant values.
60//! - `hugr-model` has support for passing closed regions as static parameters to operations,
61//! which allows for higher-order operations that require their function arguments to be
62//! statically known. We currently do not yet support converting this to `hugr-core`.
63//! - In a model module, ports are connected when they share the same link. This differs from
64//! the type of port connectivity in the graph data structure used by `hugr-core`. However,
65//! `hugr-core` restricts connectivity so that in any group of connected ports there is at
66//! most one output port (for dataflow) or at most one input port (for control flow). In
67//! these cases, there is no mismatch.
68//! - `hugr-core` only allows to define type aliases, but not aliases for other terms.
69//! - `hugr-model` has no concept of order edges, encoding a strong preference that ordering
70//! requirements be encoded within the dataflow paradigm.
71//! - Both `hugr-model` and `hugr-core` support metadata, but they use different encodings.
72//! `hugr-core` encodes metadata as JSON objects, while `hugr-model` uses terms. Using
73//! terms has the advantage that metadata can be validated with the same type checking
74//! mechanism as the rest of the model ([#1553]).
75//! - `hugr-model` have a root region that corresponds to a root `Module` in `hugr-core`.
76//! `hugr-core` however can have nodes with different operations as their root ([#1554]).
77//!
78//! [#1556]: https://github.com/CQCL/hugr/discussions/1556
79//! [#1553]: https://github.com/CQCL/hugr/issues/1553
80//! [#1554]: https://github.com/CQCL/hugr/issues/1554
81//! [Text]: crate::v0::ast
82//! [Binary]: crate::v0::binary
83//! [Table]: crate::v0::table
84//! [AST]: crate::v0::ast
85use ordered_float::OrderedFloat;
86#[cfg(feature = "pyo3")]
87use pyo3::PyTypeInfo as _;
88#[cfg(feature = "pyo3")]
89use pyo3::types::PyAnyMethods as _;
90use smol_str::SmolStr;
91use std::sync::Arc;
92use table::LinkIndex;
93
94/// Describes how a function or symbol should be acted upon by a linker
95#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
96pub enum Visibility {
97 /// The linker should ignore this function or symbol
98 Private,
99 /// The linker should act upon this function or symbol
100 Public,
101}
102
103/// Core function types.
104///
105/// - **Parameter:** `?inputs : (core.list core.type)`
106/// - **Parameter:** `?outputs : (core.list core.type)`
107/// - **Result:** `core.type`
108pub const CORE_FN: &str = "core.fn";
109
110/// The type of runtime types.
111///
112/// Runtime types are the types of values that can flow between nodes at runtime.
113///
114/// - **Result:** `?type : core.static`
115pub const CORE_TYPE: &str = "core.type";
116
117/// The type of static types.
118///
119/// Static types are the types of statically known parameters.
120///
121/// This is the only term that is its own type.
122///
123/// - **Result:** `?type : core.static`
124pub const CORE_STATIC: &str = "core.static";
125
126/// The type of constraints.
127///
128/// - **Result:** `?type : core.static`
129pub const CORE_CONSTRAINT: &str = "core.constraint";
130
131/// The constraint for non-linear runtime data.
132///
133/// Runtime values are copied implicitly by connecting an output port to more
134/// than one input port. Similarly runtime values can be deleted implicitly when
135/// an output port is not connected to any input port. In either of these cases
136/// the type of the runtime value must satisfy this constraint.
137///
138/// - **Parameter:** `?type : core.type`
139/// - **Result:** `core.constraint`
140pub const CORE_NON_LINEAR: &str = "core.nonlinear";
141
142/// The type of metadata.
143///
144/// - **Result:** `?type : core.static`
145pub const CORE_META: &str = "core.meta";
146
147/// Runtime algebraic data types.
148///
149/// Algebraic data types are sums of products of other runtime types.
150///
151/// - **Parameter:** `?variants : (core.list (core.list core.type))`
152/// - **Result:** `core.type`
153pub const CORE_ADT: &str = "core.adt";
154
155/// Type of string literals.
156///
157/// - **Result:** `core.static`
158pub const CORE_STR_TYPE: &str = "core.str";
159
160/// Type of natural number literals.
161///
162/// - **Result:** `core.static`
163pub const CORE_NAT_TYPE: &str = "core.nat";
164
165/// Type of bytes literals.
166///
167/// - **Result:** `core.static`
168pub const CORE_BYTES_TYPE: &str = "core.bytes";
169
170/// Type of float literals.
171///
172/// - **Result:** `core.static`
173pub const CORE_FLOAT_TYPE: &str = "core.float";
174
175/// Type of control flow regions.
176///
177/// - **Parameter:** `?inputs : (core.list (core.list core.type))`
178/// - **Parameter:** `?outputs : (core.list (core.list core.type))`
179/// - **Result:** `core.type`
180pub const CORE_CTRL: &str = "core.ctrl";
181
182/// The type for runtime constants.
183///
184/// - **Parameter:** `?type : core.type`
185/// - **Result:** `core.static`
186pub const CORE_CONST: &str = "core.const";
187
188/// Constants for runtime algebraic data types.
189///
190/// - **Parameter:** `?variants : (core.list core.type)`
191/// - **Parameter:** `?types : (core.list core.static)`
192/// - **Parameter:** `?tag : core.nat`
193/// - **Parameter:** `?values : (core.tuple ?types)`
194/// - **Result:** `(core.const (core.adt ?variants))`
195pub const CORE_CONST_ADT: &str = "core.const.adt";
196
197/// The type for lists of static data.
198///
199/// Lists are finite sequences such that all elements have the same type.
200/// For heterogeneous sequences, see [`CORE_TUPLE_TYPE`].
201///
202/// - **Parameter:** `?type : core.static`
203/// - **Result:** `core.static`
204pub const CORE_LIST_TYPE: &str = "core.list";
205
206/// The type for tuples of static data.
207///
208/// Tuples are finite sequences that allow elements to have different types.
209/// For homogeneous sequences, see [`CORE_LIST_TYPE`].
210///
211/// - **Parameter:** `?types : (core.list core.static)`
212/// - **Result:** `core.static`
213pub const CORE_TUPLE_TYPE: &str = "core.tuple";
214
215/// Operation to call a statically known function.
216///
217/// - **Parameter:** `?inputs : (core.list core.type)`
218/// - **Parameter:** `?outputs : (core.list core.type)`
219/// - **Parameter:** `?func : (core.const (core.fn ?inputs ?outputs))`
220/// - **Result:** `(core.fn ?inputs ?outputs ?ext)`
221pub const CORE_CALL: &str = "core.call";
222
223/// Operation to call a functiion known at runtime.
224///
225/// - **Parameter:** `?inputs : (core.list core.type)`
226/// - **Parameter:** `?outputs : (core.list core.type)`
227/// - **Result:** `(core.fn [(core.fn ?inputs ?outputs) ?inputs ...] ?outputs)`
228pub const CORE_CALL_INDIRECT: &str = "core.call_indirect";
229
230/// Operation to load a constant value.
231///
232/// - **Parameter:** `?type : core.type`
233/// - **Parameter:** `?value : (core.const ?type)`
234/// - **Result:** `(core.fn [] [?type])`
235pub const CORE_LOAD_CONST: &str = "core.load_const";
236
237/// Operation to create a value of an algebraic data type.
238///
239/// - **Parameter:** `?variants : (core.list (core.list core.type))`
240/// - **Parameter:** `?types : (core.list core.type)`
241/// - **Parameter:** `?tag : core.nat`
242/// - **Result:** `(core.fn ?types [(core.adt ?variants)])`
243pub const CORE_MAKE_ADT: &str = "core.make_adt";
244
245/// Constructor for documentation metadata.
246///
247/// - **Parameter:** `?description : core.str`
248/// - **Result:** `core.meta`
249pub const CORE_META_DESCRIPTION: &str = "core.meta.description";
250
251/// Metadata to tag a node or region as the entrypoint of a module.
252///
253/// - **Result:** `core.meta`
254pub const CORE_ENTRYPOINT: &str = "core.entrypoint";
255
256/// Constructor for JSON encoded metadata.
257///
258/// This is included in the model to allow for compatibility with `hugr-core`.
259/// The intention is to deprecate this in the future in favor of metadata
260/// expressed with custom constructors.
261///
262/// - **Parameter:** `?name : core.str`
263/// - **Parameter:** `?json : core.str`
264/// - **Result:** `core.meta`
265pub const COMPAT_META_JSON: &str = "compat.meta_json";
266
267/// Constructor for JSON encoded constants.
268///
269/// This is included in the model to allow for compatibility with `hugr-core`.
270/// The intention is to deprecate this in the future in favor of constants
271/// expressed with custom constructors.
272///
273/// - **Parameter:** `?type : core.type`
274/// - **Parameter:** `?json : core.str`
275/// - **Result:** `(core.const ?type)`
276pub const COMPAT_CONST_JSON: &str = "compat.const_json";
277
278/// Metadata constructor for order hint keys.
279///
280/// Nodes in a dataflow region can be annotated with a key. Each node may have
281/// at most one key and the key must be unique among all nodes in the same
282/// dataflow region. The parent dataflow graph can then use the
283/// `order_hint.order` metadata to imply a desired ordering relation, referring
284/// to the nodes by their key.
285///
286/// - **Parameter:** `?key : core.nat`
287/// - **Result:** `core.meta`
288pub const ORDER_HINT_KEY: &str = "core.order_hint.key";
289
290/// Metadata constructor for order hint keys on input nodes.
291///
292/// When the sources of a dataflow region are represented by an input operation
293/// within the region, this metadata can be attached the region to give the
294/// input node an order hint key.
295///
296/// - **Parameter:** `?key : core.nat`
297/// - **Result:** `core.meta`
298pub const ORDER_HINT_INPUT_KEY: &str = "core.order_hint.input_key";
299
300/// Metadata constructor for order hint keys on output nodes.
301///
302/// When the targets of a dataflow region are represented by an output operation
303/// within the region, this metadata can be attached the region to give the
304/// output node an order hint key.
305///
306/// - **Parameter:** `?key : core.nat`
307/// - **Result:** `core.meta`
308pub const ORDER_HINT_OUTPUT_KEY: &str = "core.order_hint.output_key";
309
310/// Metadata constructor for order hints.
311///
312/// When this metadata is attached to a dataflow region, it can indicate a
313/// preferred ordering relation between child nodes. Code generation must take
314/// this into account when deciding on an execution order. The child nodes are
315/// identified by a key, using the `order_hint.key` metadata.
316///
317/// The graph consisting of both value dependencies between nodes and order
318/// hints must be directed acyclic.
319///
320/// - **Parameter:** `?before : core.nat`
321/// - **Parameter:** `?after : core.nat`
322/// - **Result:** `core.meta`
323pub const ORDER_HINT_ORDER: &str = "core.order_hint.order";
324
325pub mod ast;
326pub mod binary;
327pub mod scope;
328pub mod table;
329
330pub use bumpalo;
331
332/// Type to indicate whether scopes are open or closed.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
334pub enum ScopeClosure {
335 /// A scope that is open and therefore not isolated from its parent scope.
336 #[default]
337 Open,
338 /// A scope that is closed and therefore isolated from its parent scope.
339 Closed,
340}
341
342#[cfg(feature = "pyo3")]
343impl<'py> pyo3::FromPyObject<'py> for ScopeClosure {
344 fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
345 let value: usize = ob.getattr("value")?.extract()?;
346 match value {
347 0 => Ok(Self::Open),
348 1 => Ok(Self::Closed),
349 _ => Err(pyo3::exceptions::PyTypeError::new_err(
350 "Invalid ScopeClosure.",
351 )),
352 }
353 }
354}
355
356#[cfg(feature = "pyo3")]
357impl<'py> pyo3::IntoPyObject<'py> for ScopeClosure {
358 type Target = pyo3::PyAny;
359 type Output = pyo3::Bound<'py, Self::Target>;
360 type Error = pyo3::PyErr;
361
362 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
363 let py_module = py.import("hugr.model")?;
364 let py_class = py_module.getattr("ScopeClosure")?;
365
366 match self {
367 ScopeClosure::Open => py_class.getattr("OPEN"),
368 ScopeClosure::Closed => py_class.getattr("CLOSED"),
369 }
370 }
371}
372
373/// The kind of a region.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
375pub enum RegionKind {
376 /// Data flow region.
377 #[default]
378 DataFlow = 0,
379 /// Control flow region.
380 ControlFlow = 1,
381 /// Module region.
382 Module = 2,
383}
384
385#[cfg(feature = "pyo3")]
386impl<'py> pyo3::FromPyObject<'py> for RegionKind {
387 fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
388 let value: usize = ob.getattr("value")?.extract()?;
389 match value {
390 0 => Ok(Self::DataFlow),
391 1 => Ok(Self::ControlFlow),
392 2 => Ok(Self::Module),
393 _ => Err(pyo3::exceptions::PyTypeError::new_err(
394 "Invalid RegionKind.",
395 )),
396 }
397 }
398}
399
400#[cfg(feature = "pyo3")]
401impl<'py> pyo3::IntoPyObject<'py> for RegionKind {
402 type Target = pyo3::PyAny;
403 type Output = pyo3::Bound<'py, Self::Target>;
404 type Error = pyo3::PyErr;
405
406 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
407 let py_module = py.import("hugr.model")?;
408 let py_class = py_module.getattr("RegionKind")?;
409
410 match self {
411 RegionKind::DataFlow => py_class.getattr("DATA_FLOW"),
412 RegionKind::ControlFlow => py_class.getattr("CONTROL_FLOW"),
413 RegionKind::Module => py_class.getattr("MODULE"),
414 }
415 }
416}
417
418/// The name of a variable.
419#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
420pub struct VarName(SmolStr);
421
422impl VarName {
423 /// Create a new variable name.
424 pub fn new(name: impl Into<SmolStr>) -> Self {
425 Self(name.into())
426 }
427}
428
429impl AsRef<str> for VarName {
430 fn as_ref(&self) -> &str {
431 self.0.as_ref()
432 }
433}
434
435#[cfg(feature = "pyo3")]
436impl<'py> pyo3::FromPyObject<'py> for VarName {
437 fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
438 let name: String = ob.extract()?;
439 Ok(Self::new(name))
440 }
441}
442
443#[cfg(feature = "pyo3")]
444impl<'py> pyo3::IntoPyObject<'py> for &VarName {
445 type Target = pyo3::types::PyString;
446 type Output = pyo3::Bound<'py, Self::Target>;
447 type Error = pyo3::PyErr;
448
449 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
450 Ok(self.as_ref().into_pyobject(py)?)
451 }
452}
453
454/// The name of a symbol.
455#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
456pub struct SymbolName(SmolStr);
457
458impl SymbolName {
459 /// Create a new symbol name.
460 pub fn new(name: impl Into<SmolStr>) -> Self {
461 Self(name.into())
462 }
463}
464
465impl AsRef<str> for SymbolName {
466 fn as_ref(&self) -> &str {
467 self.0.as_ref()
468 }
469}
470
471#[cfg(feature = "pyo3")]
472impl<'py> pyo3::FromPyObject<'py> for SymbolName {
473 fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
474 let name: String = ob.extract()?;
475 Ok(Self::new(name))
476 }
477}
478
479/// The name of a link.
480#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
481pub struct LinkName(SmolStr);
482
483impl LinkName {
484 /// Create a new link name.
485 pub fn new(name: impl Into<SmolStr>) -> Self {
486 Self(name.into())
487 }
488
489 /// Create a new link name from a link index.
490 #[must_use]
491 pub fn new_index(index: LinkIndex) -> Self {
492 // TODO: Should named and numbered links have different namespaces?
493 Self(format!("{index}").into())
494 }
495}
496
497impl AsRef<str> for LinkName {
498 fn as_ref(&self) -> &str {
499 self.0.as_ref()
500 }
501}
502
503#[cfg(feature = "pyo3")]
504impl<'py> pyo3::FromPyObject<'py> for LinkName {
505 fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
506 let name: String = ob.extract()?;
507 Ok(Self::new(name))
508 }
509}
510
511#[cfg(feature = "pyo3")]
512impl<'py> pyo3::IntoPyObject<'py> for &LinkName {
513 type Target = pyo3::types::PyString;
514 type Output = pyo3::Bound<'py, Self::Target>;
515 type Error = pyo3::PyErr;
516
517 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
518 Ok(self.as_ref().into_pyobject(py)?)
519 }
520}
521
522/// A static literal value.
523///
524/// Literal values may be large since they can include strings and byte
525/// sequences of arbitrary length. To enable cheap cloning and sharing,
526/// strings and byte sequences use reference counting.
527#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
528pub enum Literal {
529 /// String literal.
530 Str(SmolStr),
531 /// Natural number literal (unsigned 64 bit).
532 Nat(u64),
533 /// Byte sequence literal.
534 Bytes(Arc<[u8]>),
535 /// Floating point literal
536 Float(OrderedFloat<f64>),
537}
538
539#[cfg(feature = "pyo3")]
540impl<'py> pyo3::FromPyObject<'py> for Literal {
541 fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
542 if pyo3::types::PyString::is_type_of(ob) {
543 let value: String = ob.extract()?;
544 Ok(Literal::Str(value.into()))
545 } else if pyo3::types::PyInt::is_type_of(ob) {
546 let value: u64 = ob.extract()?;
547 Ok(Literal::Nat(value))
548 } else if pyo3::types::PyFloat::is_type_of(ob) {
549 let value: f64 = ob.extract()?;
550 Ok(Literal::Float(value.into()))
551 } else if pyo3::types::PyBytes::is_type_of(ob) {
552 let value: Vec<u8> = ob.extract()?;
553 Ok(Literal::Bytes(value.into()))
554 } else {
555 Err(pyo3::exceptions::PyTypeError::new_err(
556 "Invalid literal value.",
557 ))
558 }
559 }
560}
561
562#[cfg(feature = "pyo3")]
563impl<'py> pyo3::IntoPyObject<'py> for &Literal {
564 type Target = pyo3::PyAny;
565 type Output = pyo3::Bound<'py, Self::Target>;
566 type Error = pyo3::PyErr;
567
568 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
569 Ok(match self {
570 Literal::Str(s) => s.as_str().into_pyobject(py)?.into_any(),
571 Literal::Nat(n) => n.into_pyobject(py)?.into_any(),
572 Literal::Bytes(b) => pyo3::types::PyBytes::new(py, b)
573 .into_pyobject(py)?
574 .into_any(),
575 Literal::Float(f) => f.0.into_pyobject(py)?.into_any(),
576 })
577 }
578}
579
580#[cfg(test)]
581mod test {
582 use super::*;
583 use proptest::{prelude::*, string::string_regex};
584
585 impl Arbitrary for Literal {
586 type Parameters = ();
587 type Strategy = BoxedStrategy<Self>;
588
589 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
590 prop_oneof![
591 any::<String>().prop_map(|s| Literal::Str(s.into())),
592 any::<u64>().prop_map(Literal::Nat),
593 prop::collection::vec(any::<u8>(), 0..100).prop_map(|v| Literal::Bytes(v.into())),
594 any::<f64>().prop_map(|f| Literal::Float(OrderedFloat(f)))
595 ]
596 .boxed()
597 }
598 }
599
600 impl Arbitrary for SymbolName {
601 type Parameters = ();
602 type Strategy = BoxedStrategy<Self>;
603
604 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
605 string_regex(r"[a-zA-Z\-_][0-9a-zA-Z\-_](\.[a-zA-Z\-_][0-9a-zA-Z\-_])*")
606 .unwrap()
607 .prop_map(Self::new)
608 .boxed()
609 }
610 }
611
612 impl Arbitrary for VarName {
613 type Parameters = ();
614 type Strategy = BoxedStrategy<Self>;
615
616 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
617 string_regex(r"[a-zA-Z\-_][0-9a-zA-Z\-_]")
618 .unwrap()
619 .prop_map(Self::new)
620 .boxed()
621 }
622 }
623
624 proptest! {
625 #[test]
626 fn test_literal_text(lit: Literal) {
627 let text = lit.to_string();
628 let parsed: Literal = text.parse().unwrap();
629 assert_eq!(lit, parsed);
630 }
631 }
632}