This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

DataCanon

DataCanon is a data model for structured scientific data with deterministic, language-independent identity.

Scientific data often outlives the software that created it. A calculation may move between programming languages, be stored in several formats, and remain in use through years of development. DataCanon defines what the data contains and when two instances are equal, independently of how it is stored or displayed.

Each DataCanon Instance (DCI) belongs to exactly one DataCanon Type (DCT). Its type and information determine its identity. Every valid DCI has one canonical byte encoding; equal DCIs have identical encodings across conforming implementations. SHA-256 of those bytes provides a deterministic fingerprint.

Store, recover, and compare data

The Python reference implementation constructs DCIs from instances of supported conventional types, writes the DCIs to disk, and reconstructs them with their DataCanon identity intact. Conventional types include built-in types, library types, and user-defined types used directly for data and computation. Canonical binary files (.dcb) contain the complete canonical encoding.

Human-readable Shorthand, YAML interchange, and HDF5 provide other representations of the same data. Shorthand can mix concise mathematical and collection notation with explicit constructors. Its longhand form uses constructors for structured values while retaining ordinary literals such as 1, 1.0, and "hello". HDF5 stores Arrays as native numerical datasets and can embed externally referenced payloads.

Text layout and storage choices do not determine DCI identity. A change from a List to a Tuple does: those are different DCTs even when their items agree.

Rigidly structured scientific data

DataCanon supports exact numerical structure alongside finite machine numbers. Fractions, Sums, Products, OrderedProducts, Powers, Quotients, and Complex values can represent quantities such as

1/2 * sqrt(3)
quotient(1, sqrt(3) + sqrt(2))
complex(1/2, 1/2 * sqrt(3))

A Fraction and a Power within a Product can exactly represent the irrational number in the first expression. The second preserves a structured numerator and denominator. Complex components retain their individual DCTs.

Lists, Tuples, Sets, Multisets, ordered and unordered Mappings, and named Records provide different ways to organize data. Multisets preserve repeated members without assigning semantic order. Arrays retain their portable type, shape, and exact finite element representations, including signed zero.

Bytes contains an opaque byte sequence directly in a DCI. Applications can place interpretation metadata alongside it in a Record. ExternalFile and ExternalArray instead preserve a relative path and content digest. Their metadata can be read without retrieving the external bytes; retrieval and integrity verification are separate operations.

Identity and conventional types

DataCanon defines structured data, not mathematical equivalence. Integer 1 and Float 1.0 have different identities. Numerical nesting, repeated terms, and the constituent information prescribed by each DCT are preserved.

Conversion interfaces connect DCIs with instances of conventional types in a given programming language. Python examples include list, Fraction, and NumPy arrays. A List DCI represented by a ListDC object and a conventional list object belong to different types with different equality and operation rules. Both are Python objects.

Where suitable conventional types are missing, the Python implementation supplies additional types such as Power, Sum, and Multiset. The numerical conventional types support arithmetic independently of DCI construction and persistence. Their operations can simplify structure; conversion through conventional objects is not guaranteed to retain an arbitrary DCI’s identity. The DataCanon data model defines no such arithmetic operations.

Optional Shorthand preprocessing supplies compact Lists, textual substitutions, and explicit scalar eval(...) before strict parsing. It is an input facility, not an extension of DCI identity.

Using this documentation

  • Getting Started: construct data, write binary files, and explore the other representations.
  • Guides: conventional types, command-line operations, and Shorthand preprocessing, explained step by step.
  • Python API: conversion tables, conventional types, rendering, and persistence.
  • Reference: the 24 DCTs, their structure, and representation choices.
  • Specifications: seven Version 1 documents, including the optional Conventional Types companion.

Version 1 remains a draft. Outstanding interoperability and conformance items are recorded in the individual specifications.

1 - Getting Started

Scientific data moves between programs, readable input files, and storage formats. A different file layout need not mean different data; a changed Array type or coordinate structure can mean different data even when the numbers look the same.

DataCanon gives structured scientific data a precisely defined identity, independent of its programming language or storage representation. Each DataCanon Instance (DCI) has one unique canonical byte encoding. A fingerprint derived from those bytes lets you identify the data across representations.

This guide constructs a dataset, identifies it, stores and inspects it, then shows how to author scientific input directly.

Installation

Clone the DataCanon code repository, then run these commands from its root:

python3 -m venv DC
source DC/bin/activate
python -m pip install .

The Makefile also provides environment setup, editable installation, and tests.

Construct and identify a DCI

Start with a Python dictionary containing a Tuple key, a List of species, a coordinate Tuple, and a numerical Array:

import numpy as np

from data_canon import as_dc_instance, fingerprint

data = {
    ("Eg", 0): 2.37,
    "species": ["Sr", "Ti", "O"],
    "origin": (0.0, 0.0, 0.0),
    "lattice": np.eye(3, dtype="float64"),
}
instance = as_dc_instance(data)
identifier = fingerprint(instance)
print(identifier)

Output:

eb8f306c801ca4452e283f052b51a6834a846399cc3aac43720fb7cf48faeca4

The whole dataset is one DCI: a Mapping containing other DCIs. Mapping keys can be structured values, including the Tuple ("Eg", 0).

Python’s dict, tuple, and NumPy arrays are conventional types used by the application. Conversion constructs a DCI whose information and equality rules are defined by DataCanon, independently of those Python types.

The fingerprint is SHA-256 of the DCI’s canonical bytes, written as 64 lowercase hexadecimal characters. You can record it alongside a calculation to identify its input data. It is derived from the data’s defined identity, not from the memory address of a Python object or the layout of a text file.

Type participates in identity. Replacing the origin Tuple with a List changes the DCI even though its coordinates have the same numerical values:

changed_data = dict(data)
changed_data["origin"] = [0.0, 0.0, 0.0]
changed_instance = as_dc_instance(changed_data)

assert changed_instance != instance
assert fingerprint(changed_instance) != identifier

Array type and shape also participate in identity. Equal DCIs have identical canonical bytes and fingerprints; DCI equality itself is defined by the data model, not by comparing fingerprints.

Write a DCI to disk as binary data

Use .dcb to store the canonical bytes directly:

from data_canon import read, write

write(instance, filename="output_data.dcb")
restored = read("output_data.dcb")

assert restored == instance
assert fingerprint(restored) == identifier

The file is binary, not text. Reading reconstructs the same DCI, including its types, Array shape, and element values. DataCanon decoding reconstructs DCIs without invoking application-defined code.

Render a DCI as Shorthand

Shorthand is a human-readable representation of a DCI:

from data_canon import parse_shorthand, render_shorthand

text = render_shorthand(instance)
print(text)

Output:

{
  ( "Eg", 0 ): 2.37,
  "species": [ "Sr", "Ti", "O" ],
  "origin": ( 0.0, 0.0, 0.0 ),
  "lattice":
    array(
      [[1.0, 0.0, 0.0],
       [0.0, 1.0, 0.0],
       [0.0, 0.0, 1.0]]
    )
}

Parsing the text recovers the same identity:

parsed = parse_shorthand(text)
assert parsed == instance
assert fingerprint(parsed) == identifier

Text layout is presentation. Canonical bytes determine the fingerprint, so spacing does not become part of the data’s identity.

Inspect a DCI via the command line

Installing the package also installs dc. With the environment active, display the binary file as the Shorthand shown above:

dc output_data.dcb

Inspect particular values without writing a Python script:

dc output_data.dcb --get '("Eg", 0)'
dc output_data.dcb --get species --index 0

Output:

2.37
"Sr"

Print the same fingerprint calculated in Python:

dc output_data.dcb --fingerprint

The CLI guide covers searches, selection, and format transfers in more detail.

Preserve identity through YAML and HDF5

The same DCI can be represented in other formats without changing its identity. YAML uses native values where faithful and a $ marker for embedded strict Shorthand, including Tuple keys and Array values:

from data_canon import parse_yaml, render_yaml

yaml_text = render_yaml(instance)
print(yaml_text)

Output:

$ ( "Eg", 0 ): 2.37
species:
- Sr
- Ti
- O
origin: $ ( 0.0, 0.0, 0.0 )
lattice: |-
  $
  array(
    [[1.0, 0.0, 0.0],
     [0.0, 1.0, 0.0],
     [0.0, 0.0, 1.0]]
  )
from_yaml = parse_yaml(yaml_text)
assert from_yaml == instance
assert fingerprint(from_yaml) == identifier

For numerical storage, HDF5 represents Arrays as native numerical datasets:

write(instance, filename="output_data.h5")
from_hdf5 = read("output_data.h5")
assert from_hdf5 == instance
assert fingerprint(from_hdf5) == identifier

Use .dcs with the same write() and read() functions to store Shorthand text. The .dcb, Shorthand, YAML, and HDF5 representations recover equal DCIs and therefore the same canonical bytes and fingerprint. Hashing the YAML or HDF5 file bytes directly would instead identify that particular file representation.

Author scientific data directly

So far, we have constructed data in Python. DataCanon also lets us author structured scientific data directly in Shorthand:

scientific_input = parse_shorthand("""
{
  "position": (1/2, 1/4, 0),
  "phase": complex(1/2, 1/2 * sqrt(3))
}
""")

The position contains exact Fractions. The Product 1/2 * sqrt(3) exactly represents an irrational quantity, and the Complex retains the structure of its real and imaginary components.

DataCanon does not do math. Parsing constructs and preserves Fraction, Power, Product, and Complex DCIs. Applications can convert DCIs to instances of conventional types for computation; DC identity belongs to the stored data structure.

Make scientific input easier to maintain

Optional Shorthand preprocessing lets an input file use named parameters. An excerpt of a crystal-structure input illustrates the idea: a parameter controls several coordinates while exact reference positions remain visible.

source = """
u = 0.305 |
{
  "basis_atoms": {
    "O": [
      (u,        u,        0),
      (1/2 +  u, 1/2 + -u, 1/2)
    ]
  }
}
"""

authored = parse_shorthand(source, preprocess=True)
write(authored, filename="structure.dcb")

Preprocessing substitutes u before strict parsing. The resulting DCI stores the coordinate values and their numerical structure; the parameter definition is an input convenience. The same identity and persistence operations now apply to this authored data.

The CLI enables preprocessing for .dcs input files. The Shorthand Preprocessing guide develops substitutions, compact Lists, and explicit eval(...) separately, and includes a complete crystal-structure example.

Next steps

2 - Guides

The guides develop individual features step by step:

  • Identity and Fingerprinting: identify calculation inputs, distinguish data changes from presentation changes, and associate results with their inputs.
  • Persistence and External Resources: write complete packages, retrieve verified payloads, and transfer compressed external Arrays between directories and HDF5.
  • Conventional Types: distinguish conventional objects from DCIs, use numerical and collection operations, and understand conversion.
  • Custom Conversion Rules: define application-owned Record conventions and recursively convert conventional objects, using dates as an example.
  • Command-Line Interface: inspect files, transfer formats, search nested mappings, and compute fingerprints.
  • Shorthand Preprocessing: compact Lists, substitutions, and scalar eval(...).

Start with Getting Started for binary persistence, a first conversion example, and the command-line interface. The Reference describes DCT structure and identity; conventional objects follow their own type-specific rules.

2.1 - Identity and Fingerprinting

A calculation’s inputs may appear in a readable text file, a binary archive, and several programming languages. Comparing the files tells you whether their bytes agree. DataCanon lets you ask a different question: do the representations describe the same structured data?

This guide identifies a calculation’s inputs, distinguishes changes in data from changes in presentation, and uses fingerprints to associate results with their inputs. Start with Getting Started for installation and the first persistence workflow.

Identify the inputs to a calculation

Consider an application that evaluates a response at several sampling points:

import numpy as np

from data_canon import as_dc_instance, canonical_encode, fingerprint

inputs = {
    "model": "response-v1",
    "temperature": 300.0,
    "sampling_points": np.array(
        [[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]],
        dtype="float64",
    ),
    ("solver", "tolerance"): 1e-8,
}
instance = as_dc_instance(inputs)
input_id = fingerprint(instance)

The DCI identifies the whole input structure: its keys, values, types, Array shape, and Mapping entry order. The fingerprint is SHA-256 of the canonical encoding, returned as 64 lowercase hexadecimal characters.

Canonical encoding preserves all distinctions in DCI identity. Equal DCIs have identical canonical bytes; unequal DCIs have different canonical bytes. SHA-256 compresses those bytes to a fixed-size identifier. Fingerprint equality is a practical identification mechanism based on collision resistance, not the definition of DCI equality.

Change the presentation without changing the data

Render the input using different layouts, then parse both versions:

from data_canon import parse_shorthand, render_shorthand

readable = render_shorthand(instance)
single_line = render_shorthand(instance, layout="inline")

first = parse_shorthand(readable)
second = parse_shorthand(single_line)

assert first == second == instance
assert canonical_encode(first) == canonical_encode(second)
assert fingerprint(first) == fingerprint(second) == input_id

Whitespace and layout belong to the text representation. They do not change the DCI. The same principle applies when the DCI is stored in HDF5 rather than Shorthand: the physical file bytes can differ while the reconstructed data has the same identity.

Hash a file’s bytes when you need to identify that exact file. Fingerprint its DCI when you need to identify its DataCanon data across representations.

Recognize changes that matter

A change in type can reveal an unintended change in a calculation pipeline:

changed_inputs = dict(inputs)
changed_inputs["sampling_points"] = inputs["sampling_points"].astype("float32")
changed = as_dc_instance(changed_inputs)

assert changed != instance
assert fingerprint(changed) != input_id

All coordinates in this example are exactly representable in both Array types. The type change still changes the DCI. DataCanon retains a distinction that a comparison of numerical values alone could miss.

Other identity rules follow each DCT’s contract:

ChangeEffect on identity
Replace a List with a Tuple containing the same itemsChanges identity
Reshape an Array while keeping the same element streamChanges identity
Reorder Mapping entries or Record fieldsChanges identity
Reorder Set members or UnorderedMapping entriesPreserves identity
Replace positive Float zero with negative Float zeroChanges identity
Change a relative path in an external referenceChanges identity
Move a complete package to another directoryPreserves identity

Choose the DCT that expresses the application’s intended distinctions. If association order should not matter, an UnorderedMapping expresses that choice explicitly. The Reference summarizes all DCT identity rules.

Choose what the identifier covers

An input identifier covers exactly the information placed in its DCI. Adding a run timestamp changes that DCI even when the calculation inputs stay the same. Keep operational metadata separate when it should not affect input identity:

run_record = {
    "input_fingerprint": input_id,
    "inputs": instance,
    "operator_note": "Convergence study, second run",
}
run_instance = as_dc_instance(run_record)

The input DCI remains available unchanged inside the run record. The run record has its own identity, which also covers the note.

Application conventions determine what is required to reproduce a calculation. Include a model version, solver settings, or other dependencies when they affect the result. DataCanon cannot infer missing information or decide that two different parameterizations describe the same physical system. An application can perform its own normalization before constructing a DCI when it wants such equivalences to share an identifier.

Records provide named structures for application conventions. See Custom Conversion Rules for mapping an application’s classes to explicit Record structures.

Associate results with their inputs

A fingerprint is useful as a lookup key for a result cache or provenance record:

results_by_input = {
    input_id: {"response": 12.5, "converged": True},
}
result = results_by_input[fingerprint(instance)]
assert result["converged"]

For a cache to be valid, its key must cover every dependency that can affect the cached result. If a code version or random seed matters, include it in the identified inputs or in a larger cache-key DCI.

Within one Python process, DCIs can also be dictionary keys directly:

point = as_dc_instance(np.array([0.5, 0.0, 0.0], dtype="float64"))
responses = {point: 12.5}
same_point = as_dc_instance(np.array([0.5, 0.0, 0.0], dtype="float64"))
assert responses[same_point] == 12.5

All DCIs are Python-hashable, including Array and List DCIs. Python’s hash() supports dictionary lookup; use fingerprint() for a portable identifier to store or communicate between processes and implementations. Preserve DCI immutability when using a DCI as a key.

External content has a separate integrity check

An ExternalFile or ExternalArray DCI includes a relative path and a declared content digest. Its fingerprint covers that metadata. Constructing the DCI or computing its fingerprint does not retrieve the external bytes.

Materialization verifies the retrieved content against the declared digest. Missing or corrupt content prevents materialization without changing the metadata DCI’s identity. The Persistence and External Resources guide develops that workflow, including complete packages and cross-format transfer.

For the formal encoding and fingerprint contract, see DataCanon Core.

2.2 - Persistence and External Resources

A scientific dataset often combines a small description with a large numerical Array or an existing file. DataCanon can store the data together or preserve an explicit reference to external content. The DCI retains its identity as a complete dataset moves between a directory and an HDF5 container.

This guide constructs a dataset with an external Array, writes a complete package, retrieves its numerical contents, and transfers the package. The Identity and Fingerprinting guide explains what its identifier covers.

Decide what belongs inside the DCI

An Array DCI contains its numerical elements. An ExternalArray DCI contains the information needed to identify and interpret an external element stream: a relative path, portable Array type, shape, and SHA-256 content digest.

RequirementDCT
Keep numerical elements inside the DCIArray
Refer to a separately stored numerical element streamExternalArray
Refer to an opaque file’s exact bytesExternalFile
Keep a small opaque byte sequence inside the DCIBytes

Array and ExternalArray are distinct DCTs. Converting an Array into an external reference changes the DCI. Moving an existing ExternalArray’s payload between supported storage representations can preserve its identity.

Construct a dataset with an external Array

Force-constant matrices provide a useful example: a calculation may produce a large matrix with many zeros, accompanied by relatively small descriptive data. Use a small matrix here to keep the example easy to run:

import numpy as np

from data_canon import ExternalArray, as_dc_instance, fingerprint

forces = np.zeros((32, 32), dtype="float64")
forces[0, 0] = 2.5
forces[0, 1] = -0.0

dataset = as_dc_instance({
    "description": "Force-constant example",
    "force_constants": ExternalArray(
        forces,
        filename="forces",
        compression=True,
    ),
})
dataset_id = fingerprint(dataset)

The conventional ExternalArray object supplies the numerical data during conversion. The resulting ExternalArray DCI records the path ("external_dc", "forces.bin.gz"), type float64, shape (32, 32), and content digest. The implementation retains the supplied data as a nonsemantic cache so the writer can persist it.

The content digest covers the uncompressed canonical Array element bytes. Those bytes contain only elements: no type, shape, or DCI header. A .bin or .bin.gz payload is therefore different from a .dcb file, which contains one complete canonical DCI encoding.

Write a complete directory package

from pathlib import Path
from data_canon import read, write

source = Path("force_example") / "dataset.dcs"
write(dataset, filename=source)

The writer creates the Shorthand representation at force_example/dataset.dcs and the compressed payload at force_example/external_dc/forces.bin.gz. Together, the representation and all referenced payloads form a complete package.

Use dataset.dcb instead to store the DCI as canonical binary bytes. Both directory-backed formats resolve relative paths beneath the representation’s containing directory by default.

The current Python write() verifies and persists required resources. It does not expose a metadata-only export option. Missing resources or conflicting contents can cause a write to fail. An existing external file with conflicting contents is not silently overwritten.

Read metadata before retrieving content

restored = read(source)
assert restored == dataset
assert fingerprint(restored) == dataset_id

Reading reconstructs the DCI hierarchy without materializing external payloads. The dataset’s metadata can be read even if the payload is unavailable. The reader retains the representation’s source location as nonsemantic context for later retrieval.

Converting the restored dataset to conventional objects requests the external Array’s numerical contents:

from data_canon import dc_instance_to_python

conventional = dc_instance_to_python(restored)
loaded_forces = conventional["force_constants"]
assert np.array_equal(loaded_forces, forces)
assert np.signbit(loaded_forces[0, 1])

Materialization decompresses the payload, verifies its digest, and validates its type, shape, required byte length, and element validity. It returns a NumPy array; the original DCI remains an ExternalArray DCI.

A missing file, malformed compressed stream, incorrect digest, or invalid element stream causes retrieval to fail. The metadata DCI and its fingerprint remain unchanged.

Transfer the package to HDF5

container = Path("force_example") / "dataset.h5"
write(restored, filename=container)

embedded = read(container)
assert embedded == dataset
assert fingerprint(embedded) == dataset_id

The HDF5 writer retrieves and verifies the source payload before embedding it. The container holds the DCI hierarchy and an embedded payload collection, so the complete package can travel as one file. External references retain their paths and digests; they are not converted into Array DCIs.

Reading the HDF5 hierarchy still does not materialize its external Arrays. Subsequent conversion retrieves the embedded payload by its complete relative path and verifies it.

Export the package back to a directory:

destination = Path("force_export") / "dataset.dcb"
write(embedded, filename=destination)

assert (destination.parent / "external_dc" / "forces.bin.gz").exists()
assert read(destination) == dataset

The writer recreates the compressed external file using the same relative path. The Shorthand, HDF5, and binary representations recover equal DCIs and therefore share a fingerprint. Their physical file bytes need not agree.

Understand compression and identity

ExternalArray(..., compression=True) selects .bin.gz storage. With compression=False, the default, a filename stem receives .bin instead. Explicit .bin and .bin.gz suffixes must match the compression setting.

In a directory, .bin.gz stores a gzip stream. In HDF5, the same reference uses a gzip-compressed dataset containing the logical element bytes. The HDF5 library handles dataset decompression; the implementation does not interpret those returned bytes as another gzip file.

Changing forces.bin to forces.bin.gz changes the path and therefore changes DCI identity. Once the path is chosen, transferring that reference between directory and HDF5 storage preserves it. Compression does not change the content digest’s definition.

The option applies to ExternalArrays. It does not automatically compress every Array or ExternalFile, or introduce support for .dcb.gz representations.

Keep opaque files opaque

Use an ExternalFile when the payload is a file whose interpretation belongs to the application:

from data_canon import ExternalFile

notes = as_dc_instance(ExternalFile.from_bytes(
    b"Calibration instrument: A17\n",
    filename="calibration.txt",
))
write(notes, filename=Path("force_example") / "calibration.dcb")

An ExternalFile digest covers the file’s exact bytes. A .gz filename does not cause DataCanon to reinterpret that digest as a digest of decompressed content. The Python conversion interface can apply a filename-based content loader after verification; for example, .txt content becomes text. Such interpretation is separate from the external DCI’s identity.

Move resources without changing references

Move a directory package with its relative resource tree intact. Moving only dataset.dcs or dataset.dcb may leave a valid metadata representation whose payloads cannot be found in the new directory.

If a DCI was decoded without a source location, supply the namespace explicitly when converting it:

from data_canon import canonical_decode, canonical_encode

detached = canonical_decode(canonical_encode(dataset))
recovered = dc_instance_to_python(detached, source=source.parent)
assert np.array_equal(recovered["force_constants"], forces)

The source selects where to look. The DCI still determines which bytes are acceptable through its digest and Array metadata. Source context never changes equality or fingerprinting.

Within a complete package, references to the same path must agree on one shared payload. Different paths remain distinct locations, even when their payloads agree. Writers reject conflicting references rather than rename paths or select one payload silently.

For method signatures, see the Python API. The Persistence specification and HDF5 specification define the portable requirements for independent implementations.

2.3 - Conventional Types

DataCanon implementations connect DataCanon Instances (DCIs) with instances of conventional types in a programming language. Conventional types include built-in types, types provided by libraries, and user-defined types used directly for data and computation. Python examples include list, fractions.Fraction, and numpy.ndarray.

A DataCanon Type (DCT) defines information, invariants, and equality. A DCI is an instance of a DCT. The Python implementation represents DCIs using classes such as ListDC and FractionDC. A ListDC object and a conventional list object are both Python objects, but belong to different types with different rules. Conversion connects the types; it does not make their semantics identical.

The examples use Python. Implementations in Julia, Rust, C, or another language can define conversions involving conventional types from their own language and libraries. The DataCanon DCTs and DCI equality rules remain independent of those choices.

The optional Conventional Types specification defines common construction, arithmetic, comparison, and collection requirements for independent implementations. Support is declared per conventional type; Core conformance does not require conventional arithmetic. The guide introduces the Python binding, including its interfaces and conversion choices.

Distinguish DCTs, their implementations, and conventional types

DCTPython class implementing the DCTExamples of associated conventional types in Python
IntegerIntegerDCint
FloatFloatDCfloat
ListListDClist
TupleTupleDCtuple
FractionFractionDCfractions.Fraction
ArrayArrayDCnumpy.ndarray
ComplexComplexDCcomplex, ComplexNumber

The table identifies conversion relationships, not type equivalences or a universal one-to-one correspondence. For example, both built-in complex and ComplexNumber objects can be converted to Complex DCIs, but their component information differs. The Python API specifies the conversion directions and resulting types.

Exact named mathematical constants

The conventional Constant type preserves a named mathematical constant. Pi and E are instances representing pi and Euler’s number; neither stores a decimal approximation. Conventional arithmetic retains the constant:

import math
from data_canon import Constant, Pi, E, Sum, Product, as_dc_instance, render_shorthand

assert Pi == Constant("pi")
assert Sum(Pi, Pi) == Product(2, Pi)
assert Pi / Pi == 1
assert Pi != math.pi
assert float(E) == math.e

instance = as_dc_instance(2 * Pi)
assert render_shorthand(instance, padding=0) == "2 * Pi"

float(Pi) explicitly requests an approximation. The corresponding Constant DCI preserves its name, and its shorthand spelling is Pi (E for Euler’s number). Longhand uses constant("pi") or constant("e"). Physical constants and user-defined names are not included.

Begin with a List DCI and a conventional list

Construct a List DCI from a conventional list, then convert the DCI to a new list object:

from data_canon import as_dc_instance, dc_instance_to_python
from data_canon.instance import ListDC

source = [1, 2, 3]
instance = as_dc_instance(source)
converted = dc_instance_to_python(instance)

assert isinstance(instance, ListDC)
assert type(converted) is list
assert converted == source

Both instance and converted are Python objects. The function constructs an instance of the conventional list type from the List DCI represented by instance.

The difference affects equality:

assert [1] == [1.0]
assert as_dc_instance([1]) != as_dc_instance([1.0])

Conventional lists use the equality rules of their members. List DCIs compare the identities of their constituent DCIs, so Integer and Float constituents remain distinct. Conversion must be understood in terms of both types’ rules.

Use existing conventional types

Most scientific data already has useful conventional types in Python. Use built-in scalars and containers, Fraction for rational numbers, and NumPy arrays for machine data:

from fractions import Fraction
import numpy as np
from data_canon.instance import MappingDC

measurements = {
    ("Eg", 0): 2.37,
    "position": (Fraction(1, 2), Fraction(1, 4), 0.199),
    "lattice_vectors": np.eye(3, dtype="float64"),
}
measurement_instance = as_dc_instance(measurements)
recovered = dc_instance_to_python(measurement_instance)

assert isinstance(measurement_instance, MappingDC)
assert type(recovered) is dict
assert recovered[("Eg", 0)] == 2.37
assert type(recovered["position"]) is tuple
assert isinstance(recovered["position"][0], Fraction)
assert isinstance(recovered["lattice_vectors"], np.ndarray)

The Tuple key is a DCI within the Mapping DCI and a conventional tuple within the recovered dictionary. Array type and shape are preserved by the specified Array conversion. The returned NumPy array is read-only; .copy() creates an independently mutable array. Mutable buffers or unsupported numerical types may require explicit conversion before they can be represented by a DCI.

as_dc_instance() accepts an existing DCI too, returning that instance unchanged. The identity case does not eliminate the distinction between a DCI and a conventional object of another type.

NumPy scalar values

NumPy scalars convert by value to the corresponding scalar DCTs:

NumPy scalarResulting DCT
bool_Boolean
Signed and unsigned integer scalarsInteger, without a precision limit
Finite float16, float32, float64Float, preserving the value and sign of zero
Finite complex64, complex128Complex, with Float real and imaginary components
str_String
bytes_Bytes

For example:

from data_canon.instance import FloatDC, IntegerDC

assert as_dc_instance(np.uint64(2**64 - 1)) == IntegerDC(2**64 - 1)
assert as_dc_instance(np.float32(0.5)) == FloatDC(0.5)

Binary16 and binary32 values widen exactly to binary64. Scalar conversions do not preserve the original NumPy width as identity metadata. For a numerical element whose type must be preserved, use a rank-zero Array:

from data_canon.instance import ArrayDC

scalar = as_dc_instance(np.float32(0.5))
typed_scalar = as_dc_instance(np.array(0.5, dtype="float32"))
assert isinstance(scalar, FloatDC)
assert isinstance(typed_scalar, ArrayDC)
assert typed_scalar.data.shape == ()
assert typed_scalar.data.dtype == np.dtype("float32")

NaN and infinities remain invalid numerical DCIs. Extended-precision scalar formats, such as a platform’s wider longdouble or clongdouble, require an explicit application conversion or custom registry rule. A platform alias of a supported portable dtype follows that portable dtype’s rule.

NumPy datetime64, timedelta64, and structured or opaque void scalars have no default conversion. Their units or field structure must be handled explicitly, for example by a registered conversion to a Record. A duration never implicitly becomes a unitless Integer. Exact-type registry rules take precedence over the default NumPy conversions and apply recursively in collections and Mapping keys.

Supply conventional types where suitable ones are missing

A language may lack a conventional type for a structure that DataCanon can represent. The Python implementation supplies Power, Sum, Product, Quotient, ComplexNumber, Multiset, and UnorderedDict for such cases. The additional classes are conventional types, just like types provided by other libraries; they are distinct from PowerDC, SumDC, and the other classes implementing DCTs.

DataCanon defines structured data and its identity; it does not define mathematical operations on that data. Working with structured numbers naturally creates a need to calculate with them. The additional numerical conventional types therefore support arithmetic under their own construction, equality, and simplification rules, similar in spirit to Python’s Fraction.

The arithmetic examples below construct conventional objects directly. No Shorthand parser, DCI construction, or persistence operation is needed to use Power, Sum, or the other numerical conventional types. Arithmetic operations on those objects do not evaluate or simplify stored DCIs.

Arithmetic with structured numbers

Retain a radical exactly

Construct a conventional Power object with a base and a positive rational exponent:

from data_canon import Power

sqrt_two = Power(2, 1, 2)
sqrt_three = Power(3, 1, 2)
cube_root_five = Power(5, 1, 3)

Power(3, 1, 2) represents three raised to the power one half. Its base remains a conventional int object here. Omitting the denominator gives an integer exponent: Power(3, 2) means three squared.

Conventional Power reduces the exponent fraction and extracts an exact root of the base when that makes the exponent an integer. Construction retains the Power type and the remaining integer exponent:

assert Power(5, 2, 4) == Power(5, 1, 2)
root_four = Power(4, 1, 2)
assert type(root_four) is Power
assert (root_four.base, root_four.numerator, root_four.denominator) == (2, 1, 1)
assert root_four == 2
assert Power(8, 1, 3) == 2

cubed = Power(16, 3, 2)
assert (cubed.base, cubed.numerator, cubed.denominator) == (4, 3, 1)

integer_power = Power(4, 8)
assert (integer_power.base, integer_power.numerator, integer_power.denominator) == (4, 8, 1)
assert integer_power == 65536
assert int(integer_power) == 65536

An integer exponent is preserved, rather than expanded merely to initialize the object. Equality and explicit int() or float() conversion can still compute its value. If the requested root is not exact, the constructor retains the base and reduced exponent: Power(8, 1, 2) keeps those fields.

Float bases follow the same rule, with an exactness check on the replacement float. For example, Power(4.0, 1, 2) becomes Power(2.0, 1, 1) and Power(16.0, 3, 2) becomes Power(4.0, 3, 1). Integer powers such as Power(0.1, 2) remain powers; construction introduces no floating-point rounding.

The behavior follows the model of Fraction: construction retains the requested conventional type, while equality can identify an equivalent scalar. A Power DCI has different rules and retains its original base and exponent constituents.

Calculate with exact geometric quantities

For an equilateral triangle of side length one, retain the exact height and area using conventional Fraction and Power objects:

from fractions import Fraction

height = Fraction(1, 2) * sqrt_three
area = height / 2

assert area == Fraction(1, 4) * sqrt_three
assert height + height == sqrt_three

The multiplication produces a conventional Product object. Division by a conventional int adjusts the rational factor; adding the two identical heights combines their factors. No decimal approximation of the square root is needed.

Add terms and combine matching contributions

Python operators construct conventional Sum and Product objects:

from data_canon import Sum

value = sqrt_two + sqrt_three + 3 * sqrt_three
assert value == Sum(sqrt_two, 4 * sqrt_three)
assert sqrt_three - sqrt_three == 0

Conventional Sum combines matching terms and rational constants, and flattens nested Sum objects. Extractable radical factors are simplified before terms are collected:

assert Sum(Power(12, 1, 2), sqrt_three).items == (3 * sqrt_three,)
assert Sum(1, Power(4, 1, 2)).items == (3,)

Explicit constructors are useful when assembling a collection of terms:

terms = [sqrt_two, sqrt_three, 3 * sqrt_three]
assembled = Sum(*terms)
assert assembled == value

The constructor retains a conventional Sum object even if simplification leaves one term. Arithmetic operators may return that remaining term directly:

assert Sum(sqrt_three, 3 * sqrt_three) == Sum(4 * sqrt_three)
assert sqrt_three + 3 * sqrt_three == 4 * sqrt_three

Sum(*items) and Product(*items) take separate operands, rather than one iterable argument. Conventional Sum and Product objects are immutable and unhashable, independently of the hashable Sum and Product DCIs.

Multiply factors and combine powers with a common base

Conventional Product combines rational factors and combines exponents for matching bases:

from data_canon import Product

assert Product(Fraction(1, 2), Fraction(2, 3)) == Product(Fraction(1, 3))
assert Power(5, 1, 2) * Power(5, 1, 3) == Power(5, 5, 6)
assert sqrt_two * sqrt_three == Product(sqrt_two, sqrt_three)

The last product can retain its two factors while comparing equal to the square root of six. Surrounding arithmetic can evaluate an integer power and combine the result with other terms or factors. Retaining the exponent in a standalone Power does not prevent that simplification:

assert sqrt_two * sqrt_three == Power(6, 1, 2)
assert sqrt_three * sqrt_three == 3
assert Product(3, Power(4, 1, 2)).items == (6,)

Multiplication distributes over conventional Sum objects:

assert 2 * Sum(sqrt_two, sqrt_three) == Sum(2 * sqrt_two, 2 * sqrt_three)

The simplification rules support exact structured calculations while keeping the scope of simplification limited and predictable.

Divide by a structured denominator

A reciprocal of a sum needs more than a conventional Fraction. Quotient retains a structured numerator and denominator:

from data_canon import Quotient

denominator = sqrt_three + sqrt_two
reciprocal = 1 / denominator
assert reciprocal == Quotient(1, Sum(sqrt_three, sqrt_two))

The denominator remains a conventional Sum; construction does not rationalize it. Equality can nevertheless recognize the rationalized equivalent by exact cross multiplication:

assert reciprocal == sqrt_three - sqrt_two
assert Quotient(4, 6) == Fraction(2, 3)
assert Quotient(2 * sqrt_three, 4 * sqrt_three) == Fraction(1, 2)

The Quotient constructor reduces rational coefficients and cancels common factors. It retains the Quotient type even when the result equals a scalar. Division rejects denominators recognized as zero.

Division by a conventional int or Fraction adjusts exact rational factors, as in the triangle example. Division by a conventional float follows floating-point arithmetic.

Keep floating-point contributions separate

Exact rational and floating-point contributions retain their separate roles:

mixed = Sum(Fraction(1, 4), 0.125)
assert mixed.items == (Fraction(1, 4), 0.125)

with_offset = Sum(sqrt_three, 0.25, 0.5)
assert with_offset == Sum(sqrt_three, 0.75)

Conventional float contributions can combine with one another using Python floating-point arithmetic, with its usual rounding. Float contributions remain separate from exact rational coefficients and are not converted to large rational numbers. The numerical conventional types accept finite float values and reject bool values as numerical operands.

Compare numerical values exactly

Conventional numerical equality uses exact normalization and cross multiplication. Equality can hold across Sum, Product, Power, Quotient, int, Fraction, and float; it is independent of presentation order and does not require matching conventional types.

Float comparison uses the exact binary value already stored by the float, following Python’s Fraction behavior:

assert Quotient(1, 2) == 0.5
assert Quotient(1, 10) != 0.1
assert Sum(2**53 + 1) != float(2**53 + 1)
assert mixed == Fraction(3, 8)

Keeping float and rational contributions separate during construction does not require them to compare unequal. No rounded floating-point evaluation is used as an equality test. Exact comparison forms are cached when needed without changing the object’s displayed operands or its conversion to a DCI.

Normalization handles rational arithmetic, distributive expressions, rational radicals, and quotient cross multiplication. It is not a complete procedure for recognizing every mathematical equivalence. Fractional powers with nonrational bases remain symbolic. Integer factorization uses trial divisors through 1000 and extracts residual perfect powers through degree 64; a large unresolved factor stays intact. Comparisons can therefore miss equivalences requiring further factorization or algebraic identities. No numerical tolerance or floating-point fallback is used to guess equality.

Choose when to request a machine number

Use float() at the point where a numerical routine or display needs a machine floating-point result:

import math

assert math.isclose(float(area), math.sqrt(3) / 4)
assert math.isclose(float(reciprocal), 1 / (math.sqrt(3) + math.sqrt(2)))

The conventional numerical object remains unchanged. Fractional powers use the real-valued domain and rounding behavior of math.pow during explicit evaluation.

int() also evaluates, then truncates toward zero, following Python conventions:

assert float(Sum(1, 1.0)) == 2.0
assert int(Quotient(-7, 3)) == -2

Explicit numerical conversions can raise errors for undefined operations or overflow. Construction rejects an even root of a known negative scalar; odd roots of negative scalars use the real branch. A symbolic expression with a more complicated base may require further evaluation to determine its domain.

Use structured complex components

Use conventional ComplexNumber when real or imaginary components should retain Fraction, Power, Sum, or other conventional numerical objects:

from data_canon import ComplexNumber

phase = ComplexNumber(Fraction(1, 2), Fraction(1, 2) * sqrt_three)
shifted = phase + 1
assert shifted.real == Fraction(3, 2)
assert shifted.imaginary == phase.imaginary

Component equality follows the same numerical rules. For example, ComplexNumber(Power(4, 1, 2), 0) compares equal to 2. Comparisons with built-in complex values compare the actual component values. Conventional ComplexNumber objects are unhashable.

Addition, subtraction, multiplication, division, negation, and conjugation are available. For example, multiplication by the imaginary unit rotates the components:

rotated = phase * ComplexNumber(0, 1)
assert rotated.real == -phase.imaginary
assert rotated.imaginary == phase.real
assert phase.conjugate() == ComplexNumber(phase.real, -phase.imaginary)

Request a built-in Python complex number explicitly when needed:

machine_phase = complex(phase)
assert math.isclose(machine_phase.real, 0.5)
assert math.isclose(machine_phase.imag, math.sqrt(3) / 2)

Component operations follow their underlying Python types. In particular, division of two conventional int values or float operations can introduce floating-point results; use Fraction or structured numerical operands when exact rational division is required.

Array expressions and ordered multiplication

The conventional Sum class supports scalar terms and Array expressions. The conventional OrderedProduct class preserves factor order, while Product remains a scalar type. Construction preserves the expression; .evaluate() performs conventional arithmetic. For example, combine the Pauli matrices:

import numpy as np
from data_canon import Sum, OrderedProduct, Power, as_dc_instance

sigma_x = np.array([[0, 1], [1, 0]])
sigma_y = np.array([[0, -1j], [1j, 0]])
operator = Sum(OrderedProduct(2, sigma_x), OrderedProduct(3, sigma_y))
np.testing.assert_array_equal(np.asarray(operator, dtype=complex),
                              2 * sigma_x + 3 * sigma_y)
instance = as_dc_instance(operator)

Factor order matters in conventional matrix multiplication as well as DCI identity. OrderedProduct(A, B) and OrderedProduct(B, A) can give different results. Scalar factors may occur anywhere; NumPy array factors currently must have rank two when an OrderedProduct is evaluated. Conventional Sum supports NumPy arrays of any rank with exactly matching shapes. Neither operation broadcasts or reshapes data. Nonzero scalar terms mixed into an Array Sum require an explicit identity Array.

Exact coefficients are retained in object-array entries:

exact = OrderedProduct(Power(2, 1, 2), np.eye(2, dtype=int)).evaluate()
assert exact.dtype == object
assert exact[0, 0] == Power(2, 1, 2)

from fractions import Fraction
exact_y = OrderedProduct(Fraction(1, 3), sigma_y).evaluate()
assert exact_y[1, 0].imaginary == Fraction(1, 3)

Use an explicit dtype when a machine approximation is wanted. An exact object Array cannot itself be encoded as a portable Array DCI; use a supported machine dtype or nested Lists to preserve exact per-element DCTs. Scalar-valued fields such as Power bases and Complex components do not accept Array-valued Sums.

Additional conventional collection types

Retain repeated collection members

A conventional Multiset preserves the number of occurrences without assigning semantic order. For example, it can describe a composition:

from data_canon import Multiset

composition = Multiset(["Ti", "O", "O"])
assert composition == Multiset(["O", "Ti", "O"])
assert composition != Multiset(["Ti", "O"])

composition.add("O")
assert composition.count("O") == 3

Conventional Multiset is mutable. It accepts an iterable and supports list-like operations. Its members need not be hashable:

samples = Multiset([[1, 2], [1, 2]])
assert samples == Multiset([[1, 2], [1, 2]])

Member comparison follows Python equality, so members must provide usable Boolean equality results. This differs from DCI equality; for example, Python Boolean and integer equality can identify values that DataCanon distinguishes.

Mark an unordered dictionary

Python dict already compares equal dictionaries independently of insertion order. Its insertion order is nevertheless retained when converting it to a DataCanon Mapping. Use UnorderedDict to request an UnorderedMapping instead:

from data_canon import UnorderedDict

settings = UnorderedDict({"temperature": 300, "pressure": 0})
settings["temperature"] = 350
assert settings["temperature"] == 350

Conventional UnorderedDict behaves as a Python dictionary, with the same key hashability and equality rules. Its additional role is to express the intended DataCanon collection type during conversion.

Construct DCIs from the additional conventional objects

The arithmetic and collection examples above created instances of conventional types. Use the conversion interface when their results need DataCanon identity:

from data_canon.instance import ProductDC, MultisetDC, UnorderedMappingDC

area_instance = as_dc_instance(area)
assert isinstance(area_instance, ProductDC)
assert dc_instance_to_python(area_instance) == area
assert isinstance(as_dc_instance(composition), MultisetDC)
assert isinstance(as_dc_instance(settings), UnorderedMappingDC)

A conventional Power object follows the arithmetic implementation’s rules. A Power DCI retains its base DCI and prescribed exponent fields. Likewise, conventional Sum objects simplify matching terms, while Sum DCIs retain the specified numerical structure and multiplicity.

The conventional objects can be placed in a dictionary with Tuple keys and converted together:

from data_canon import render_shorthand

results = {
    ("triangle", "height"): height,
    ("triangle", "area"): area,
    "phase": phase,
    "composition": composition,
    "settings": settings,
}
result_instance = as_dc_instance(results)
print(render_shorthand(result_instance))
assert render_shorthand(results) == render_shorthand(result_instance)

render_shorthand() accepts conventional objects by constructing a DCI through the same conversion interface before rendering. The DCI’s identity is governed by its DCT, rather than by the original object’s equality rules.

Preserve the distinction during conversion

Conventional types have their own construction, equality, and operation rules. Conversion can simplify structure, lose distinctions, or fail when a target type cannot hold the required information. A round trip through conventional objects is not guaranteed to preserve an arbitrary DCI’s identity.

For example, converting a Sum DCI invokes the conventional Sum constructor:

from data_canon import parse_shorthand, Sum

original = parse_shorthand("sum(1, 1)")
calculated_sum = dc_instance_to_python(original)
assert calculated_sum == Sum(2)
assert as_dc_instance(calculated_sum) != original

No original DCI is modified by the conversion. The newly constructed Sum DCI has the structure of the conventional result, rather than that of original. Python’s Fraction reduces ratios, and conventional Power reduces its rational exponent; their conversion rules can also change DCI structure.

Collection conversion has similar consequences:

first = {"temperature": 300, "pressure": 0}
second = {"pressure": 0, "temperature": 300}

assert first == second
assert as_dc_instance(first) != as_dc_instance(second)
assert as_dc_instance(UnorderedDict(first)) == as_dc_instance(UnorderedDict(second))

Python dictionary equality ignores insertion order, while Mapping DCI equality includes entry order. UnorderedDict requests an UnorderedMapping DCI instead. A conventional Multiset uses its members’ conventional equality rules, which need not coincide with DCI equality.

All DCIs in the reference implementation can be used as Python dictionary keys. Converting a DCI key to a conventional object can nevertheless produce an unhashable list or NumPy array. Distinct Boolean, Integer, or Float DCI keys can also collide under conventional dictionary-key equality. Conversion to a conventional dictionary is therefore not always a suitable way to retain a DataCanon Mapping. See the Python API for conversion details.

Store and recover the results

Writers accept conventional objects through as_dc_instance(). Readers reconstruct DCIs from the stored representation:

from data_canon import read, write

write(results, filename="exact_results.dcb")
restored = read("exact_results.dcb")
assert restored == result_instance

recovered_results = dc_instance_to_python(restored)
assert recovered_results[("triangle", "area")] == area

The storage round trip preserves DCI identity. The subsequent conversion from the Mapping DCI to a conventional dict is a separate operation, subject to the target types’ rules. Keep the DCI when its complete structural identity is needed; use conventional objects when their collection or arithmetic operations are wanted.

ExternalFile and ExternalArray are conventional types for constructing external references. Their reverse conversions are different operations: dc_instance_to_python() retrieves and verifies an ExternalFile payload, returning bytes or a content loader’s result, and materializes an ExternalArray as a NumPy array. The conversion does not return another ExternalFile or ExternalArray construction object. Metadata-only work can use the external DCIs without retrieving the payloads.

For constructor signatures, complete conversion tables, and resource handling, use the Python API. For numerical DCT structure, nesting, and identity, use the Reference and Core specification. For the language-independent conventional behavior and its required comparison cases, use the Conventional Types specification.

Application-defined Record conventions

An application can connect its conventional types to ordinary Records through an explicit ConversionRegistry. For example, a conventional date can become a Record holding year, month, and day fields, while another convention might also store calendar metadata. DC preserves that structure without choosing a calendar or defining date operations.

Rules apply recursively through supported collections and Record fields, using as_dc_instance(obj, registry=registry) and dc_instance_to_python(node, registry=registry). Reading a stored DCI does not execute those rules. The Custom Conversion Rules guide works through a complete date example and explains validation and defaults.

Compressed external Arrays

Use ExternalArray(array, filename="forces", compression=True) to select gzip storage for zero-heavy numerical data such as force constants. The conventional constructor finalizes the name as forces.bin.gz; compression defaults to off, which produces forces.bin. Explicit suffixes must agree with the setting.

Convert to a DCI before writing. Directory-backed packages contain the gzip file, while HDF5 packages apply dataset compression to the logical element bytes. The digest covers uncompressed canonical bytes in both representations. The DCI’s finalized path and identity are preserved through transfers. A name ending in .bin identifies a different DCI from the corresponding .bin.gz name. ExternalFiles retain opaque-byte semantics.

See the Python API for filename examples, validation, and compression behavior.

2.4 - Shorthand Preprocessing

Overview

Shorthand preprocessing provides three conveniences for entering scientific data by hand: compact List notation, named textual substitutions, and scalar mathematical evaluation with eval(...).

Preprocessing transforms text into strict Shorthand. Parsing that text then constructs a DataCanon Instance (DCI). This guide introduces each feature separately before showing how they work together.

Start with one compact List

Ordinary Shorthand Lists use brackets and commas:

[1, 2, 3]

Compact notation lets you enter the same List using angle brackets and spaces:

<1 2 3>

Use preprocess_shorthand() to expand it:

from data_canon import preprocess_shorthand

source = "<1 2 3>"
prepared = preprocess_shorthand(source)
print(prepared)

The resulting text is:

[1, 2, 3]

No DataCanon Instance has been constructed yet. prepared is a Python String. Parse that String to construct the List DCI:

from data_canon import parse_shorthand

instance = parse_shorthand(prepared)

Preprocessing and strict parsing are separate steps: preprocessing expands input notation; strict parsing interprets the resulting Shorthand and validates the data.

A compact group always produces a List, even when it contains only one item:

<6>  -> [6]

Empty compact groups are invalid. Use ordinary [] for an empty List.

Enter nested Lists

A comma can separate rows whose items are separated by spaces:

<1 2 3, 4 5 6>

expands to:

[
  [1, 2, 3],
  [4, 5, 6]
]

A semicolon can introduce another level:

<1 2, 3 4; 5 6, 7 8>

expands to:

[
  [
    [1, 2],
    [3, 4]
  ],
  [
    [5, 6],
    [7, 8]
  ]
]

At each level, the preprocessor looks for separators in this order:

  1. Newline.
  2. Semicolon.
  3. Comma.
  4. Horizontal whitespace: space, tab, or carriage return.

The first separator that splits the contents into multiple nonempty parts establishes the outer level. Each part is then processed in the same way. Newlines therefore introduce a level outside semicolons, commas, and spaces. Explicitly nested angle-bracket groups are also supported.

The layouts above illustrate the resulting structure; exact output spacing is not important to strict parsing. Avoid repeated or trailing separators while their treatment remains a Version 1 draft finalization item.

Existing structures remain intact

Separators inside quoted Strings or balanced (), [], and {} do not split items. For example:

<[1,2] 3>

becomes:

[[1,2], 3]

The inner [1,2] is one item. Likewise,

<"Sr Ti O" 3>

becomes:

["Sr Ti O", 3]

The spaces inside the String do not introduce List items.

Construct an Array when needed

Compact notation alone creates Lists. To enter an Array, place it inside an Array expression:

source = 'array(<1.0 0.0, 0.0 1.0>, dtype="float64")'
prepared = preprocess_shorthand(source)
instance = parse_shorthand(prepared)

The prepared text represents:

array([[1.0, 0.0], [0.0, 1.0]], dtype="float64")

The strict Array parser constructs the Array. Compact notation supplies its numerical body without itself performing Array inference.

Substitute one name

A substitution associates a name with replacement text. Define the name before |, then use it in the document body:

species="Sr" | {"species": species}

Preprocessing produces:

{"species": "Sr"}

In Python:

source = 'species="Sr" | {"species": species}'
prepared = preprocess_shorthand(source)
instance = parse_shorthand(prepared)

The name species is an input convenience. After substitution, the parser receives an ordinary Shorthand Mapping.

Define several names

Separate definitions with whitespace. Whitespace around = is permitted:

species = "Sr"
position = [0.5, 0.5, 0.5] |
{
  "species": species,
  "position": position
}

becomes:

{
  "species": "Sr",
  "position": [0.5, 0.5, 0.5]
}

Definitions can contain Strings, Tuples, Arrays, Mappings, or other text:

label = "oxygen"
site = (1/2, 1/2, 0)
lattice = array([[1.0, 0.0], [0.0, 1.0]], dtype="float64") |
{
  "label": label,
  "site": site,
  "lattice": lattice
}

Replacement text is inserted literally. It is not separately parsed as a DCI; only the final document must be valid strict Shorthand. Replacement text can therefore also be an expression fragment used later inside eval(...).

Name rules and protected Strings

A name begins with an ASCII letter or underscore, followed by ASCII letters, digits, or underscores. Names are case-sensitive and must be unique.

Substitution replaces complete identifier-like tokens. A definition for a does not replace part of alpha or a2, and nothing is substituted inside quoted Strings:

a=3 | {"value": a, "text": "a"}

becomes:

{"value": 3, "text": "a"}

Build on earlier definitions

Definitions are processed from left to right. Each definition may use externally supplied names and any earlier local definitions:

a        = 4.593
c_over_a = 0.644
c        = a * c_over_a |
[c]

This produces [4.593 * 0.644], which represents a List containing a Product. To compute the lattice parameter for a machine Array, request evaluation:

source = """a=4.593 c_over_a=0.644 c=eval(a * c_over_a) |
array(<a 0 0, 0 a 0, 0 0 c>)"""
instance = parse_shorthand(source, preprocess=True)

The third diagonal entry is 2.957892. An evaluated definition is resolved before later definitions use it, so d=eval(c * 2) would also work. Self-references and references to later definitions are rejected; arrange parameters before the definitions that depend on them.

Replacements remain literal text. For example, a=1 + 2 b=3 * a | b becomes 3 * 1 + 2, without implicit grouping. Use constructors when a nested numerical structure matters: a=sum(1, 2) b=product(3, a) | b preserves the Sum as one Product constituent. Ordinary Shorthand parentheses create Tuples, not groups. Inside eval(...), arithmetic parentheses provide ordinary grouping.

Readable input files with named parameters

Named parameters make an input file easier to read and revise: repeated values have one definition, and relationships remain visible in the data.

A crystal structure provides a useful example. Atomic positions are often specified as fractions of the repeating cell, with several positions sharing one displacement parameter. Define delta once and write each coordinate in terms of its reference position and displacement:

from data_canon import parse_shorthand, preprocess_shorthand, render_shorthand

source = """
delta = 0.199 |
{
  "basis_atoms": [
    (delta,         1/4 + delta,  1/8),
    (1/2 + -delta,  3/4 + -delta, 5/8),
    (-delta,        3/4 + delta,  3/8),
    (1/2 + delta,   1/4 + -delta, 7/8)
  ]
}
"""

prepared = preprocess_shorthand(source)
instance = parse_shorthand(prepared)
print(render_shorthand(instance, layout="standard", align="left"))

After substitution, the strict parser receives the following structure (whitespace may differ):

{
  "basis_atoms": [
    (0.199,         1/4 + 0.199,  1/8),
    (1/2 + -0.199,  3/4 + -0.199, 5/8),
    (-0.199,        3/4 + 0.199,  3/8),
    (1/2 + 0.199,   1/4 + -0.199, 7/8)
  ]
}

The List contains four Tuple DCIs. Coordinates such as 1/4 + 0.199 are Sum DCIs whose constituents retain the exact Fraction reference position and the Float displacement separately. The resulting DCI no longer contains the substitution name; the source text remains useful for editing the parameter.

The ASCII name delta is used because substitution names currently follow the ASCII identifier rules. With the positive value defined here, -delta becomes the negative Float literal -0.199 by textual substitution. This example needs neither Compact List notation nor eval(...).

Supply substitutions from Python

Instead of placing definitions in the input, supply a mapping of names to replacement Strings:

prepared = preprocess_shorthand(
    '{"species": species, "position": position}',
    substitutions={
        "species": '"Sr"',
        "position": "[0.5, 0.5, 0.5]",
    },
)
instance = parse_shorthand(prepared)

The result is the same Mapping as in the inline example. The values in substitutions must be text. The quotation marks in '"Sr"' are intentional: they make the replacement a Shorthand String literal.

External substitutions may also be used in inline definitions:

prepared = preprocess_shorthand(
    'site=(x, x, 0) | {"position": site}',
    substitutions={"x": "1/2"},
)

This produces:

{"position": (1/2, 1/2, 0)}

Each inline definition expands external names and earlier local names in one pass, then resolves its explicit eval(...) expressions. After definitions are resolved, external names are substituted once in the body, followed by local names once. The external mapping itself is not recursively expanded. An inline name must not conflict with an externally supplied name.

Array presets and linear combinations

Substitutions can name Arrays, making input files concise without changing DCI identity. For example, the Pauli matrices provide a small linear-operator example:

Sigma_x = array([[0, 1], [1, 0]])
Sigma_y = array([[0, 0-1j], [0+1j, 0]]) |
{
  "operator": 2 * Sigma_x + 3 * Sigma_y
}

After substitution, * constructs OrderedProduct because an Array is present; + constructs Sum. DC preserves those structures without multiplying or adding anything. Their conventional counterparts can evaluate the linear combination. The same mechanism supports 2 * I when I is defined as an identity Array.

Evaluate one scalar expression

Use eval(...) when you want to compute a numerical result before parsing:

prepared = preprocess_shorthand("eval(2 + 3)")
print(prepared)

Output:

5

Parsing prepared constructs Integer 5:

instance = parse_shorthand(prepared)

Evaluation is explicit. The preprocessor does not perform arithmetic on expressions outside eval(...).

Supported arithmetic

The evaluator supports real integer and floating-point literals, parentheses, unary + and -, binary +, -, *, /, and **, and real sqrt(...):

InputResulting Shorthand
eval(1/2)0.5
eval((2 + 3)*4)20
eval(2**10)1024
eval(sqrt(9) + 1)4
eval(-2**2)-4
eval(5+(1.89-2.53)*6/3.72)3.967741935483871

Operator precedence follows Python. Division / performs true division; exponentiation uses **, not ^.

Intermediate values use arbitrary-precision integers and binary64 floating-point arithmetic. If a final Float has an exactly integral value, it is emitted as an Integer. Otherwise, a finite floating-point result is emitted as a Float literal. No tolerance is used to decide whether a value is integral.

Evaluation versus stored structure

Strict Shorthand can store an irrational value as a Product containing a Fraction and a Power:

1/2 * sqrt(3)

That expression retains its Product and constituent structure when preprocessed. To compute a floating-point approximation instead, request evaluation:

eval(1/2 * sqrt(3))

This distinction lets you choose between preserving scientific structure and performing numerical arithmetic before constructing the DCI.

Strict structure and preprocessing arithmetic

Outside eval(...), 1 + 2 constructs a Sum DCI and 2 * 3 constructs a Product DCI. Inside eval(...), those operators perform scalar arithmetic before the result is parsed:

assert parse_shorthand("1 + 2") != parse_shorthand("3")
assert parse_shorthand(preprocess_shorthand("eval(1 + 2)")) == parse_shorthand("3")

The evaluator follows Python operator precedence. For example, eval(1/2**3) produces 0.125; strict Shorthand rejects 1/2**3 because a Fraction base requires an explicit power(...) constructor. They are different input grammars used at different stages.

Evaluation limits

The evaluator supports real scalar arithmetic only. It excludes unresolved names, arbitrary function calls, Arrays, indexing, attribute access, and general code execution.

Division by zero, negative real square roots, complex results, and nonfinite floating-point results are rejected. Implementations may limit expression length, nesting, and intermediate integer size. Cross-language numerical conformance details remain recorded in the Version 1 draft specification.

Combine the features

Once each feature is familiar, they can be used together. Consider entering a symmetric matrix with repeated numerical parameters:

a=1.1 b=0.2 |
<a 0 b, 0 a 0, b 0 eval(-a)>

First, compact List notation expands:

[
  [a, 0, b],
  [0, a, 0],
  [b, 0, eval(-a)]
]

Next, substitutions are inserted:

[
  [1.1, 0, 0.2],
  [0, 1.1, 0],
  [0.2, 0, eval(-1.1)]
]

Finally, eval(...) is evaluated:

[
  [1.1, 0, 0.2],
  [0, 1.1, 0],
  [0.2, 0, -1.1]
]

Strict parsing now constructs a List of Lists. To construct an Array, wrap the compact body in array(..., dtype="float64").

The order also matters when replacement text contains spaces. For example:

term=1/2 * sqrt(3) | <term 0>

Compact List expansion happens first:

[term, 0]

Substitution then produces:

[1/2 * sqrt(3), 0]

The spaces within the replacement Product do not split it into additional List items because compact List expansion has already finished.

Longhand and preprocessing

Longhand is the explicit-constructor subset of strict Shorthand. Preprocessing can insert constructors just as it can insert shorter forms:

prepared = preprocess_shorthand('values=list(1, 2) | tuple(values, fraction(1, 3))')
assert parse_shorthand(prepared) == parse_shorthand('([1, 2], 1/3)')

The longhand renderer flag has no effect on preprocessing. Compact List notation refers specifically to angle-bracket input groups; it is unrelated to the renderer’s layout="compact" delimiter policy.

Python parsing and CLI defaults

Call parse_shorthand() directly for strict Shorthand. When preprocessing is wanted, call preprocess_shorthand() first and parse its result.

Quoted Strings are protected during every stage. Text such as "<1 2>", "eval(2 + 3)", or "a | b" remains ordinary String content.

Preprocessing does not change DataCanon’s types or identity rules. YAML $ values and .dcs persistence files use strict Shorthand and do not implicitly perform preprocessing when read through the ordinary Python persistence API. The CLI is a human-input frontend and enables preprocessing by default:

echo '<1 2, 3 4>' | dc
dc rutile.dcs --dump rutile.dcb

Use --strict to require strict Shorthand input in the CLI. The option does not change the Python API defaults or enable preprocessing in key/count queries.

For the detailed rules, see the Shorthand Preprocessing specification.

2.5 - Custom Conversion Rules

DataCanon Records let applications describe structured values without adding new DataCanon Types. An application can define a Record convention and supply explicit rules connecting that Record to a conventional type.

A calendar date is a useful example. Python provides datetime.date, but DC need not prescribe a calendar, year range, or date arithmetic. The application can make those choices and store the necessary information in a Record.

This guide defines an illustrative "example.Date" convention. The name is application data, not a built-in DC type or a universally prescribed date format.

1. Choose the Record structure

The example stores three Integer fields in the order year, month, day:

record(
  "example.Date",
  "year": 2026,
  "month": 10,
  "day": 22
)

DC defines the Record’s structure and identity. The application defines how those fields describe a date and which dates are valid for its purposes. Other applications can choose different names, additional calendar metadata, or different representations without changing the DC type system.

2. Write the conventional-to-DCI rule

A handler receives the conventional object and a convert() function. Calling convert() applies the same registry recursively to a constituent object.

from datetime import date

from data_canon import ConversionRegistry, as_dc_instance, dc_instance_to_python
from data_canon.instance import Field, IntegerDC, RecordDC

DATE_RECORD = "example.Date"
DATE_FIELDS = ("year", "month", "day")


def date_to_record(value, convert):
    return RecordDC(DATE_RECORD, tuple(
        Field(name, convert(getattr(value, name)))
        for name in DATE_FIELDS
    ))

The handler constructs an ordinary Record DCI. Integer field values use the usual conversion rules. A conventional-to-DCI handler must return a DCI; returning a conventional dictionary or another unsupported object is an error.

3. Write and validate the reverse rule

A reverse handler receives the original Record DCI and a recursive convert() function. Validate the expected schema before constructing a conventional object:

def date_from_record(node, convert):
    if tuple(field.name for field in node.fields) != DATE_FIELDS:
        raise ValueError("example.Date requires year, month, day in that order.")
    if not all(isinstance(field.value, IntegerDC) for field in node.fields):
        raise TypeError("example.Date fields must be Integer DCIs.")
    return date(*(convert(field.value) for field in node.fields))

Python’s date constructor checks its calendar and supported date range. An invalid calendar date raises an error during this application conversion. The Record itself can still be a valid DCI: DC does not assign date semantics to its name or perform the application’s calendar validation.

The example deliberately requires exactly three fields in the stated order. An application’s own handler can validate additional calendar or provenance fields according to its documented convention.

4. Register both directions explicitly

registry = ConversionRegistry()
registry.register_type(date, date_to_record)
registry.register_record(DATE_RECORD, date_from_record)

original = date(2026, 10, 22)
instance = as_dc_instance(original, registry=registry)
assert isinstance(instance, RecordDC)
assert dc_instance_to_python(instance, registry=registry) == original

The registry belongs to the application. Registration does not change global conversion behavior or affect another registry.

Conventional input rules match the exact type. Registering date does not also register datetime.datetime, which carries additional time information. Register a separate rule for each supported conventional type. An explicit rule for an exact built-in type takes precedence over its usual conversion, but existing DCIs are always returned unchanged by as_dc_instance().

Reverse rules match the full Record name using exact String equality. There is no import, class lookup, or prefix-based dispatch derived from that name. Duplicate registrations are rejected so a rule cannot be silently replaced.

5. Convert a nested structure

Rules apply throughout supported collections, including Mapping keys and values:

source = {
    ("run", 7): {
        "recorded_on": date(2026, 10, 22),
        "samples": [date(2026, 10, 21), date(2026, 10, 22)],
    },
    date(2026, 10, 22): "completed",
}

instance = as_dc_instance(source, registry=registry)
restored = dc_instance_to_python(instance, registry=registry)
assert restored == source

A custom handler for a container-like conventional type should call its supplied convert() function for each constituent that needs conversion. Calling the public conversion function without registry=registry would instead use the default rules. Reverse convert() calls also retain the caller’s external resource source context.

Arrays keep their specialized machine elements. Custom conversion does not walk raw Array elements, Fraction integers, Record names, or other fixed metadata as though that information consisted of separate DCIs.

6. Store and recover the DCI

Conversion rules are separate from canonical encoding and persistence:

from data_canon import canonical_decode, canonical_encode

encoded = canonical_encode(instance)
decoded = canonical_decode(encoded)
assert decoded == instance
assert dc_instance_to_python(decoded, registry=registry) == source

Decoding recovers Record DCIs without executing application handlers. Another implementation can preserve, fingerprint, and inspect the same Records without knowing the application’s date convention.

The same sequence works with files:

from data_canon import read, write

write(instance, filename="dated_data.dcb")
loaded = read("dated_data.dcb")
assert loaded == instance
assert dc_instance_to_python(loaded, registry=registry) == source

Pass the registry to the conversion functions. read(), write(), renderers, and canonical codecs do not accept a registry; convert conventional objects to DCIs first and convert decoded DCIs afterward.

Convert your own class

The same mechanism works for an application-defined class. Consider a simple DataPoint whose coordinates are stored in underscore-prefixed attributes:

from fractions import Fraction

from data_canon import (
    ConversionRegistry, as_dc_instance, dc_instance_to_python,
    render_shorthand, read, write,
)
from data_canon.instance import Field, RecordDC


class DataPoint:
    def __init__(self, x, y, z):
        self._x = x
        self._y = y
        self._z = z

Choose an application Record name and stable field names. The Record need not expose the class’s attribute naming convention: x, y, and z describe the stored data, while _x, _y, and _z remain implementation details.

POINT_FIELDS = ("x", "y", "z")


def point_to_record(value, convert):
    return RecordDC("example.DataPoint", tuple(
        Field(name, convert(getattr(value, "_" + name)))
        for name in POINT_FIELDS
    ))


def point_from_record(node, convert):
    if tuple(field.name for field in node.fields) != POINT_FIELDS:
        raise ValueError("example.DataPoint requires x, y, z in that order.")
    return DataPoint(*(convert(field.value) for field in node.fields))


point_registry = ConversionRegistry()
point_registry.register_type(DataPoint, point_to_record)
point_registry.register_record("example.DataPoint", point_from_record)

The class imposes no coordinate-type restrictions, so the reverse handler only validates its field names and order. If your class requires particular coordinate types or other invariants, validate those requirements in the handler or its conventional constructor. Recursive convert() calls retain each coordinate’s usual conventional-to-DCI conversion.

Construct and inspect the DCI:

point = DataPoint(Fraction(1, 2), 0, 1.25)
point_instance = as_dc_instance(point, registry=point_registry)
print(render_shorthand(point_instance, padding=0))

Output:

record(
  "example.DataPoint",
  "x": 1/2,
  "y": 0,
  "z": 1.25
)

Write the Record DCI, read it back, and explicitly reconstruct a DataPoint:

write(point_instance, filename="point.dcb")
loaded_point = read("point.dcb")
assert isinstance(loaded_point, RecordDC)

restored_point = dc_instance_to_python(loaded_point, registry=point_registry)
assert type(restored_point) is DataPoint
assert (restored_point._x, restored_point._y, restored_point._z) == (
    Fraction(1, 2), 0, 1.25
)

Reading returns a Record DCI. The registered reverse handler calls your class’s constructor only during the subsequent conventional conversion. A Record name never causes DC to import or instantiate a class automatically.

The rules also apply inside collections:

cloud = {"points": [point, DataPoint(1, 2, 3)]}
cloud_instance = as_dc_instance(cloud, registry=point_registry)
restored_cloud = dc_instance_to_python(cloud_instance, registry=point_registry)
assert all(type(value) is DataPoint for value in restored_cloud["points"])
assert restored_cloud["points"][1]._z == 3

This example does not define DataPoint.__eq__, so the assertions inspect the restored class and attributes rather than comparing separately constructed objects using Python’s default identity-based equality. The runnable file examples/custom_datapoint.py performs the file round trip in a temporary directory.

Defaults, errors, and limits

With no registry, existing conversion rules are unchanged. An unsupported conventional object raises TypeError. An unregistered Record follows the existing generated-namedtuple conversion, which requires suitable Python class and field names. In particular, a qualified Record name such as "example.Date" needs an application handler for conventional conversion; it remains valid for DCI reading, writing, and identity operations without one.

Application handlers are ordinary Python functions, explicitly chosen by the caller. Handler exceptions propagate; the implementation does not silently fall back to another rule or repair the Record. A handler is responsible for its own schema, conversion policy, and any information it discards.

A custom convention does not make every conversion lossless. Conventional collections still apply their own equality and hashability rules, and a handler can choose a lossy conventional representation. The example date convention has a tested round trip because both directions agree on its schema.

The runnable date example is examples/custom_conversion.py; the user-defined class example is examples/custom_datapoint.py in the code repository. See the Python API for registry signatures.

2.6 - Command-Line Interface

The dc command loads a DataCanon file and displays, converts, searches, or selects items from its DCI. It works with Shorthand (.dcs), canonical binary bytes (.dcb), and DataCanon HDF5 files (.h5 or .hdf5), and YAML interchange files (.yml or .yaml).

Put an input filename first, or omit it to read Shorthand from standard input. With no action option, dc prints Shorthand:

dc structure.dcb

Installation and completion

Installing the Python package installs the dc command in the same environment. Activate that environment before using it. For a development installation from the code repository:

source DC/bin/activate
make install
dc --help

Enable shell completion in the active environment:

eval "$(register-python-argcomplete dc)"

The Makefile also provides make completion, which prints that command for you to execute. Add the registration to your shell initialization after environment activation if you want it applied automatically. Completion suggests command-line options, filenames, and choices such as rendering styles. It does not inspect file contents to suggest mapping keys. Option names appear when you type -; completion does not fill an empty argument position with every CLI option. After --get, further bare arguments are keys until another option begins.

Read Shorthand from standard input

Omit the filename to try a DCI directly at the command line:

echo 'power(8, 1, 3)' | dc

Output:

8**(1/3)

The command preserves the Power DCI and displays it in Shorthand. Constructor names are lowercase. All existing actions and rendering options also work with piped input:

echo 'power(8, 1, 3)' | dc --longhand
echo '{"outer": {"x": 3}}' | dc --get x
echo '[1, 2, 1]' | dc --count 1
echo 'power(8, 1, 3)' | dc --fingerprint
echo 'power(8, 1, 3)' | dc --dump root.dcb

Use - to select standard input explicitly, including with shell redirection:

dc - --layout standard --align left < structure.dcs

The CLI preprocesses Shorthand input by default, so compact Lists, substitutions, and explicit eval(...) expressions need no extra option:

echo '<1 2, 3 4>' | dc --dump matrix.dcb

The resulting DCI is a List of Lists. Standard input defaults to Shorthand; select YAML with --input-format yaml. Use a filename for canonical binary or HDF5 input. A displayed DCI can feed another dc command:

dc matrix.dcb | dc --longhand

Input may span multiple lines and must contain exactly one complete expression. The command reads through end-of-file; when typing directly in a terminal, finish with Ctrl-D on Linux or macOS. Empty input and malformed expressions are errors.

External-resource paths in standard input resolve from the current working directory. Piped Shorthand carries the DCI’s metadata; it does not carry the original file’s resource namespace or its external payloads.

Example input file

A crystal structure illustrates how named parameters keep a scientific input file readable. The example describes rutile, a form of titanium dioxide (TiO₂). Three lattice vectors define a repeating cell; atom positions are given as fractions of those vectors and grouped by chemical element.

Save the following input as rutile.dcs:

a        = 4.593
c_over_a = 0.644
c        = a * c_over_a
u        = 0.305 |
{
  "lattice_vectors": [
    (a, 0, 0),
    (0, a, 0),
    (0, 0, c)
  ],

  "basis_atoms": {
    "Ti": [
      (0,   0,   0),
      (1/2, 1/2, 1/2)
    ],

    "O": [
      (u,        u,        0),
      (1   + -u, 1   + -u, 0),
      (1/2 +  u, 1/2 + -u, 1/2),
      (1/2 + -u, 1/2 +  u, 1/2)
    ]
  }
}

The lattice_vectors and basis_atoms keys follow the Principia Materia (PM) conventions. Lattice vectors and atom positions are written explicitly as Lists of Tuples. The exact halves remain Fractions, and the parameter u makes the relationships between oxygen positions visible in the input.

Load the input file directly:

dc rutile.dcs

Definitions are resolved in source order, so c can use a and c_over_a. The definition c = a * c_over_a retains a Product in the third lattice vector. No evaluation is needed to load or store that structure. Use c = eval(a * c_over_a) if a computed numerical lattice parameter is wanted instead.

The CLI preprocesses .dcs and standard-input Shorthand by default. Use --strict to disable compact Lists, substitutions, and eval(...) and require strict Shorthand:

dc structure.dcs --strict
echo '<1 2, 3 4>' | dc --strict

The second command reports a syntax error. --strict controls input parsing; it does not change rendering or mapping-key interpretation. Canonical binary and HDF5 files use their existing readers in either mode. The Python parse_shorthand() function, read() function, and YAML airlocks retain their strict defaults. See Shorthand Preprocessing for the individual input features.

Dump to another format

Write the resolved DCI as canonical binary bytes:

dc rutile.dcs --dump rutile.dcb

The .dcb file is binary. It stores the DCI, including its numerical structure; substitution definitions and source formatting are not stored. Subsequent reads of this file do not need preprocessing.

The destination suffix selects the output format:

Destination suffixOutput
.dcsUTF-8 strict Shorthand
.dcbCanonical binary bytes
.h5 or .hdf5DataCanon HDF5
.yml or .yamlDataCanon YAML interchange
No suffixShorthand, with .dcs appended

For example:

dc rutile.dcb --dump rutile.h5
dc rutile.h5 --dump restored.dcs
dc rutile.dcb --dump rutile.yaml

A successful dump prints nothing. The resulting representation preserves the DCI’s identity. For DCIs with external references, dumping produces a complete package: resources are verified and copied beside .dcs, .dcb, or YAML output, or embedded in HDF5. Missing or invalid required resources cause the dump to fail.

ExternalArray paths ending in .bin.gz select gzip storage for directory packages and compressed embedded datasets for HDF5. The digest covers the uncompressed canonical element bytes. Transfers retain the same path and digest; an HDF5-to-directory dump recreates the gzip file. Configure compression when constructing the conventional ExternalArray; no additional CLI flag is needed. ExternalFile payloads remain opaque even when their paths end in .gz.

Display YAML

Use --yaml to print the DCI as YAML instead of Shorthand:

dc structure.dcb --yaml
dc structure.dcb --get basis_atoms --yaml
echo '{("Eg", 0): 2.37, "position": (1/2, 0, 0)}' | dc --yaml

The existing YAML interchange rules apply: native YAML values are used where faithful, and $ introduces strict Shorthand for other DCTs, including Tuple keys and values. The $ marker must be followed by whitespace. $100 and ${HOME} remain literal Strings. The marker performs no substitution or evaluation; quoting alone does not escape it. A literal reserved prefix uses a backslash, such as \$ 1/2. The writer handles literal-prefix escaping.

YAML output uses block-style Lists and Mappings, with one List member per - entry; empty collections remain [] or {}. Single-line $ values are not wrapped to a fixed width. YAML quotes scalars only when needed by its syntax or scalar-resolution rules, and multiline Shorthand uses block scalars.

A .yml or .yaml dump target selects YAML without requiring --yaml:

dc structure.dcb --dump structure.yml
dc structure.dcb --yaml --dump structure

The second command creates structure.yaml. Combining --yaml with an explicit non-YAML suffix is an error. YAML export preserves the DCI and does not convert it to conventional Python objects. Terminal output needs only external metadata; a YAML file dump verifies and copies referenced payloads beside the output file.

--yaml works with display and --get, but not --count or --fingerprint. YAML output accepts --align left, --align right, --align none, and --padding N:

dc rutile.dcs --yaml --align left --padding 0
dc rutile.dcb --dump rutile.yaml --align left --padding 1

Alignment is disabled by default. Within a native YAML List, rectangular groups of nonempty, flat Tuple rows share column widths. Each List aligns independently. Ragged rows, mixed List/Tuple rows, or cells requiring multiline Shorthand fall back to normal rendering. Native YAML Lists remain Lists; alignment does not replace them with Tuple escapes. Embedded Shorthand uses the same alignment rules as render_shorthand().

Padding defaults to 1 and controls spaces beside same-line delimiters inside $ escapes, including Tuple keys and numerical constructors. --padding 0 produces $ (1, 2) instead of $ ( 1, 2 ). Padding does not change YAML indentation or the space separating $ from its value. Fraction literals and other fixed forms keep their usual spelling. --longhand, --layout, --custom-layout, and --indent remain Shorthand-only options and are rejected for YAML output.

Read or write YAML

A .yaml or .yml input suffix selects YAML automatically. The default output remains Shorthand, so conversion needs no output option:

dc structure.yaml
dc structure.yaml --dump structure.dcb
dc structure.yaml --yaml --align left

For standard input, choose YAML explicitly:

cat structure.yaml | dc --input-format yaml
cat structure.yaml | dc --input-format yaml --yaml --align left --padding 0

--input-format yaml and --input-format shorthand override text input format selection for stdin or a named file. Input and output choices are independent; --yaml always selects output, never input. The CLI does not guess format from text contents. Canonical binary and HDF5 input still require filenames and their existing readers.

YAML output uses block-style Lists and Mappings. Native multiline Strings use YAML literal block scalars where supported by PyYAML, with quoted fallback when necessary to preserve their contents. Multiline $ escapes likewise use literal block scalars when possible. The --fold-string N option enables width-controlled folded scalars for eligible long single-line Strings. YAML’s native folding and chomping rules apply; these differ from DataCanon Shorthand block-string rules. YAML $ escapes always use strict Shorthand; they never run preprocessing. --strict applies only to Shorthand input and is rejected with YAML input. External paths in a YAML file resolve relative to its containing directory; YAML received on stdin uses the current working directory. Metadata reads do not retrieve external payloads. Python YAML import remains parse_yaml(); this CLI support does not change generic Python read()/write() dispatch.

Choose the displayed notation and layout

Choose delimiter layout, column alignment, and indentation independently:

dc rutile.dcb --layout standard --align left
dc rutile.dcb --layout compact --align left --indent 4
dc rutile.dcb --longhand

The default is standard layout with shorter notation where appropriate. --layout standard puts multiline delimiters on separate lines; --layout semi-compact shares the opening line with the first item or entry; --layout compact also shares the closing line with the final content. --layout inline forces the selected value and its whole subtree onto one line.

dc rutile.dcb --layout inline --padding 0
dc rutile.dcb --custom-layout '{"list":"inline"}'

A List forced inline also renders nested Mappings, Arrays, and other values inline. Child layout overrides yield to that enclosing inline requirement; siblings outside the List retain their own layouts. Column alignment and structural indentation have no effect inside an inline subtree.

--align left, --align right, and --align none control column padding. --align without a value selects left alignment. Alignment is disabled unless requested. --indent N controls structural nesting on new lines, defaulting to 2. It does not add spaces after a same-line opener. Compact continuation items align beneath the first item; indentation moves nested blocks below keys rather than changing that alignment.

--padding N controls spaces beside same-line collection and constructor delimiters, defaulting to 1. It applies to flat values too: [ 1, 2 ] becomes [1, 2] with --padding 0. Standalone delimiters receive no padding. Both options accept nonnegative integers, including zero.

echo '{1, 2, [0, 1]}' | dc --layout compact --indent 0 --padding 0

Output:

{1,
 2,
 [0, 1]}

Use --custom-layout for per-type overrides:

dc rutile.dcb --layout standard --custom-layout '{"mapping":"semi-compact","list":"compact","tuple":"compact","set":"compact"}' --align left

The argument is a strict DataCanon Mapping or UnorderedMapping with String keys and String values. Quote the entire argument for the shell. Constructor form, such as mapping("set": "compact"), is also accepted. The CLI does not use JSON, Python evaluation, or preprocessing to read this configuration.

Every structured type accepts the same four layout contracts. For example, "set", "multiset", "unordered_mapping", "record", and "array" can be configured alongside Lists, Tuples, and Mappings. The Python API lists all accepted type names. An override affects only its named DCT; unspecified types use --layout (default standard). No separate per-type command-line flags are needed. Unknown names, invalid values, and duplicate keys are errors before input loading. Multiline Mapping values start below their keys unless an enclosing inline layout suppresses the line breaks.

--longhand selects explicit constructors for structured values. Null, Boolean, Integer, Float, and String remain literals: sum(1, 1.0, fraction(1, 2)) is longhand. Fraction uses fraction(...), Bytes keeps bytes(...), and Array elements retain their specialized syntax. Layout and notation are independent:

dc rutile.dcb --longhand --layout semi-compact --align left

Lists, Tuples, Sets, and Multisets use one row per item when they contain a List or Tuple, or an item that already spans multiple lines, unless forced inline. Coordinate Tuples remain inline, so the rutile lattice vectors and atom positions appear as separate rows. Alignment adds column padding for rectangular rows, including coordinates containing Sums or Products. For example:

echo '[(1, 0), (0, -1)]' | dc --layout semi-compact --align left

Output:

[ ( 1,  0 ),
  ( 0, -1 )
]

Using --layout compact also puts the final ] on the last row.

The same options apply when dumping Shorthand:

dc rutile.dcb --dump readable.dcs --layout standard --align left
dc rutile.dcb --dump constructors.dcs --longhand

Rendering options affect textual representation, not DCI identity. The options also apply to values printed by --get. They are rejected with --count, --fingerprint, and binary or HDF5 output. YAML accepts alignment and padding as described above; Shorthand layout options do not apply to YAML.

Render multiline and folded Strings

By default, standard Shorthand layout renders Strings containing LF characters as literal blocks when possible:

{
  "message":
    """
    First line
    Second line
    """
}

Literal blocks preserve the String’s actual line breaks and whitespace. Strings that cannot safely use block syntax retain their ordinary quoted representation.

For long Strings without semantic line breaks, use --fold-string N to request physical wrapping at a preferred width of N characters:

dc input_data.dcs --fold-string 80
dc input_data.dcs --dump readable.dcs --fold-string 80

The renderer uses folded blocks when it can do so without changing the String DCI:

{
  "description":
    """>
    This is a long description that can be wrapped across
    physical lines without introducing semantic newlines
    into the underlying String.
    """
}

The requested width is a preference, not a strict maximum. Long words and significant whitespace may require longer lines or fallback to quoted rendering.

Block Strings are emitted only under standard Shorthand layout. The semi-compact, compact, and inline policies retain quoted Strings with escapes. Enclosing inline layouts take precedence over child rendering preferences.

The same --fold-string option is available for YAML output:

dc input_data.dcs --yaml --fold-string 80
dc input_data.dcs --dump readable.yaml --fold-string 80

YAML uses its native literal (|) and folded (>) scalar syntax. PyYAML may choose quoted scalars when block formatting is unsuitable, particularly when significant trailing whitespace is present. Width-controlled folding is applied selectively to eligible Strings, not globally to all YAML output.

These options affect textual presentation only. Parsing the output recovers the same DCI and therefore the same canonical bytes and fingerprint.

Find a mapping key

Supply a bare name for a String key. For example, find the titanium positions in the nested basis_atoms Mapping:

dc rutile.dcb --get Ti

The search visits Mappings and UnorderedMappings throughout the DCI, so it finds "Ti" inside "basis_atoms". It also visits mappings nested in collections, Record field values, and mapping keys. It does not require a Mapping at the root.

One matching entry prints its value. If the same key occurs in multiple mappings, the command prints a List containing all matching values, in traversal order. Equal repeated values remain separate occurrences. It continues searching inside a matched value, so deeper matches are included too. Traversal follows the recovered constituent order; unordered collections do not promise an application-level ordering of results.

For example, save this strict Shorthand as repeated.dcs:

{"left": {"x": 1}, "right": {"x": 2}}

Then:

dc repeated.dcs --get x

prints a List containing 1 and 2. A single match whose value is itself a List is printed directly; multiple List-valued matches form an outer List.

Keys can belong to any DCT. For example, when a file contains a Tuple key:

dc results.dcb --get '(1/2, 0, 0)'

Valid Shorthand keys retain their DCTs. To select String keys whose text would otherwise parse as another DCT, include explicit Shorthand quotes:

dc results.dcb --get 1          # Integer key
dc results.dcb --get true       # Boolean key
dc results.dcb --get '"1"'      # String key
dc results.dcb --get '"true"'   # String key

Implicit String handling accepts bare identifiers, including Unicode letters and names such as basis_atoms or site_1. Names with spaces or punctuation need explicit Shorthand quotes, for example --get '"site label"'. Malformed structured keys such as power(1, remain errors; they do not become Strings.

Key comparison uses DCI equality. Integer 1, Float 1.0, and Boolean true remain distinct keys. Record names and field names are fixed metadata, so --get does not treat them as mapping keys. A missing key is an error.

Follow a chain of mapping keys

Supply several keys to --get to search for the first and then follow direct Mapping entries:

dc rutile.dcb --get basis_atoms O
dc results.dcb --get measurements '("Eg", 0)'

The first key is searched recursively using the existing single-key behavior. Every subsequent key is looked up directly in each matching Mapping or UnorderedMapping value. Later steps do not search recursively or descend through Lists. Each key uses the same strict Shorthand or bare-String-name rules. Negative integer keys work within the chain. For negative expressions that argument parsing could mistake for options, use constructor form, such as --get measurements 'fraction(-1, 2)'. Dots are not separators; a String key containing a dot needs Shorthand quotes, for example --get '"basis.atoms"'.

Branches missing a subsequent key, or whose intermediate value is not a Mapping, are skipped. If no branch completes the chain, the command reports an error. Results stay separate during traversal: one final match is printed directly, while multiple final matches become a List in traversal order. Equal repeated matches remain separate, and List-valued matches are not flattened.

For example, this input has two complete chains and one incomplete branch:

echo '[{"a": {"b": 1}}, {"a": {"b": 2}}, {"a": 3}]' | dc --get a b

Output:

[ 1, 2 ]

Put an input filename before --get; subsequent bare arguments are consumed as keys. For stdin, omit the filename or use -. Another option ends the key sequence:

dc rutile.dcb --get basis_atoms O --yaml --align

Pipelines remain useful, but a second dc --get searches recursively again. A multi-key --get follows direct entries after the first recursive search.

Select or slice a List, Tuple, or Array

--index SELECTOR [SELECTOR ...] applies integers or slices successively. Integer indices are zero-based and accept negative values from the end:

echo '[(1, 2), (3, 4)]' | dc --index 0
echo '[(1, 2), (3, 4)]' | dc --index=-1
echo '[(1, 2), (3, 4)]' | dc --index 1 0

Without --get, indexing applies to the root DCI. With --get, it applies to the final result after the entire key chain:

dc rutile.dcb --get basis_atoms O --index 2
dc rutile.dcb --get basis_atoms O --index=-1 --yaml

If there are multiple final matches, --index selects one member of the aggregate List; it does not index each match individually. A single List-valued match is indexed directly. Selection order is always --get, then --index, then rendering, independently of where the options occur on the command line.

For Lists and Tuples, integer indexing selects a contained DCI. For Arrays, integer indexing selects along the first dimension. If the result is an Array, it is returned as an Array DCI; if the result is an Array element, that value is converted to a DCI. Strings, Sets, Multisets, and Mappings are not indexed. An incompatible target or an out-of-range integer index reports an error; malformed selectors are command-line usage errors. Empty sequences have no valid indices.

Slicing operates along the first dimension and preserves the DCT: a List slice is a List, a Tuple slice is a Tuple, and an Array slice is an Array. Out-of-range bounds are clipped, and slices may produce an empty result. A zero step or malformed slice is a usage error before input is loaded. Subsequent selectors operate on the slice result itself, without distributing across its members.

Shorthand formatting and YAML output options apply to the selected DCI. --index cannot be combined with --dump, --count, or --fingerprint. Each index is applied to the preceding result: --index 1 0 selects item 1, then item 0 from that value. Every intermediate target must be a List or Tuple; otherwise the command fails without printing a partial result. Another option ends the index sequence. Put the filename before --index as well as --get. Use --index -1 -1 for several negative integer indices.

Slices use start:stop[:step], with Python’s exclusive stop bound, omitted bounds, negative bounds, and negative steps:

dc rutile.dcb --get basis_atoms O --index 0:2
echo '(0, 1, 2, 3, 4)' | dc --index ::2
echo '[0, 1, 2, 3, 4]' | dc --index ::-1

Integer indexing selects a contained DCI for Lists and Tuples. For Arrays, integer indexing selects along the first dimension. If the result is an Array, it is returned as an Array DCI; if the result is an Array element, that value is converted to a DCI. Slicing operates along the first dimension and preserves the DCT: a List slice is a List, a Tuple slice is a Tuple, and an Array slice is an Array. Out-of-range bounds are clipped, and slices may produce an empty result. A zero step or malformed slice is a usage error before input is loaded. Subsequent selectors operate on the slice result itself, without distributing across its members:

echo '[(0, 1), (2, 3), (4, 5)]' | dc --index 1: 0 1

That command selects the suffix List, its first Tuple, then the Tuple’s second item, producing 3.

A slice beginning with - can look like an option to argparse, so attach it with =. Repeat --index to continue the same selection chain:

echo '[0, 1, 2, 3, 4]' | dc --index=-2:
echo '[[0, 1, 2], [3, 4, 5]]' | dc --index 1 --index=-2: --index 0

Repeated --index options concatenate their selectors in order. Integers and slices are the entire selection syntax; there is no expression evaluation or filter language. Put the input filename before the selection arguments.

Approximate and round numerical values

Use --round N to round floating-point values to N decimal places, including floating-point and complex Array elements. Negative N rounds to powers of ten. Array type and shape are preserved. Rounding uses Python’s round rule on binary floating-point values, not exact decimal arithmetic.

dc results.dcb --get reciprocal_vectors --round 3 --align
dc results.dcb --round 3 --yaml

Use --to-float to collapse a complete scalar Fraction, Constant, Sum, Product, Power, or Quotient value to a Float. Integer and Boolean DCIs remain unchanged; Complex components are processed individually. Existing Arrays retain their types. Array-valued Sums and OrderedProducts retain their structure; scalar subexpressions inside them are approximated without evaluating Array arithmetic. Lists, Tuples, Records, and other collections are processed recursively. For example:

echo '2 * Pi + 1/2' | dc --to-float --round 3

produces:

6.783

Sum and Product operands are evaluated in canonical order so equal DCIs have equal approximations, independent of their stored operand order.

Selection (--get and --index) happens first, then conversion, then rounding, regardless of the order of flags. Mapping keys and external references are unchanged, and no external payload is loaded. A Set transformation that would produce duplicate members fails explicitly; Multisets retain occurrences.

Both options create a transformed DCI. They work with Shorthand, YAML, and --dump, including binary or HDF5 output. The original DCI is unchanged; a transformed DCI can have a different fingerprint. Neither option can be combined with --count or --fingerprint. Omit --to-float to round Float leaves while retaining the surrounding numerical structures and exact fields.

Count occurrences of a DCI

--count searches for a complete DCI using its type-defined equality:

dc rutile.dcb --count '1/2'

It prints an integer count. Zero matches is a successful search that prints 0.

The search includes the root and all constituent DCIs: collection members, mapping keys and values, Record field values, Complex components, Power bases, and Quotient constituents. Repeated occurrences count separately, including repeated members of Multisets, Sums, and Products.

Fixed information prescribed directly by a DCT is not a separate DCI. Searching for Integer 1 does not match a Fraction numerator or a Power exponent field. Likewise, Array elements, Record names, relative-path components, and byte payload contents are not searched as constituent DCIs.

Queries are never preprocessed. --count requires strict Shorthand; --get additionally accepts bare String names as described above. For a negative numeric query, use an equals sign so argument parsing keeps the value with its option:

dc results.dcb --count=-1

The same convention works for negative keys: --get=-1.

dc rutile.dcb --fingerprint

The output is the 64-character lowercase hexadecimal SHA-256 fingerprint of the DCI’s canonical bytes. It is independent of Shorthand layout and HDF5 storage layout. It is not a hash of the input file’s representation bytes, except when that file is itself the raw canonical .dcb encoding.

Displaying, searching, and fingerprinting external DCIs use their metadata. Metadata operations do not retrieve external payloads. Dumping a complete package does require those resources.

Options and errors

Choose at most one action: --dump, --get, --count, or --fingerprint. --get accepts one or more keys. --index optionally selects from the root or the final --get result; it is not valid with the other actions. With none supplied, the action is to display Shorthand, or YAML with --yaml. Omitting the filename, or supplying -, reads text from standard input (Shorthand by default; --input-format yaml selects YAML). Help and version output do not read input:

dc --help
dc --version

Successful output goes to standard output and diagnostics go to standard error. Displayed Shorthand and YAML are suitable for shell redirection, but use --dump when creating a complete package with external resources.

Exit statusMeaning
0Success, including a count of zero
1Input, data, resource, output, or selection error; including no matching key chain or invalid index target/bounds
2Command-line usage error

A malformed Shorthand query, invalid input file, or failed resource verification produces a diagnostic and a nonzero exit status.

3 - Python API

A DataCanon Type (DCT) defines its instances and their identity. The Python reference implementation provides construction of DataCanon Instances (DCIs), conversion, Shorthand, canonical bytes, fingerprints, persistence, and additional conventional numerical and collection types.

A conventional type is a type used directly for data and computation in a programming language, including built-in, library-provided, and user-defined types. Conventional list, Fraction, and Power objects are distinct from DCIs represented by ListDC, FractionDC, and PowerDC objects. DCIs and conventional objects are both Python objects; conversion connects types with different contracts.

For a step-by-step introduction, use Conventional Types.

Public API

PurposeExports
Conversion between conventional objects and DCIsConversionRegistry, as_dc_instance, dc_instance_to_python
Shorthandparse_shorthand, render_shorthand
Preprocessingpreprocess_shorthand
Canonical bytescanonical_encode, canonical_decode
Fingerprintingfingerprint
Persistenceread, write, read_hdf5, write_hdf5
YAMLparse_yaml, render_yaml
Explicit numerical transformationsto_float, round_floats
Structural queriesiter_dcis, get_values, index_value, count_occurrences
Shared output operationsrender_output, dump_output
Numerical conventional typesConstant, Pi, E, Sum, Product, OrderedProduct, Power, Quotient, ComplexNumber
Collection conventional typesMultiset, UnorderedDict
External-reference construction typesExternalFile, ExternalArray

Classes implementing DCTs, such as RecordDC and BytesDC, are available from data_canon.instance. Field is a helper class for Record fields. Conversion and Shorthand parsing can construct DCIs without requiring direct use of those classes.

Shared queries, transformations, and output

The functions below are available from data_canon. No function invokes argparse or calls the dc executable. Applications can select, transform, and display a DCI using the same operations as the command-line tool.

FunctionContract
iter_dcis(node)Visit each DCI occurrence, including the root and Mapping keys; omit fixed information and Array elements.
get_values(node, key, *following_keys)Search recursively for the first key, then follow direct Mapping keys. Return one value, or a List DCI of multiple matches; fail if none. Keys are DCIs.
index_value(node, index)Select a List/Tuple item with an integer, or preserve its DCT when slicing with a Python slice.
count_occurrences(node, query)Count equal DCI occurrences using DCT-defined equality. Both arguments are DCIs.
to_float(data)Collapse complete non-Integer scalar numerical values to Float DCIs; recursively process collections and Complex components. Arrays remain unchanged.
round_floats(data, digits)Round Float DCIs and floating/complex Array elements to decimal places; negative digits is allowed. Array type and shape are preserved.
render_output(data, *, format="shorthand", **options)Render Shorthand or YAML; options are align, padding, longhand, layout, custom_layout, and indent. YAML uses alignment and padding only.
dump_output(data, filename, *, format="shorthand", **options)Write a complete package. A filename suffix selects its format; otherwise format supplies .dcs, .yaml, .dcb, or .h5. Text formats accept the rendering options. Return the destination Path.

Numerical transformations preserve Mapping keys and external references, without retrieving payloads. Set collisions fail rather than dropping members; Multiset occurrences are retained. Rounding retains numerical expression structure; call to_float() first to approximate complete expressions. Scalar Sum and Product evaluation follows canonical operand order; scalar OrderedProduct evaluation retains factor order. Array-valued Sums and OrderedProducts retain their structure while scalar subexpressions are converted. The input DCI and its cached fingerprint remain unchanged. Evaluation failures, nonfinite results, and Array overflow are reported as exceptions.

from data_canon import (
    parse_shorthand, get_values, index_value, to_float, round_floats,
    render_output,
)
from data_canon.instance import StringDC

source = parse_shorthand('{"coordinates": [(Pi, 1/2), (E, 0)]}')
selected = index_value(get_values(source, StringDC("coordinates")), 0)
result = round_floats(to_float(selected), 3)
assert render_output(result, padding=0) == "(3.142, 0.5)"

Conversion between conventional objects and DCIs

as_dc_instance(obj, *, registry=None)

Construct a DCI from an instance of a supported conventional type. Passing an existing DCI returns that instance unchanged. An optional ConversionRegistry supplies application-owned conventional-type rules, applied recursively.

from fractions import Fraction

from data_canon import as_dc_instance

instance = as_dc_instance({
    ("Eg", 0): 2.37,
    "position": (Fraction(1, 2), Fraction(1, 4), 0.199),
    "label": "O",
    "payload": b"\x00\xff",
})
Conventional inputResulting DCT
None, bool, int, float, str, bytesNull, Boolean, Integer, Float, String, Bytes, respectively
fractions.FractionFraction
Constant (including Pi and E)Constant
Sum, Product, OrderedProduct, Power, QuotientCorresponding numerical DCT
Built-in complexComplex with Float components
ComplexNumberComplex with separately typed components
list, tuple, setList, Tuple, Set
MultisetMultiset
dictOrdered Mapping
UnorderedDictUnorderedMapping
Portable numpy.ndarrayArray
ExternalFile, ExternalArrayCorresponding external DCT

Floats and portable floating-point Array elements must be finite. Conventional bytes is supported directly; convert mutable byte buffers to bytes explicitly. Construction of a DCI cannot recover information already lost when the source conventional object was constructed or manipulated.

dc_instance_to_python(node, source=None, *, registry=None)

Construct an instance of the corresponding conventional type from a DCI, recursively converting constituent DCIs. The DCI is already a Python object; the function name identifies the implementation language, not a change from one language to another.

from data_canon import dc_instance_to_python

converted = dc_instance_to_python(instance)
assert type(converted) is dict
assert type(converted["position"]) is tuple
assert isinstance(converted["position"][0], Fraction)
DCTConventional result
NullNone
Booleanbool
Integerint
Floatfloat
Stringstr
Bytesbytes
Fractionfractions.Fraction
ConstantConstant, retaining the exact name
Sum, Product, OrderedProduct, Power, QuotientCorresponding conventional numerical classes
ComplexComplexNumber
List, Tuple, Setlist, tuple, set, respectively
MultisetMultiset
Mappingdict
UnorderedMappingUnorderedDict
RecordA registered handler’s result, or an instance of a generated collections.namedtuple class
Arraynumpy.ndarray
ExternalFileVerified payload bytes, or a content loader’s result
ExternalArrayA verified, materialized numpy.ndarray

The conversion directions are not necessarily symmetric. A conventional Fraction reduces ratios; Sum, Product, and Power apply their own simplifications. A generated named tuple is accepted as a conventional Tuple input by as_dc_instance(), so it does not reconstruct the original Record name and fields through that function.

Conventional dictionaries and sets apply their own hashability and equality rules. A hashable List DCI key becomes an unhashable conventional list, which can make conversion to a dict fail. Distinct DCI keys or Set members can also be merged if their converted conventional objects compare equal.

Without a matching custom Record handler, names and fields must satisfy namedtuple restrictions for conventional Record conversion to succeed. Array conversion returns the stored read-only NumPy array; use .copy() when an independently mutable array is wanted. Do not circumvent the stored array’s read-only protection, because mutation would invalidate the DCI’s identity and any cached fingerprint or hash.

External resources are materialized during this conversion. Their bytes are retrieved and verified using retained source context or the supplied source. ExternalFile conversion can additionally use the implementation’s filename-based content loaders. Keep the external DCI for metadata-only work.

Conversion to conventional objects and construction of a new DCI do not promise a lossless round trip for every DCI. The DCI model retains its own information and equality rules independently of the conventional result types.

NumPy scalar conversion

as_dc_instance() supports NumPy Boolean, signed/unsigned integer, portable floating-point and complex, Unicode, and byte scalars. The conversions preserve their values using Boolean, Integer, Float, Complex, String, and Bytes DCIs. Every supported NumPy integer width converts exactly to the unlimited-precision Integer DCT. Finite binary16/32 values widen exactly to binary64, preserving signed zero; complex64/128 become Complex DCIs with Float components.

Scalar width does not become identity metadata. A zero-dimensional ndarray is still an Array DCI and retains its Array type and empty shape.

Nonfinite numerical scalars are rejected. Wider floating-point/complex formats, temporal scalars (datetime64, timedelta64), and structured or opaque void scalars require an explicit conversion or registry handler. Portable dtype aliases follow their corresponding dtype rules. Custom exact-type handlers take precedence over the built-in NumPy conversions.

See NumPy scalar values.

ConversionRegistry()

Create an application-owned registry. The conversion functions accept it through registry=registry; omission retains the existing default rules.

MethodRule
register_type(conventional_type, converter)Exact conventional type → DCI
register_record(name, converter)Exact Record name → conventional object

Both methods receive a handler callable as converter(value, convert). The second argument recursively converts using the same registry. A reverse handler’s recursive converter also retains the outer source context. Conventional-to-DCI handlers must return an InstanceDC; reverse handlers receive the original RecordDC and are responsible for validating their schema.

Rules for exact conventional types take precedence over built-in conversion. Rules do not apply to subclasses implicitly. Existing DCIs always pass through as_dc_instance() unchanged, and registering a DCI implementation class is rejected. Record matching uses the complete name, without import or class lookup. Duplicate registrations, invalid types or names, and noncallable handlers raise errors. Handler exceptions propagate without fallback.

Registries do not alter global behavior, DCT definitions, canonical encoding, or parsing and persistence. No handler is serialized into the DCI. An unregistered Record retains the generated-namedtuple fallback, including its name restrictions. Writers and codecs do not accept a registry: convert to a DCI explicitly before writing and convert decoded DCIs afterward.

The Custom Conversion Rules guide defines both directions of a date convention and demonstrates nested conversion, schema validation, and canonical and file round trips.

Shorthand parsing

parse_shorthand(text, *, preprocess=False, substitutions=None)

Parse exactly one complete Shorthand expression into a DCI:

from data_canon import parse_shorthand

phase = parse_shorthand("complex(1/2, 1/2 * sqrt(3))")

The parser accepts explicit constructors and their shorter forms in any mixture:

assert parse_shorthand("fraction(1, 2)") == parse_shorthand("1/2")
assert parse_shorthand("list(1, 2)") == parse_shorthand("[1, 2]")
assert parse_shorthand("tuple(1)") == parse_shorthand("(1)")
assert parse_shorthand("set()") == as_dc_instance(set())
assert parse_shorthand('mapping("x": 1)') == parse_shorthand('{"x": 1}')

Collection constructor arguments are their members or entries. For example, list([1, 2]) is a List containing one List, not a conversion of that inner List. set() is the empty Set; {} is the empty Mapping. The old emptyset spelling is not accepted.

Strict parsing rejects trailing input and invalid DCT information. Numerical parentheses are not general grouping: (1 + 2) is a singleton Tuple containing a Sum. Use constructors when nesting needs to be explicit.

Preprocessing can be requested directly:

prepared_instance = parse_shorthand("<1 2 3>", preprocess=True)

The dc CLI preprocesses Shorthand by default and offers --strict; that frontend policy does not change this function’s strict default.

Supplying substitutions requires preprocess=True. For a clear two-stage workflow, call preprocess_shorthand() separately as shown below.

Shorthand rendering

render_shorthand(node, *, layout="standard", custom_layout=None, align=False, indent=2, padding=1, longhand=False)

Render a DCI as identity-preserving Shorthand. A conventional input object is first converted with as_dc_instance(), and the resulting DCI is rendered:

from data_canon import render_shorthand

text = render_shorthand(phase)
assert parse_shorthand(text) == phase

Shorthand has no canonical text layout. Rendering must preserve DCI equality; fingerprints use canonical bytes rather than this text.

Longhand

longhand is a Boolean, defaulting to False. With True, all structured DCIs use explicit constructors. Null, Boolean, Integer, Float, and String retain their literal spellings. Fraction uses fraction(...), and Bytes retains bytes(...). Array elements retain their specialized numerical syntax. The boundary is syntactic, not the primitive/atomic/composite classification. For example, sum(1, 1.0, fraction(1, 2)) is longhand; literals need no additional wrappers. Version 1 defines neither integer(...), float(...), nor string(...) wrappers, nor a separate fully wrapped rendering mode. The same parser accepts both outputs.

value = parse_shorthand("1/2 * sqrt(3)")
assert render_shorthand(value, layout="standard") == "1/2 * sqrt( 3 )"
assert render_shorthand(value, layout="standard", longhand=True) == (
    "product( fraction(1, 2), power( 3, 1, 2 ) )"
)
StructureDefault examplesLonghand
Fraction1/2fraction(1, 2)
Sum1 + 2sum(1, 2)
Product2 * sqrt(3)product(2, power(3, 1, 2))
Power5**2, 5**(1/3), sqrt(5)power(5, 2), power(5, 1, 3), power(5, 1, 2)
List[1, 2]list(1, 2)
Tuple(1, 2)tuple(1, 2)
Set{1, 2}, set()set(1, 2), set()
Mapping{"a": 1}mapping("a": 1)

Default rendering uses constructors to retain empty or singleton Sums and Products, nesting, and unambiguous Power bases. Quotient retains quotient(...). No separate notation or power_style option is provided.

Independent layout, alignment, indentation, and padding

layout controls line breaking and delimiter placement:

LayoutOpening delimiterClosing delimiter
standardSeparate lineSeparate line
semi-compactShares the first item or entry lineSeparate line
compactShares the first item or entry lineShares the final content line
inlineShares the only content lineShares the only content line

For example, a semi-compact Mapping renders as:

{ "a": 1,
  "b": 2
}

Scalar-only collections and empty collections normally stay inline. Lists, Tuples, Sets, and Multisets normally become multiline when they directly contain a List or Tuple, or an item already spans multiple lines. An inline subtree suppresses those line breaks. Nonempty Mappings are multiline unless their effective layout is inline. Multiline Mapping values start below their keys, so indentation does not depend on key length. Multiline keys remain valid Shorthand and use the same recursive rendering rules.

align="left" or align="right" pads cells in rectangular List/Tuple rows and supported Array tables. align=False disables column padding. Alignment can be combined with every layout and with longhand. Ragged rows receive no column padding. Alignment does not introduce a line-width wrapping threshold.

indent controls extra indentation when structural nesting places content on new lines, defaulting to 2. For example, it controls content after a standalone opener and a multiline value below its Mapping key. It does not add spacing after an opener that shares its content line. Compact continuation items align beneath the first item, whose column is determined by the opening token and padding. Consequently, changing indentation does not move compact Mapping keys; it moves nested blocks below them.

padding controls gaps beside same-line collection and constructor delimiters, defaulting to 1. For example, a flat List is [ 1, 2 ]; padding=0 gives [1, 2]. It also supplies the gap after a shared multiline opener and before a shared multiline closer. A standalone delimiter receives no padding. Continuation items begin at relative column len(opening) + padding.

Both options accept nonnegative integers, including zero, and reject Boolean, negative, and noninteger arguments. Empty collections retain [], (), set(), and similar spellings. Primitive literals and their fixed constructor spellings, item separators, and specialized Array element brackets retain their existing forms. align independently controls table-cell padding.

packed = parse_shorthand('{1, 2, [0, 1]}')
print(render_shorthand(packed, layout="compact", indent=0, padding=0))

Output:

{1,
 2,
 [0, 1]}
rows = parse_shorthand('{"rows": [(1, 20), (30, 4)]}')
text = render_shorthand(
    rows,
    custom_layout={"list": "compact", "tuple": "compact", "mapping": "semi-compact"},
    align="left",
    indent=2,
)
print(text)
assert parse_shorthand(text) == rows

Output:

{ "rows":
    [ ( 1 , 20 ),
      ( 30, 4  ) ]
}

layout is a single global layout name, defaulting to "standard". custom_layout is an optional conventional mapping from lowercase type names to layout names. Each override applies only to the named DCT; all unspecified types use the global layout. The arguments do not change one another.

Accepted override names are list, tuple, mapping, unordered_mapping, record, set, multiset, sum, product, ordered_product, power, quotient, complex, array, external_file, and external_array. Sets, Multisets, Records, and other constructor forms follow the same four layout contracts. Mapping overrides do not implicitly affect UnorderedMappings or Records. Ordinary scalar literals remain unchanged. The three multiline layouts do not force a flat sequence to break; inline suppresses structural line breaks.

text = render_shorthand(
    rows,
    layout="compact",
    custom_layout={"mapping": "semi-compact", "set": "standard"},
    align="right",
    indent=4,
)
assert parse_shorthand(text) == rows

Constructor continuation arguments align beneath the first argument, at the opening token’s length plus padding. Indentation does not change that column. Unknown layouts or type names are rejected. layout does not accept a mapping. align defaults to False; only False, "left", and "right" are accepted. Settings are passed per call.

Force one line globally or for a subtree

layout="inline" renders the selected value and every descendant on one line. An inline parent overrides child layout choices inside that subtree. Alignment is disabled there, and indentation has no effect; padding still applies. String line breaks remain escaped within their literals. No input is evaluated or mathematically simplified.

text = render_shorthand(rows, layout="inline", padding=0)
print(text)
assert parse_shorthand(text) == rows

Output:

{"rows": [(1, 20), (30, 4)]}

Use custom_layout={"list": "inline"} to keep Lists on one line while other values follow their selected layouts. A Mapping or Array inside such a List is also inline, even if its own type override requests standard. Siblings outside that List retain their selected layouts. A type override is consulted at each node unless an enclosing inline value has already forced the subtree.

Array element brackets retain their specialized numerical syntax; Array elements are not List DCIs. The Array constructor follows its selected layout, and structural Array nesting uses the requested indentation. Scalars retain their existing spellings.

Rectangular rows and line breaking

Outside an inline subtree, Lists, Tuples, Sets, and Multisets become multiline when they directly contain a List or Tuple, including an empty one, or when an item already renders across multiple lines. Scalar and inline numerical items do not independently force breaks. Flat sequences have no automatic line-width threshold.

Alignment recognizes rectangular List or Tuple rows inside any of those four collection types and preserves each row’s DCT. Short Sums, Products, and Powers can occupy a single cell. Ragged rows receive no table padding. The rules also apply to longhand constructors. Column padding may change across a table when one cell grows; use align=False when minimizing such whitespace changes matters for version-control diffs.

Numerical conventional types

from data_canon import Sum, Product, OrderedProduct, Power, Quotient, ComplexNumber

The numerical conventional types accept int, finite float, fractions.Fraction, and instances of the supported numerical conventional types. Boolean is not a numerical operand. Their construction, equality, and arithmetic rules are distinct from the associated DCT rules.

Sum(*items) and Product(*items)

Construct variadic sums and products:

a = Power(2, 1, 2)
b = Power(3, 1, 2)
assert Sum(b, a, 3*b) == Sum(4*b, a)
assert Product(a, a) == Product(Power(2, 1))

For scalar-only operands, conventional Sum and Product flatten instances of their own type, combine matching terms or factors, and preserve first-occurrence presentation order. Equality compares exact normalized values across supported numerical types and ignores operand order. Conventional float contributions remain separate from exact rational quantities; matching float contributions can combine with one another. Scalar multiplication distributes over conventional scalar Sum objects. Array-valued Sums retain structure until explicit evaluation.

.items is the immutable tuple of simplified operands. Conventional Sum and Product objects are frozen and unhashable. Sum and Product DCIs are hashable and retain their prescribed DCI structure independently of conventional simplification.

Power(base, numerator, denominator=1)

Construct a symbolic positive rational power:

root = Power(3, 1, 2)
cube_root = Power(5, 1, 3)
assert Power(5, 2, 4) == Power(5, 1, 2)

The exponent fields must be positive Python int values; conventional Power reduces their ratio. A Power DCI instead retains its direct integer exponent fields exactly. Conventional Power extracts an exact root of a rational base when the reduced exponent’s denominator permits it, retaining the remaining integer exponent. For example, Power(4, 1, 2) stores base=2, numerator=1, denominator=1, and Power(16, 3, 2) stores base=4, numerator=3, denominator=1. Power(4, 8, 1) retains all three fields; construction does not expand it to Power(65536, 1, 1). Without an exact denominator root, the base and reduced exponent remain unchanged.

Float bases follow the same construction rule, provided the extracted root is exactly representable as a float. Even roots of known negative scalar bases are rejected, and odd roots use the real branch. Nested scalar-base powers combine exponents with the real branch accounted for.

Equality and explicit numerical conversion still recognize the value of a retained integer power:

integer_power = Power(4, 8)
assert (integer_power.base, integer_power.numerator, integer_power.denominator) == (4, 8, 1)
assert integer_power == 65536
assert int(integer_power) == 65536

Surrounding Sum, Product, and Quotient arithmetic may evaluate retained powers when simplifying its operands. DCI conversion stores the conventional object’s current base and exponent fields; it does not use the cached comparison form.

Quotient(numerator, denominator)

Represent a structured ratio, including a Sum or Product denominator:

ratio = Quotient(1, Sum(a, b))
assert 1 / Sum(a, b) == ratio

The conventional constructor reduces rational coefficients, cancels common factors, and rejects denominators recognized as zero. It retains the Quotient type and does not rationalize a Sum denominator during construction. Exact cross multiplication can recognize a rationalized equivalent during comparison. The Quotient DCT’s structural validity rules are separately defined by Core.

Numerical equality

Comparison uses exact rational-expression normalization, with cached comparison forms, rather than comparing float() results. Float leaves contribute their actual binary rational values: Quotient(1, 2) == 0.5, but Quotient(1, 10) != 0.1. Float and rational storage remain separate during construction.

The supported normalization is not complete mathematical equality for arbitrary expressions. Fractional powers of nonrational bases remain symbolic; integer factorization is bounded to trial divisors through 1000 and residual perfect powers through degree 64. Unresolved factors are preserved exactly, so some mathematical equivalences may not be recognized. See Conventional Types for examples and comparison limits.

Explicit float() and int() conversion

assert float(Sum(1, 1.0)) == 2.0
approximation = float(ratio)
assert int(Quotient(-7, 3)) == -2

Conversions evaluate without modifying the object. int() truncates toward zero. Exact integer and Fraction intermediates remain exact where possible; fractional powers follow math.pow. Domain errors, division by zero, and overflow can raise Python exceptions. Numerical evaluation operates on the conventional objects, not on DCIs.

ComplexNumber(real, imaginary)

Construct a conventional ComplexNumber object with separately typed numerical components. Equality compares numerical component values, including against supported real scalars and built-in complex values. Conventional ComplexNumber is unhashable. Built-in complex stores floating-point components:

z = ComplexNumber(Fraction(1, 2), Product(Fraction(1, 2), Power(3, 1, 2)))
assert as_dc_instance(z) == phase
approximate_complex = complex(z)

The conventional ComplexNumber type supports arithmetic and conjugation. complex(z) requests floating-point complex components.

OrderedProduct and Array-valued Sum

OrderedProduct(*factors) stores real conventional scalar values, NumPy arrays, and other array-expression objects in order. Sum(*terms) also accepts Arrays and OrderedProducts. A scalar-only Sum retains its existing normalization rules. Scalar Product, Power, Quotient, and ComplexNumber components reject Array-valued structures.

  • expression.evaluate() evaluates Array structure into an object-dtype NumPy Array, retaining exact conventional scalar entries.
  • np.asarray(expression, dtype=float) or dtype=complex explicitly requests machine values. Without a dtype, exact object entries remain.
  • Array-valued Sum requires matching shapes at any rank. OrderedProduct requires rank-two Arrays and compatible multiplication dimensions. Scalar factors scale the result; no broadcasting, implicit identity insertion, or reshaping occurs.
  • Construction does not infer shapes or multiply Arrays. Conversion from a valid DCI can still fail if a conventional scalar subexpression is invalid, as with a Quotient having a recognized zero denominator.
  • Conventional whole-Array equality returns a Boolean using exact scalar comparisons. Array-valued expressions cannot be converted to scalar float/int.

is_numerical_dci(node) and is_scalar_numerical_dci(node) are available from data_canon.instance. Both classify structure without mathematical evaluation. OrderedProduct DCIs use OrderedProductDC(items_tuple); their immutable operands are exposed through .items. Sum DCIs remain unordered and preserve multiplicity.

Bytes and collections

Conventional bytes

payload = as_dc_instance(b"\x00\xff\x80")
assert payload == parse_shorthand('bytes("00ff80")')

A Bytes DCI has byte-for-byte identity and no interpretation metadata. To store metadata alongside a payload, use a Record:

from data_canon.instance import BytesDC, Field, RecordDC, StringDC

blob = RecordDC("Payload", (
    Field("media_type", StringDC("application/octet-stream")),
    Field("filename", StringDC("sample.bin")),
    Field("data", BytesDC(b"\x00\xff\x80")),
))

Record and field names are direct Unicode information. Field values remain DCIs whose DCTs participate in identity.

Multiset(iterable)

from data_canon import Multiset

members = Multiset(["O", "Sr", "O"])
members.add("O")
assert as_dc_instance(members) == parse_shorthand('multiset("Sr", "O", "O", "O")')

Multiset() is empty. Duplicates are retained, and order is insignificant for equality. Conventional Multiset uses its members’ equality rules and supports unhashable members when their equality yields a usable Boolean. It does not impose DCI equality on conventional members.

UnorderedDict

from data_canon import UnorderedDict

first = as_dc_instance(UnorderedDict({"a": 1, "b": 2}))
second = as_dc_instance(UnorderedDict({"b": 2, "a": 1}))
assert first == second

Conventional dict objects construct ordered Mapping DCIs. UnorderedDict retains Python dictionary-key restrictions and cannot recover keys already merged by conventional equality.

Canonical encoding and fingerprints

canonical_encode(node) and canonical_decode(data)

from data_canon import canonical_encode, canonical_decode

encoded = canonical_encode(instance)
assert type(encoded) is bytes
assert canonical_decode(encoded) == instance

Encoding includes every DCI’s one-byte DCT identifier and its prescribed payload. Variable-type constituents use complete DCI encodings. Fixed integer fields in Fraction and Power use signed integer payloads directly. Record names and path components use UTF-8 byte counts and bytes directly, without String identifiers.

Array elements use logical C order and portable element representations, with multibyte values in big-endian order. Canonical encoding is independent of Shorthand and NumPy memory layout.

canonical_decode() requires conventional bytes, consumes exactly one complete DCI, and rejects malformed or noncanonical input. External metadata decodes without resource retrieval.

fingerprint(node)

from data_canon import fingerprint

identifier = fingerprint(instance)
assert type(identifier) is str
assert len(identifier) == 64

The Python return value is lowercase hexadecimal SHA-256 of the canonical bytes. The abstract fingerprint is 32 digest bytes. Equal DCIs have equal fingerprints; a matching digest is not the definition of equality.

Python hashing of DCIs

All 24 DCT implementations support Python hash() and can be used as dictionary keys, including Arrays, Lists, Sets, and nested structures:

from data_canon.instance import FloatDC, IntegerDC, ListDC

key = ListDC((IntegerDC(1), FloatDC(-0.0)))
lookup = {key: "sample"}
assert lookup[ListDC((IntegerDC(1), FloatDC(-0.0)))] == "sample"

Hash computation is lazy. Constructing a DCI does not compute its hash or fingerprint. Null, Boolean, Integer, Float, String, Bytes, and Fraction DCIs use direct hashes; Float uses its binary64 bits. Structured DCIs use their cached fingerprint bytes, computing the fingerprint on the first hash request if it is not already available. Nonsemantic source context and payload caches do not participate in the hash.

Python hashes satisfy the equality requirement that equal DCIs have equal hashes. Hash values need not distinguish unequal DCIs and are not stable cross-process identifiers. Use fingerprint() for the portable identifier. Hashability of a DCI does not imply hashability of its converted conventional object: a List DCI and a conventional list remain different types.

Persistence

write(data, filename=None, format="shorthand")

from data_canon import read, write

written = write(instance, filename="output_data.dcb")
assert read(written) == instance
formatDefault suffixContents
"shorthand".dcsUTF-8 strict Shorthand
"bytes".dcbRaw canonical binary encoding
"hdf5".h5DataCanon HDF5

A recognized filename suffix overrides format selection. .hdf5 is also recognized. Without a suffix, the selected format’s suffix is appended. Without a filename, the fingerprint supplies the filename stem. The return value is a pathlib.Path.

The current writer verifies and persists referenced external resources. For .dcs and .dcb, their paths are relative to the output directory; HDF5 embeds them separately. This public interface has no metadata-only export flag. A write can fail when required resources are missing or conflicting.

read(filename)

Select the format by suffix and reconstruct a DCI. Reads preserve nonsemantic source context for later external-resource access. They do not materialize external payloads.

write_hdf5(node, filename, *, store_fingerprint=False) and read_hdf5(filename)

from data_canon import read_hdf5, write_hdf5

write_hdf5(instance, "example.h5", store_fingerprint=True)
assert read_hdf5("example.h5") == instance

Arrays use native HDF5 numerical datasets; Bytes uses a rank-one uint8 dataset. Fixed metadata fields reuse integer/string payload storage without DCI kind attributes. Genuine constituent DCIs retain their required kind attributes. HDF5 file bytes are not canonical DCI bytes.

store_fingerprint adds optional file-root metadata. It does not add a DCI constituent. Complete HDF5 writes verify external payloads before embedding them.

External-reference construction types

ExternalFile(path, _data=None)

Construct a conventional ExternalFile object from a relative path to an existing file, or from contained input bytes or text:

from data_canon import ExternalFile

resource = ExternalFile.from_bytes(b"input data", filename="input.bin")
resource_instance = as_dc_instance(resource)

from_bytes(data, filename=None, directory="external_dc") retains input bytes as a nonsemantic cache. from_string(text, filename=None, directory="external_dc") encodes UTF-8. If omitted, the filename is derived from the content digest. The resulting DCI contains a relative path and digest, not the payload itself.

ExternalArray(data, directory="external_dc", filename=None, compression=False)

import numpy as np
from data_canon import ExternalArray

external_array = as_dc_instance(ExternalArray(
    np.array([1.0, -0.0], dtype="float64"),
    filename="values.bin",
))

Conversion records portable type, shape, relative path, and SHA-256 of the canonical Array element stream. Cached data and source context do not affect identity. Metadata reads need no payload; conversion of the ExternalArray DCI to a conventional NumPy array materializes and verifies it.

Compress external Array storage

compression is a Boolean and defaults to False. Choose a filename stem; the constructor appends .bin or .bin.gz and stores the finalized filename:

forces = np.zeros((32, 32), dtype="float64")
compressed = ExternalArray(forces, filename="forces", compression=True)
assert compressed.filename == "forces.bin.gz"
compressed_instance = as_dc_instance(compressed)
assert compressed_instance.path.components[-1] == "forces.bin.gz"
Filename argumentcompression=Falsecompression=True
Omitted<digest>.bin<digest>.bin.gz
"forces"forces.binforces.bin.gz
"forces.bin"AcceptedError
"forces.bin.gz"ErrorAccepted

A filename is a nonempty single component. Explicit suffixes must match the setting; suffixes other than .bin and .bin.gz are rejected. Use directory for path prefixes. Compression is encoded by the finalized path, not a new DCI constituent. The digest always covers the uncompressed canonical element bytes.

write(compressed_instance, filename="forces.dcs")
compressed_restored = read("forces.dcs")
assert compressed_restored == compressed_instance
assert np.array_equal(dc_instance_to_python(compressed_restored), forces)

Directory output writes external_dc/forces.bin.gz; retrieval transparently decompresses, verifies the digest, and validates shape, type, and elements. Malformed gzip and excess decompressed data are rejected. Existing conflicting resources are not overwritten. HDF5 output embeds logical bytes in a chunked gzip-compressed dataset (level 6), retaining the same DCI path and digest:

write(compressed_instance, filename="forces.h5")
assert read("forces.h5") == compressed_instance

Exporting that HDF5 DCI to a directory recreates the gzip file. Empty and scalar Arrays are supported. This option does not compress native Array DCIs or opaque ExternalFiles, and does not add .dcb.gz representation support. A .gz ExternalFile still means its exact opaque bytes. Raw canonical element payloads use .bin, not .dcb, because they are not complete DCI encodings. The runnable examples/external_array_compression.py demonstrates directory/HDF5/directory transfer with a zero-heavy force-constant Array.

Preprocessing

preprocess_shorthand()

The common call form is:

from data_canon import preprocess_shorthand

prepared = preprocess_shorthand("<1 2 3>")
assert parse_shorthand(prepared) == parse_shorthand("[1, 2, 3]")

Transformations run in order: compact List expansion, external and inline ordered textual substitution, then explicit scalar eval(...) in the body. Local definitions may use external names and earlier local definitions. Each local definition resolves its own explicit eval(...) before becoming available to later definitions. Self-references and forward references are rejected. Replacements are literal text; use constructors to preserve nested numerical expression boundaries. The result is text. Quoted Strings, including Bytes hexadecimal arguments, are protected.

See the Shorthand Preprocessing guide for separate introductions to each feature.

YAML interchange

parse_yaml(text) and render_yaml(node, *, align=False, padding=1)

from data_canon import parse_yaml, render_yaml

yaml_instance = parse_yaml("""
'$ ("Eg", 0)': 2.37
name: calculation
phase: $ complex(1/2, 1/2 * sqrt(3))
payload: $ bytes("00ff80")
""")
assert parse_yaml(render_yaml(yaml_instance)) == yaml_instance
assert dc_instance_to_python(yaml_instance)[("Eg", 0)] == 2.37

Native YAML sequences are Lists; mappings become ordered Mappings. The $ prefix is interpreted after YAML String decoding and invokes strict Shorthand. The $ marker requires a whitespace boundary; $100 and ${HOME} remain literal Strings. A lone $ is reserved but lacks the required embedded DCI. Quoting alone does not escape a marker; a backslash does (\$ 1/2). Literal reserved prefixes are escaped by the writer. Native key collisions or unhashable converted keys cause the entire Mapping to use the airlock.

Bytes, Multiset, and all structured numerical DCTs use embedded Shorthand. YAML does not implicitly preprocess embedded text or retrieve external resources. Its presentation is not canonical and is not fingerprinted directly.

YAML uses block-style Lists and Mappings, keeps single-line scalars unwrapped, and uses literal blocks for multiline Shorthand. Empty collections retain [] and {}. Scalar quoting is left to YAML’s syntax and scalar-resolution rules.

rows = parse_shorthand("[(1, 2), (30, 4)]")
yaml_text = render_yaml(rows, align="left", padding=0)
assert parse_yaml(yaml_text) == rows

align accepts False (default), "left", or "right". Lists of rectangular, nonempty, flat Tuple rows share column alignment; ragged or mixed rows retain ordinary formatting. Embedded Shorthand also receives the alignment setting. padding is a nonnegative integer, defaulting to 1, with the same same-line delimiter contract as render_shorthand(). Padding affects $ text rather than YAML indentation, and applies to embedded keys as well as values.

Specification and draft compatibility

The seven specifications define the language-independent contracts. The optional Conventional Types companion defines common conventional behavior; this API documents the Python binding. Core conformance does not require conventional arithmetic. The current fixed-field revision changes canonical bytes and fingerprints from earlier drafts. Regenerate affected old-draft .dcb files; automatic legacy-format detection is not provided.

4 - Publication Outline

This outline proposes a paper for the Journal of Open Source Software (JOSS) describing DataCanon and its Python reference implementation.

The paper should explain the research problem, the software’s contribution, and the evidence supporting its use. The seven specifications and documentation carry the detailed type definitions, encoding rules, and API reference.

Working title

DataCanon: Deterministic Identity and Persistence for Structured Scientific Data

An alternative retaining the original emphasis is:

DataCanon: Deterministic Semantic Identity for Structured Scientific Data

A different choice which tries to capture the versatility for exploring data:

DataCanon: The Last Mile of Scientific Data

Canonical Identity Across Programming Languages and Storage Formats

Central thesis

Scientific software needs a precise way to say what structured data is, independently of how that data happens to be represented, stored, or manipulated. DataCanon provides a formal, language-independent data model for structured scientific data together with canonical identity.

DataCanon defines DataCanon Types (DCTs), their valid DataCanon Instances (DCIs), and their equality relations. Every DCI has a unique canonical byte encoding. Human-readable Shorthand, YAML interchange, and HDF5 represent that same data without defining its identity.

This distinction should be explicit throughout the paper. DataCanon is not a database, merely a serialization format, a schema system, or a programming language. Those systems answer related but different questions. DataCanon’s core question is: what data object is this? Representation asks how to communicate it; persistence asks how to store it; a database asks how to organize, locate, query, and update collections of it; computation asks what operations to perform on it.

The scientific motivation should lead: exact numerical structures built from Fractions, Products, Powers, Sums, and Quotients, structured Complex values, typed multidimensional Arrays, and reliable persistence of nested scientific data. The paper should then explain how canonical identity makes those structures portable and reproducibly identifiable.

JOSS format and scope

The current JOSS paper guidance sets a 750–1,750-word range and identifies sections for Summary, Statement of need, State of the field, Software design, Research impact statement, AI usage disclosure, Acknowledgements, and References.

Organize the manuscript around those sections. Aim for approximately 1,400–1,600 words of prose, leaving room to adjust the emphasis during writing. The word allocations below are planning targets, not journal requirements.

Section Approximate words


Summary 150 Statement of need 250 State of the field 325 Software design 400 Research impact statement 250 AI usage disclosure and Acknowledgements 100

The submitted paper concerns usable research software. The Python package, its tests, documentation, and scientific applications should therefore have a substantial role. Independent implementations strengthen the portability evidence, but should be described as completed work only when they have been implemented and validated.

1. Summary

Introduce DataCanon in language accessible to a researcher who has never used it. Describe it first as a formal, language-independent data model for structured scientific data with canonical identity, rather than as a serialization or persistence format.

Explain that DataCanon allows complex scientific data to be constructed, communicated, compared, fingerprinted, and persisted while retaining an identity independent of storage layout and implementation language.

Briefly identify the distinctive capabilities:

  • Exact structured numerical data, including irrational Powers, structured Quotients, and Complex values with independently typed components.
  • Portable typed Arrays alongside recursively structured collections.
  • Deterministic binary identity and SHA-256 fingerprinting.
  • Human-readable input and identity-preserving YAML and HDF5 representations.
  • External-resource references with deferred retrieval and integrity checks.

Describe the Python package as the reference implementation of a language-independent specification. Avoid an API inventory or encoding tutorial in the summary.

2. Statement of need

Research problem and audience

Address developers of long-lived computational science and engineering software, especially workflows that combine exact numerical structure with large machine Arrays.

Explain that scientific software often has no difficulty storing data, but still lacks a representation-independent answer to more basic questions: What type of data object is this? When are two independently constructed objects the same? Which structural distinctions participate in identity? Can that identity survive a change of programming language or storage technology?

Use a concrete scientific object: a crystal structure or symmetry calculation containing lattice Arrays, fractional coordinates, exact complex phase factors, parameters, and externally stored numerical results.

Such an object may need to survive storage changes, support reproducible identification, and move between implementations while retaining distinctions that matter to the application.

Scientific example

Use a structured complex phase factor as the central numerical example:

complex(1/2, 1/2 * sqrt(3))

This preserves the exact real and irrational imaginary components instead of immediately replacing both with floating-point approximations. Discuss why this is useful for crystallographic or symmetry data, alongside ordinary floating-point Arrays required by numerical calculations.

Mention that DataCanon preserves type and constituent structure without mathematical simplification. Detailed normalization edge cases belong in the specifications rather than the motivation.

Practical outcome

Explain what the package enables: identity-preserving disk round trips, format transfers, stable fingerprints of scientifically meaningful data, and communication of the same DCI across independent implementations.

If a code fragment is needed, use one short persistence example; leave a full walkthrough in Getting Started.

3. State of the field

Use this section not only to compare features, but to establish what kind of thing DataCanon is. One of the easiest ways to understand DataCanon is to distinguish the data model from neighboring layers that solve different problems.

A useful organizing framework is:

LayerPrimary question
Data modelWhat is the data?
Representation / serializationHow is the data communicated or encoded?
PersistenceHow is the data stored and recovered?
DatabaseHow are collections of data organized, located, queried, and updated?
ComputationWhat operations are performed on the data?

DataCanon fundamentally addresses the first question. Its specifications and software also provide representations and persistence mechanisms, but those do not define DCI identity. DataCanon can therefore use or coexist with systems at the other layers rather than replacing them.

Comparison with generic data models

Compare DataCanon first with generic structured-data models such as JSON and CBOR, not only with their concrete serializations. This comparison establishes the conceptual neighborhood: recursively structured, language-independent data whose values can be realized in multiple programming languages.

A compact comparison table would be valuable in the paper. The exact entries must be verified against primary specifications during the literature review; the table below records the questions and intended comparison rather than final publication claims.

CapabilityJSONCBORDocument / NoSQL modelsDataCanon
Language-independent structured-data modelCompareCompareCompareYes
Recursive heterogeneous structuresCompareCompareCompareYes
Distinct List and Tuple semanticsCompareCompareCompareYes
Set / Multiset semanticsCompareCompareCompareYes
Structured exact numerical typesCompareCompareCompareYes
Typed multidimensional machine ArraysCompareCompareCompareYes
Explicit type-and-constituent equalityCompareCompareCompareYes
Canonical identity intrinsic to the modelCompareCompareCompareYes

The point is not that other models are deficient. They make different choices for different purposes. The literature review should identify which DataCanon distinctions are unusual or useful for scientific software without claiming that canonicalization, portability, or structured data are unique inventions.

CBOR as a data model and binary encoding

Treat CBOR as a particularly important comparison, but distinguish its underlying data model from its binary encoding conventions. CBOR can represent additional scientific structures through tags and prescribed conventions. DataCanon’s contribution is not that CBOR is incapable of representing its DCTs; it is the explicit scientific type system, constituent structure, equality contract, and canonical identity.

A CBOR-based encoding of DCIs would remain possible without changing the DataCanon semantic model. However, it would require a fixed mapping of DCTs to CBOR values, rules for structured numerical types and Arrays, and a frozen deterministic encoding profile. Avoid claiming that CBOR cannot meet these requirements. Instead, distinguish the semantic need for DataCanon from the engineering choice of canonical byte encoding.

Comparison with document / NoSQL systems

Use document databases as a particularly intuitive comparison. DataCanon shares the document-model intuition that a scientifically meaningful object may naturally be one recursively structured object rather than something that must first be decomposed into relational tables.

The abstraction level is nevertheless different. A document database is primarily concerned with storing, retrieving, indexing, querying, and updating documents. DataCanon instead specifies the types, constituent structure, equality, and canonical identity of the data itself. A DCI could therefore be stored in a document database; the two systems need not compete.

Avoid treating “NoSQL” as one formal data model. Distinguish document databases from key-value, wide-column, graph, and other systems when preparing the final literature review. The document model is the relevant comparison here.

Comparison with representations and persistence formats

Then compare against technologies operating at neighboring layers: JSON canonicalization, deterministic CBOR, schema-driven serialization, YAML tooling, HDF5, and scientific data containers used by the intended audience. Select the closest alternatives rather than cataloguing every serialization format.

Evaluate the distinctions that motivate DataCanon:


Concern Question for the comparison


Identity What equality relation and canonical encoding are provided?

Scientific types How are structured exact numbers and typed Arrays represented?

Collections Are ordered and unordered structures distinguished?

Persistence Can storage layout change while retaining the same identity?

External data How are payload metadata, content verification, and retrieval separated?

Interoperability What agreement is required across implementations?

Give a concrete reason for developing DataCanon rather than extending a particular existing package. The proposed contribution is the combination of an explicit scientific data model, canonical identity, and multiple faithful representations. Existing formats may provide related capabilities or support extensions; comparisons must establish the relevant differences rather than claiming that canonicalization or portability is unique to DataCanon.

Explain that DataCanon deliberately uses mature facilities such as NumPy and HDF5 instead of replacing their numerical and storage infrastructure. The same principle applies more broadly: DataCanon defines identity at the data-model layer and can interoperate with representation, persistence, database, and computational systems built for other concerns.

4. Software design

Organize this section around the design decisions that make the abstract data model usable in scientific software. Because Section 3 establishes the data-model / representation / persistence distinction, avoid repeating that conceptual argument here; show how the software realizes it. Keep low-level byte tables in the specifications.

Identity is separate from representation

Define DCT and DCI briefly. Equality is specified by types and constituents; canonical encoding represents that equality injectively:

x = y  if and only if  E(x) = E(y)

Fingerprinting is:

F(x) = SHA-256(E(x))

Shorthand formatting does not determine identity. Distinguish exact canonical byte equality from digest comparisons, which rely on collision resistance.

Rationale for the canonical byte encoding

Explain why Core specifies its own canonical bytes instead of adopting CBOR’s binary encoding or a deterministic CBOR profile. The present encoding follows the DCT structure directly: a type identifier and a prescribed payload, with shared rules for lengths, constituents, and machine values. This favors a small, auditable specification and direct independent implementations over introducing another encoding layer.

Canonical bytes are normative to DataCanon identity, not an incidental serialization choice. Once Version 1 is stable, changing the encoding would change fingerprints even if DCI equality remained unchanged. An alternative CBOR-based encoding could preserve the same semantics, but would require an equally precise and version-stable byte contract.

Acknowledge the potential advantages of CBOR-style compact headers and established encoding tools, particularly for small nested objects. Do not claim that the current encoding is smaller or faster without measurements. The justification is presently architectural minimalism, clarity, and direct control, not demonstrated performance superiority.

Scientific types and machine Arrays

Version 1 defines the DCTs in the Core specification. In the manuscript, highlight composable numerical structures, Complex, Record, Multiset, Bytes, Array, and external references rather than explaining every type. Verify the final DCT count against the publication release rather than baking a draft count into the prose.

Numerical DCIs include Integer, Float, Fraction, Sum, Product, Power, and Quotient, together with any additional numerical DCTs present in the finalized Version 1 specification. This is a category for constituent restrictions, not an additional DCT. Type membership makes no assertion about mathematical evaluability.

Explain the composition rule with a concrete example. Fraction’s prescribed integers are direct information. A Complex component or Power base is a DCI, whose individual DCT and recursive structure matter. Encoding reuses shared payload rules for direct fields and complete DCI encodings for variable-type constituents.

Arrays retain portable type, shape, and element identity. Their elements are machine values, not individually tagged DCIs. Portable Array types support efficient numerical containers; canonical elements use logical C order and prescribed big-endian multibyte representations, independently of native strides or layout. Nested Lists remain available for individually typed constituent data.

Distinguish the fully specified Core Array identity from Shorthand inference and conversion rules where implementation-reference behavior remains relevant.

Order belongs to the data model

Mapping entry order participates in identity; UnorderedMapping entry order does not. Likewise, Lists and Tuples are ordered, while Sets are unordered. Multiset retains repeated members; numerical collection types likewise retain the multiplicity and nesting prescribed by Core. Canonical binary encoding orders unordered constituents according to the finalized Core rules. That encoding order is not a numerical comparison or a mandated text-rendering order.

Text and conventional-type conversion

One strict Shorthand grammar accepts explicit constructors and shorter forms. Longhand uses constructors for structured values and ordinary literals where specified by Shorthand. The boundary is syntactic rather than simply the Core type classification. Formatting choices preserve DCI identity.

Define conventional types as types used directly for data and computation in a programming language, including built-in, library-provided, and user-defined types. The conversion interface constructs instances of conventional types from DCIs and constructs DCIs from supported conventional objects. A ListDC object and a conventional list object are both Python objects, but their types and equality contracts differ.

Existing conventional types include list, Fraction, and NumPy arrays. The reference implementation supplies new conventional types where suitable types are missing. Arithmetic belongs to the numerical conventional types, not to the DCT identity rules. Conversion through conventional objects need not preserve an arbitrary DCI’s identity.

Present conventional arithmetic as an optional companion interface in the paper, without introducing a separately named arithmetic facility. The seventh specification, Conventional Types, defines common behavior for independent implementations, with conformance declared per type. Core does not require that support. Link the Conventional Types guide for worked examples and the companion specification for its behavioral requirements and comparison limits.

Contained and external payloads

Bytes stores an opaque byte sequence directly in the DCI. A Record can hold its interpretation metadata. This permits a canonical binary file to contain both structured scientific data and binary payloads without companion files. External references serve the distinct purpose described below.

External payloads and persistence

ExternalFile and ExternalArray retain relative paths and content digests as semantic metadata. ExternalArray also carries portable type and shape. External bytes are not needed to decode or fingerprint their metadata.

Source location, cached bytes, and retrieval state are nonsemantic. Retrieval verifies digests and, for ExternalArray, element-stream validity. Complete packages preserve those references while placing payloads in directory-backed storage or embedding them in HDF5.

Seven specifications and the Python implementation

Summarize the specification boundaries:


Specification Responsibility


Core DCTs, equality, canonical bytes, and fingerprints

Shorthand Strict text parsing and identity-preserving rendering

Shorthand Preprocessing Optional compact Lists, substitutions, and explicit eval(...)

YAML Interchange Native YAML conversion and the $ convention

HDF5 Portable object layout, numerical datasets, and embedded payloads

Persistence Format selection, resource namespaces, verification, and transfer

Conventional Types Optional conventional construction, arithmetic, comparison, collections, and conversion contracts

Connect the contracts to explicit conversion, encoding/decoding, parser/renderer, and persistence boundaries. The DCTs and DCI equality are language-independent. The optional Conventional Types companion specifies a common behavioral foundation. Language bindings document their supported types, interfaces, additional simplifications, and conversion choices. Python classes are examples of a binding rather than normative DCT definitions.

Core does not perform computation. Optional preprocessing performs explicitly requested arithmetic before strict parsing; it does not alter DCT identity.

5. Research impact statement

Build this section from specific evidence available at submission time. Distinguish actual use from proposed applications.

Possible evidence to collect includes:

  • A documented Principia Materia integration showing which scientific objects use DataCanon and what workflow it supports.
  • Reproducible crystallographic or symmetry examples using exact structured Complex values and typed Arrays.
  • Verified format transfers preserving canonical bytes and fingerprints.
  • Benchmarks for representative numerical workloads, if performance is a material reason to adopt the package.
  • Independent users, integrations, or publications, if available.

The repository tree shows tests covering conversion, canonical encoding and vectors, parsing/rendering, YAML, HDF5, and external sources. Report test results and fixtures only after running and inspecting them; filenames alone are not validation evidence.

If independent language conformance is completed, include independently generated matching canonical bytes and fingerprints, all portable Array types, and bidirectional HDF5 fixtures. Text layout and HDF5 file bytes need not match. Report completed coverage and remaining limitations explicitly.

Deferred canonical-encoding evaluation

Record a possible future comparison of the current encoding with a CBOR-inspired alternative, holding the DCT model and equality rules fixed. Compare encoded size, encoding and decoding speed, compressed size, and specification/implementation complexity. Cover every DCT, small and deeply nested objects, contained Arrays across dtype, rank, and size, ExternalArrays, and representative scientific datasets. Do not assume that large Arrays are always externalized or that any one scientific workload is representative.

This evaluation is worthwhile but is not a prerequisite for completing the remaining Version 1 implementation work. Revisit the byte encoding before finalizing the canonical identity contract if evidence reveals a substantial benefit. Otherwise retain the simpler current design.

6. AI usage disclosure

Prepare an accurate account of AI assistance with software development, documentation, or manuscript preparation, together with the human review and verification performed. This documentation and outline have received AI assistance. The implementation, tests, specifications, and documentation have also received AI assistance during this redesign. Record the reviewed scope and the actual human checks performed before preparing submission text.

Record the tools and available version information when preparing the submission. Do not describe syntax-only checks as execution, interoperability validation, or scientific verification.

7. Acknowledgements

Identify contributors, institutional support, and funding actually associated with the software. Obtain the correct grant identifiers and attribution before submission.

8. References

Prepare software and primary-source citations for the closest alternatives, the numerical/storage facilities discussed, and scientific applications used as evidence. Link the versioned DataCanon specifications and archive the software release associated with the paper.

Version 1 draft revisions

Keep this section synchronized with the final Version 1 specification. The numerical model and DCT inventory have changed during development, so publication fixtures must use the finalized encodings and terminology rather than incompatible earlier drafts.

Do not mix results from incompatible draft revisions. Tests should cover the full finalized numerical structure, longhand and shorter text forms, byte payloads, multisets, and transfers preserving the current canonical bytes.

Supporting material and remaining work

The documentation should provide the material omitted from the short paper: full type definitions, byte encodings, fixed conformance vectors, API signatures, numerical conventional-type behavior, longhand rendering, preprocessing details, and persistence examples.

Before finalizing the manuscript, resolve or clearly delimit the portability items already recorded in the seven drafts, especially implementation-reference behavior, preprocessing numerical semantics, and HDF5 datatype validation. Confirm the package’s license, release, installation instructions, tests, contribution guidance, and cited scientific evidence against the actual repository. The checklist identifies items to inspect, not claims that they are currently missing.

A longer technical treatment could develop the full model and conformance results separately. The JOSS paper should stay centered on the research software, its design choices, and demonstrated usefulness.

5 - Reference

Version 1 defines 24 DataCanon Types (DCTs). A DataCanon Instance (DCI) is an instance of one DCT. Instances of different DCTs are never equal. Each DCT specifies its information and validity rules; canonical bytes encode that identity independently of textual presentation.

Named mathematical constants

Constant stores the name pi or e, identifying the exact mathematical constant without a decimal approximation. Shorthand uses Pi and E; longhand uses constant("pi") and constant("e"). A Constant DCI is distinct from every Float approximation and is permitted as a Numerical DCI.

Conventional types and DCT implementations

A conventional type is a type used directly for data and computation in a programming language, including built-in, library-provided, and user-defined types. Conversion interfaces connect DCIs with instances of conventional types.

DCTs and conventional types have their own rules. For example, Python’s ListDC class implements the List DCT. A ListDC instance and a conventional list instance are Python objects of different types. Conversion connects the types without changing programming languages. A conventional Fraction or Sum may simplify its contents, while Fraction and Sum DCIs retain their prescribed structure.

The Conventional Types guide introduces the relationships and operations. Exact conversion directions and limitations belong to the Python API. The optional Conventional Types specification defines the common behavioral contract for independent language implementations. Its arithmetic and comparison requirements do not alter the DCT identity rules summarized below.

Primitive types

Primitive DCTs specify their information directly, without constituent DCIs. This includes Fraction’s two prescribed integer fields.

DCTInformationExample Shorthand
NullOne valuenull
BooleanFalse or truetrue
IntegerArbitrary-precision mathematical integer42
FloatFinite binary64 value, with bit identity1.0
StringUnicode scalar sequence, without normalization"hello"
BytesOpaque finite byte sequencebytes("00ff80")
ConstantExact named mathematical constantPi or E
FractionInteger numerator and positive integer denominator1/3 or fraction(1, 3)

Positive and negative Float zero are distinct. Nonfinite floating-point values are excluded. Fraction pairs are retained without reduction in the DCI model.

Numerical DCIs

A Numerical DCI is a scalar numerical structure, an Array, a Sum, or an OrderedProduct. A Scalar Numerical DCI is Integer, Float, Fraction, Constant, Product, Power, Quotient, or a Sum/OrderedProduct whose every operand is recursively scalar. Arrays, including rank-zero Arrays, are not scalar in this classification. Boolean and Complex DCIs are outside these categories.

Classification is structural, without evaluation or shape inference. Sum and OrderedProduct accept Numerical DCIs; Product, Power bases, Quotient operands, and Complex components require Scalar Numerical DCIs.

Atomic types

Atomic types have a fixed set of named fields and at least one constituent DCI.

DCTInformationExample Shorthand
PowerScalar Numerical base DCI; positive integer exponent numerator and denominator5**(1/3) or power(5, 1, 3)
QuotientScalar Numerical numerator and denominator DCIsquotient(1, sqrt(3) + sqrt(2))
ComplexReal and imaginary Scalar Numerical DCIscomplex(1/2, sqrt(3))

Power’s exponent defaults to denominator one in its two-argument constructor. Fraction bases require explicit power(...) syntax. sqrt(base) is the Power spelling with exponent fields 1 and 2. Exponent fields remain unreduced in DCIs. Quotient has no mathematical nonzero-denominator invariant in the current Core draft. Converting a Quotient DCI to a conventional Quotient object can fail for a recognized zero denominator; explicit numerical evaluation can fail for other undefined expressions.

Composite types

DCTStructure and identityExample Shorthand
ListOrdered DCIs[1, 2] or list(1, 2)
TupleOrdered DCIs, distinct from List(1, 2) or tuple(1, 2)
SetUnordered distinct DCIs{1, 2} or set(1, 2)
MultisetUnordered DCI occurrences, retaining multiplicitymultiset(1, 1, 2)
SumUnordered Numerical DCI occurrences1 + 2 or sum(1, 2)
ProductUnordered Scalar Numerical DCI occurrences2 * sqrt(3) or product(2, sqrt(3))
OrderedProductOrdered Numerical DCI occurrences, including Arraysordered_product(2, array([[1, 0], [0, 1]]))
MappingOrdered entries with distinct DCI keys{"a": 1} or mapping("a": 1)
UnorderedMappingUnordered entries with distinct DCI keysumapping("a": 1)
RecordUnicode name and ordered uniquely named DCI fieldsrecord("Point", "x": 1.0)
ArrayPortable machine type, shape, and elementsarray([1.0, 2.0])
ExternalFileRelative path and content digestexternal_file("x.bin", "DIGEST")
ExternalArrayRelative path, Array type, shape, and content digestexternal_array("x.bin", "float64", (2), "DIGEST")

DIGEST is a placeholder for 64 hexadecimal characters, not valid literal metadata. Resource availability does not change an external DCI’s identity.

Empty Sets use set(); empty Mappings use {} or mapping(). A singleton Tuple is (1) or tuple(1). Empty and singleton Sums and Products retain their constructors. Numerical nesting and multiplicity participate in identity.

Numerical structure and presentation

Sum and Product identity ignores operand order but retains repeated operands and nesting. A flat sum(1, 2, 3) and a nested sum(1, sum(2, 3)) therefore represent different DCIs. Quotient and Fraction are distinct DCTs even when their mathematical interpretations agree.

The Python DCT implementations retain supplied operand order for Shorthand presentation. Canonical encoding sorts complete operand encodings for unordered DCTs. Canonical decoding may recover a different presentation order without changing DCI identity. The numerical conventional types have separate construction and simplification rules.

Strict Shorthand parentheses construct Tuples rather than grouped arithmetic expressions. Constructors preserve explicit nesting, as in product(2, sum(3, 4)); binary subtraction is not part of strict Shorthand.

Direct information and constituent DCIs

Fixed-domain information is prescribed directly: Fraction integers, Power exponent integers, Record and field names, relative-path components, Array metadata, and digests. Their domains do not vary by field value.

A Power base, Complex component, Quotient operand, collection member, or Record field value is a DCI. Its DCT participates in the containing value’s identity. Record names do not identify another DCT or instruct an application to create a class.

Canonical encoding reuses shared payload rules for fixed information and full DCI encodings for constituent DCIs. Array elements use specialized machine representations rather than individual DCI encodings.

Portable Array types

CategoryTypes
Booleanbool
Signed integersint8, int16, int32, int64
Unsigned integersuint8, uint16, uint32, uint64
Floating pointfloat16, float32, float64
Complexcomplex64, complex128

Array type, rank, shape, and corresponding element identities all matter. A rank-zero Array contains one element. Any zero dimension gives zero elements. Elements are canonically encoded in logical C order, with the rightmost index varying fastest and multibyte numerical values in big-endian order.

Array construction and dtype promotion in the Python interface follow the supported NumPy behavior. Explicit dtype conversion can round values; the resulting Array must satisfy its DCT’s validity rules.

Shorthand, longhand, and preprocessing

Longhand uses explicit constructors for structured values and ordinary literals for Null, Boolean, Integer, Float, and String. The distinction concerns syntax, not the Core type classification: Fraction uses fraction(...) despite being primitive. Bytes keeps bytes(...), and Array elements retain their specialized syntax. sum(1, 1.0, fraction(1, 2)) is longhand without extra literal wrappers. The parser accepts both and any mixture; render_shorthand(..., longhand=True) selects longhand. Default rendering uses shorter forms with identity-preserving fallbacks. Layout styles are independent of that choice.

The Python renderer separates layout (standard, semi-compact, compact, or inline) from column align, structural indent (default 2), and same-line padding (default 1). Use custom_layout for per-type overrides. Compact continuation items align with their first item; indentation controls nested blocks on new lines. Padding also applies to flat collections. An inline value forces its entire subtree onto one line, overriding child layouts and ignoring alignment and indentation there. The Python API defines the options.

Optional preprocessing expands compact Lists, substitutes names, and evaluates explicit scalar eval(...) expressions before strict parsing. It is not implicitly used by YAML airlocks or persistence reads.

Representations

RepresentationRole
Canonical bytes / .dcbUnique binary identity encoding
Shorthand / .dcsReadable strict text, including longhand
YAML interchangeNative YAML plus $ strict Shorthand
HDF5 / .h5Native numerical datasets, structured DCIs, and embedded external payloads

SHA-256 of canonical bytes is the DCI fingerprint. It is not a hash of Shorthand, YAML, or HDF5 file bytes. SHA-256 content digests also identify external payloads; those payloads require separate retrieval and verification.

For normative rules and conformance vectors, use the Specifications.

6 - Specifications

DataCanon Version 1 is documented in seven complementary specifications. Core defines the data model, equality, canonical encoding, and fingerprinting. The other specifications define representations, optional preprocessing, and persistence behavior. The Conventional Types companion defines optional computational and collection behavior; support is not required for Core conformance.

The specifications are drafts for independent implementation review. Outstanding conformance and interoperability items are recorded in the individual specifications.

This section is generated from the specifications in the DataCanon code repository. Changes should be made there and synchronized here.

6.1 - DataCanon Core

Version 1 Draft


Introduction

DataCanon (DC) is a data model for structured scientific data with a canonical identity encoding.

DataCanon defines a set of data types and their equality relations, together with a unique canonical byte encoding for every instance.

The type and encoding definitions provide a common, representation-independent basis for communicating, identifying, comparing, and preserving structured scientific data across software implementations and persistence mechanisms.

The identity of a DataCanon Instance is determined solely by its type and its constituent information. It does not depend on any particular programming language, textual notation, parser, renderer, or persistence format.

Textual syntaxes and persistence formats may represent DataCanon Instances, but those representations are outside the core DataCanon specification.

DataCanon is intended for scientific software whose data must remain stable, portable, reproducible, and meaningful over years or decades of development.


Goals

DataCanon is designed to provide

  • rigorously defined data types,
  • deterministic data identity,
  • implementation-independent comparison,
  • reliable interchange between software systems,
  • deterministic fingerprinting,
  • long-term persistence.

The goals support scientific software whose data must remain robust over the lifetime of a project rather than the lifetime of a particular programming language, textual syntax, or persistence format.


Non-Goals

DataCanon intentionally does not provide

  • a general-purpose programming language,
  • a required textual syntax,
  • symbolic algebra,
  • general computation,
  • variables,
  • user-defined functions,
  • expression evaluation,
  • units,
  • dimensional analysis.

DataCanon represents structured data.

It does not manipulate that data or define mathematical operations between instances except where such structure is explicitly part of a DataCanon Type.

DataCanon canonicalizes identity, not mathematics. Mathematical relationships between distinct DataCanon Instances are outside the DataCanon specification.

The non-goals apply to the DataCanon data model and canonical identity. Optional textual representations and preprocessing facilities may provide conveniences such as substitutions and expression evaluation without extending the DataCanon Type system or altering DCI identity.


Terminology

The following terms are used throughout this specification.

DataCanon Type (DCT)

A DataCanon Type (DCT) is a data type defined by the DataCanon specification.

Each DCT defines

  • the information constituting its instances,
  • the invariants those instances MUST satisfy, and
  • equality between its instances.

A DCT may specify information directly and may contain constituent instances of other DCTs. Direct information has a domain prescribed by the containing DCT; a constituent DCI retains its own DataCanon type and identity.

DataCanon Instance (DCI)

A DataCanon Instance (DCI) is an instance of exactly one DCT.

A DCI is a DataCanon value. This specification makes no separate distinction between a DataCanon instance and a DataCanon value.

DCI equality is defined by the corresponding DCT.

DCI identity is independent of any implementation-language object, textual representation, persistence format, or other realization of the DCI.

Canonical Encoding

The canonical encoding of a DCI is the unique byte sequence assigned to that DCI by this specification.

Let E(x) denote the canonical encoding of DCI x.

For any DCIs x and y,

x = y  if and only if  E(x) = E(y)

where equality on the left is DCI equality as defined by the corresponding DCTs and equality on the right is byte-for-byte equality.

The canonical encoding does not define DCI equality. Equality is defined by the DCTs themselves. The canonical encoding provides a unique, portable encoding of that identity.

The canonical encoding is fully decodable and may be stored or transmitted directly. No additional representation is required to reconstruct the encoded DCI. For DCIs referencing external resources, canonical decoding reconstructs the DCI without requiring access to those resources. Retrieval and verification of external contents are separate operations.

Representation

A representation of a DCI is any form from which that DCI can be recovered without loss of DataCanon identity.

Representations may include textual syntaxes, implementation-language objects, or persistence formats.

No such representation is privileged by the core DataCanon specification.


Conventional Types

A conventional type is a type used directly for data and computation in a programming language. Conventional types may be built into the language, provided by libraries, or defined by users. An implementation may supply additional conventional types where suitable ones are missing.

An implementation may define conversions between DCIs and instances of conventional types. Each conventional type retains its own construction, equality, and operation rules, which need not coincide with the rules of an associated DCT. A conversion relationship does not establish type equivalence, a one-to-one correspondence, or a lossless round trip.

The term describes the practical types used through an implementation’s conversion interface; it does not introduce another DCT or a closed type system. A programming-language object implementing a DCI and an instance of a conventional type used in conversion can be distinct objects of different types within the same language.

Conventional-type operations do not change DCI equality or canonical encoding. Conversion to a conventional object may discard distinctions needed to recover the original DCI. Such an object is not necessarily a representation of that original DCI under the definition above.

The optional DataCanon Conventional Types Version 1 companion defines construction, arithmetic, comparison, collection behavior, and conversion requirements for implementations claiming support for those conventional types. Core conformance does not require support for that companion.

Design Principles

Representation Independence

DataCanon identity is independent of the representation used to express, communicate, or persist a DCI.

Different representations of the same DCI may differ arbitrarily in syntax, layout, storage organization, or implementation details.

Different representations recover the same DataCanon data when they recover equal DCIs.


Implementation Independence

The DataCanon specification does not prescribe how a software implementation represents DCIs internally.

Different implementations may use different classes, structures, memory layouts, or other implementation-specific mechanisms.

Implementations representing equal DCIs MUST nevertheless agree on DCI equality and canonical encoding.


Deterministic Identity

Every valid DCI has exactly one canonical encoding.

The canonical encoding depends only on the DCI and its DCT.

It does not depend on the implementation that produced it or on any other representation of the DCI.


Structural Identity

Unless explicitly stated otherwise by a DCT, DCI identity is determined by the structure and constituents defined by that DCT rather than by mathematical, application-specific, or implementation-specific equivalence.


Constituent Structure

A DCT specifies fixed-domain information directly. A field is a constituent DCI when its DataCanon type or recursive DataCanon structure participates in the containing value’s identity. For example, Fraction uses direct mathematical integer fields, while Complex uses Numerical DCIs whose individual DCTs matter.

Encoding rules reuse shared payload definitions for direct information and complete canonical encodings for constituent DCIs.

Minimality

DataCanon defines only what is required to establish portable structured data and its identity.

Facilities for textual input, textual rendering, preprocessing, application semantics, computation, and specialized persistence are defined separately when needed.

DataCanon Types

DataCanon defines the types described in this section.

Each DataCanon Type (DCT) defines its instances, the invariants those instances must satisfy, and equality between its instances.

Instances of different DCTs are never equal.

DataCanon Types are classified according to the organization of their information and constituent DCIs. Classification does not alter identity or encoding rules.

Primitive Types

A primitive DCT specifies a basic value directly, without constituent DCIs. Its information may have multiple prescribed fields.

Version 1 defines the following primitive DCTs:

  • Null
  • Boolean
  • Integer
  • Float
  • String
  • Bytes
  • Fraction
  • Constant

Null

The Null DCT contains exactly one DCI.

All Null DCIs are therefore equal.


Boolean

The Boolean DCT contains exactly two DCIs, representing the Boolean values false and true.

Two Boolean DCIs are equal if and only if they represent the same Boolean value.


Integer

The Integer DCT represents the mathematical integers (\mathbb{Z}).

An Integer DCI consists of exactly one mathematical integer. Integer has unlimited precision.

Two Integer DCIs are equal if and only if they represent the same mathematical integer.


Float

The Float DCT represents finite IEEE-754 binary64 values.

A Float DCI consists of exactly one finite IEEE-754 binary64 value, including its sign.

Two Float DCIs are equal if and only if their IEEE-754 binary64 bit representations are identical.

Positive and negative zero are therefore distinct Float DCIs.

NaN and positive and negative infinity are not Float DCIs in Version 1.

Floating-point values are inherently approximate. Applications requiring an exact rational representation should use a suitable exact DCT.


String

The String DCT represents finite sequences of Unicode scalar values.

A String DCI consists of exactly one such sequence.

Two String DCIs are equal if and only if they contain identical sequences of Unicode scalar values.

DataCanon performs no Unicode normalization. Strings that are visually identical but consist of different sequences of Unicode scalar values are therefore distinct String DCIs.

Bytes

The Bytes DCT represents finite sequences of bytes. A Bytes DCI consists of exactly one such sequence, which MAY be empty.

Two Bytes DCIs are equal if and only if their complete byte sequences are identical, including length and byte order.

Bytes are opaque. DataCanon does not assign a character encoding, file format, filename, media type, or application-specific interpretation to their contents. Applications may store such metadata alongside a Bytes DCI, for example in a Record. That metadata participates in the containing DCI’s identity according to its DCT, but is not a constituent of the Bytes DCI.

Bytes is distinct from String, Array, and ExternalFile. Its byte sequence is contained directly in the DCI, rather than referenced by a path and digest.

Fraction

The Fraction DCT represents a structured ratio of two mathematical integers.

A Fraction DCI consists of

  • a mathematical integer numerator, and
  • a positive mathematical integer denominator.

Two Fraction DCIs are equal if and only if their corresponding constituents are equal.

Fraction identity is structural rather than mathematical. Distinct numerator and denominator pairs remain distinct even when they represent the same mathematical rational number.

DataCanon does not reduce, normalize, or otherwise mathematically simplify Fractions.


Constant

The Constant DCT contains exactly two DCIs in Version 1, named pi and e. The name identifies the exact mathematical constant: pi is the ratio of a circle’s circumference to its diameter in Euclidean geometry; e is Euler’s number, the base of the natural logarithm. No decimal approximation is stored.

Two Constant DCIs are equal if and only if their names agree. Constant is a primitive DCT: its prescribed name is direct information, not a String DCI. A Constant is never equal to a Float containing an approximation of its value. No other names, aliases, user-defined constants, or physical constants are included in Version 1.

Numerical Instances

A Numerical DCI is an Integer, Float, Fraction, Constant, Product, Power, Quotient, Array, Sum, or OrderedProduct DCI. Sum and OrderedProduct operands MUST themselves be Numerical DCIs. Boolean and Complex are not members of this category. A Boolean Array is still an Array DCI; category membership does not promise that a conventional arithmetic implementation supports it.

A Scalar Numerical DCI is one of:

  • an Integer, Float, Fraction, or Constant DCI;
  • a Product, Power, or Quotient DCI, whose invariants require scalar constituents;
  • a Sum or OrderedProduct DCI whose every operand is a Scalar Numerical DCI.

An empty Sum or OrderedProduct satisfies the scalar classification. An Array, including a rank-zero Array, does not. Classification depends on constituent structure, without evaluating values or inferring ranks or shapes of results. For example, a Sum containing an Array through a nested OrderedProduct is not scalar, regardless of the Array’s shape or the factors’ mathematical values.

The categories specify permitted constituent DCIs; neither is an additional DCT. Scalar structural validity does not certify that an interpretation is mathematically defined or real. Types listed here may be defined later.

Atomic Types

An atomic DCT has a fixed set of named fields and at least one constituent DCI. Fields may contain directly specified information or constituent DCIs, as prescribed by the DCT.

Version 1 defines the following atomic DCTs:

  • Power
  • Quotient
  • Complex

Power

The Power DCT represents a base with a structured positive rational exponent. A Power DCI consists of

  • a base that is a Scalar Numerical DCI,
  • a positive mathematical integer numerator, and
  • a positive mathematical integer denominator.

The exponent may be interpreted as the numerator divided by the denominator. Both exponent constituents MUST be positive and participate independently in identity. They MUST NOT be reduced or otherwise normalized.

Two Power DCIs are equal if and only if all corresponding constituents are equal. Power is distinct from its base and from other numerical DCTs, including when its exponent is one. Nested Powers MUST NOT be collapsed.

The base MUST be a Scalar Numerical DCI. Power does not evaluate the base, select a mathematical branch, or certify that a real value exists. Its structural invariants impose no additional sign restriction on the base.


Quotient

The Quotient DCT represents a structured numerator and denominator, each a Scalar Numerical DCI. The constituents may belong to different numerical DCTs.

Two Quotient DCIs are equal if and only if corresponding numerators and corresponding denominators are equal. Constituent roles participate in identity. Quotient is distinct from Fraction, even when both constituents are Integers.

DataCanon MUST NOT evaluate, cancel, normalize, or rationalize Quotient constituents. This draft imposes numerical constituent-type restrictions but no nonzero-denominator invariant. Structural validity does not certify that the mathematical division is defined. Any stronger denominator restriction must be specified explicitly as a DCT invariant.


Complex

The Complex DCT represents a structured complex number consisting of a real component and an imaginary component.

A Complex DCI consists of

  • a real component, and
  • an imaginary component.

Each component MUST be a Scalar Numerical DCI.

Two Complex DCIs are equal if and only if their corresponding components are equal.

Because instances of different DCTs are never equal, the DCT of each component participates in Complex identity.

DataCanon does not convert Complex components between numerical DCTs or otherwise mathematically simplify them.

Composite Types

A composite DCT has variable-size constituent structure.

The organization and permitted contents of that structure are defined by the DCT.

Version 1 defines the following composite DCTs:

  • List
  • Tuple
  • Set
  • Multiset
  • Sum
  • Product
  • OrderedProduct
  • Mapping
  • UnorderedMapping
  • Record
  • Array
  • ExternalFile
  • ExternalArray

List

The List DCT represents a finite ordered sequence of DCIs.

A List DCI consists of zero or more DCIs in a defined order.

The constituent DCIs may belong to any DCT and need not belong to the same DCT.

Two List DCIs are equal if and only if

  • they contain the same number of items, and
  • corresponding items are equal.

Thus, item order participates in List identity.

Lists may recursively contain DCIs of any DCT, including other composite DCTs.


Tuple

The Tuple DCT represents a finite ordered sequence of DCIs.

A Tuple DCI consists of zero or more DCIs in a defined order.

The constituent DCIs may belong to any DCT and need not belong to the same DCT.

Two Tuple DCIs are equal if and only if

  • they contain the same number of items, and
  • corresponding items are equal.

Thus, item order participates in Tuple identity.

List and Tuple are distinct DCTs. Their instances are therefore never equal, even when they contain equal constituent DCIs in the same order.

Tuples may recursively contain DCIs of any DCT, including other composite DCTs.


Set

The Set DCT represents a finite unordered collection of distinct DCIs.

A Set DCI consists of zero or more DCIs.

The constituent DCIs may belong to any DCT and need not belong to the same DCT.

No two members of a Set DCI may be equal.

Two Set DCIs are equal if and only if they contain the same members, independent of member order.

Thus, member order does not participate in Set identity.

Sets may recursively contain DCIs of any DCT, including other composite DCTs.


Multiset

The Multiset DCT represents a finite unordered collection of DCIs in which members may occur more than once.

A Multiset DCI consists of zero or more member occurrences. Each occurrence is a DCI of any DCT; members need not belong to the same DCT.

Two Multiset DCIs are equal if and only if, for every DCI, they contain the same number of occurrences equal to that DCI under DataCanon equality.

Thus, member order does not participate in Multiset identity, while multiplicity does. An empty Multiset contains no member occurrences.

Multiset and Set are distinct DCTs. Their instances are never equal, even when a Multiset contains no repeated members.

Multisets may recursively contain DCIs of any DCT, including other Multisets. Nesting participates in identity; nested Multisets are not flattened.


Sum and Product

Sum and Product are finite unordered collections of operand occurrences. Multiplicity and nesting participate in identity. Two instances of the same DCT are equal if and only if their operands match under DCI equality with the same multiplicities, independent of operand order.

Every Sum operand MUST be a Numerical DCI. Sum therefore accepts scalar expressions, Arrays, and OrderedProducts, including recursively nested Sums. Every Product operand MUST be a Scalar Numerical DCI. Product remains an unordered scalar structure and MUST NOT contain an Array through any path.

No operands are evaluated, flattened, distributed, deduplicated, or simplified. Array ranks and shape compatibility are not Sum invariants. Scalar terms may be stored alongside Arrays; Core assigns no implicit broadcasting or identity matrix convention to their mathematical interpretation.

OrderedProduct

The OrderedProduct DCT is a finite ordered sequence of zero or more Numerical DCIs. Scalars, Arrays, Sums, and nested OrderedProducts are permitted. Operands are complete DCIs, not native information fields.

Two OrderedProduct DCIs are equal if and only if they have the same number of operands and corresponding operands are equal. Order, multiplicity, nesting, and each operand’s DCT participate in identity. OrderedProduct is distinct from Product and List, including for empty or scalar-only instances.

Core MUST NOT multiply factors, commute scalars past Arrays, flatten nested products, remove identities, or check mathematical shape compatibility. No rank restriction is imposed on Array operands. A rank-three Array is valid here even when a conventional binding cannot evaluate its ordered product.


Mapping

The Mapping DCT represents a finite ordered collection of key-value entries.

A Mapping DCI consists of zero or more entries in a defined order.

Each entry consists of

  • a key DCI, and
  • a value DCI.

Keys and values may belong to any DCT.

No two entries in a Mapping DCI may have equal keys.

Two Mapping DCIs are equal if and only if

  • they contain the same number of entries,
  • corresponding keys are equal, and
  • corresponding values are equal.

Thus, entry order participates in Mapping identity.


UnorderedMapping

The UnorderedMapping DCT represents a finite unordered collection of key-value entries.

An UnorderedMapping DCI consists of zero or more entries.

Each entry consists of

  • a key DCI, and
  • a value DCI.

Keys and values may belong to any DCT.

No two entries in an UnorderedMapping DCI may have equal keys.

Two UnorderedMapping DCIs are equal if and only if they contain the same key-value entries, independent of entry order.

Thus, entry order does not participate in UnorderedMapping identity.

Mapping and UnorderedMapping are distinct DCTs.


Record

The Record DCT represents a named structured value consisting of an ordered sequence of named fields.

A Record DCI consists of

  • a name that is a finite sequence of Unicode scalar values, and
  • zero or more fields in a defined order.

Each field consists of

  • a field name that is a finite sequence of Unicode scalar values, and
  • a value DCI.

Field values may belong to any DCT.

Field names within a Record MUST be unique.

Two Record DCIs are equal if and only if

  • their Record names are equal,
  • they contain the same number of fields,
  • corresponding field names are equal, and
  • corresponding field values are equal.

Thus, both the Record name and field order participate in Record identity.

Record is distinct from Mapping. A Record represents a named structure with a defined sequence of named fields, whereas a Mapping represents an ordered collection of associations between arbitrary DCI keys and values.

The Record name is semantic data. DataCanon does not assign application-specific meaning to that name and does not interpret it as the name of another DCT.

A Record name may correspond to a type, class, or other application-defined concept in an implementation. Such correspondences are defined by the implementation or application rather than by the Record DCT.


Array

The Array DCT represents dense, regular multidimensional machine data.

An Array DCI consists of

  • an Array type,
  • a finite multidimensional shape, and
  • one element for every position defined by that shape.

The shape is a finite ordered sequence of nonnegative mathematical integers.

The number of entries in the shape is the rank of the Array, and the product of the entries is the total number of elements.

An empty shape denotes a rank-zero Array containing one element.

The Array type determines the domain and equality relation of its elements.

Two Array DCIs are equal if and only if

  • they have the same Array type,
  • they have the same shape, and
  • corresponding elements are equal according to the equality rule of that Array type.

Thus, Array type and shape participate in Array identity independently of the values of the elements.

Arrays are distinct from Lists. A List recursively contains DCIs. The elements of an Array are instead values of its Array type and are not themselves DCIs.

DataCanon does not define a universal type system or coercion policy for Array construction. In particular, DataCanon does not prescribe

  • which additional native Array types an implementation may support,
  • how mixed input values are promoted or coerced, or
  • how an implementation represents unsupported or implementation-specific Array types.

The Array construction choices are delegated to the implementation.

Applications requiring DataCanon-defined type identity for every individual element should use nested Lists rather than Arrays.

Portable Array Types

DataCanon Version 1 defines the following portable Array types:

  • bool
  • int8
  • int16
  • int32
  • int64
  • uint8
  • uint16
  • uint32
  • uint64
  • float16
  • float32
  • float64
  • complex64
  • complex128

The portable Array type names denote DataCanon Array types rather than types belonging to any particular implementation.

The bool Array type contains exactly two values, corresponding to false and true. Two bool elements are equal if and only if they represent the same Boolean value.

The signed and unsigned integer Array types represent mathematical integers within the ranges determined by their respective fixed widths. Two integer Array elements of the same Array type are equal if and only if they represent the same mathematical integer.

The floating-point Array types represent finite IEEE-754 binary16, binary32, and binary64 values, respectively.

Two floating-point Array elements of the same Array type are equal if and only if their IEEE-754 bit representations are identical.

Positive and negative zero are therefore distinct floating-point Array elements.

NaN and positive and negative infinity are not elements of the portable floating-point Array types in Version 1.

The complex64 Array type consists of ordered pairs of float32 elements representing the real and imaginary components.

The complex128 Array type consists of ordered pairs of float64 elements representing the real and imaginary components.

Two complex Array elements of the same Array type are equal if and only if their corresponding real and imaginary components are equal.

DataCanon Version 1 defines Array DCIs only for the portable Array types listed above. Implementations MAY support additional native Array types, but values of those types are not Version 1 Array DCIs unless their types and canonical encodings are defined by an applicable extension to this specification.

ExternalFile

The ExternalFile DCT represents an externally stored finite sequence of bytes whose relative path and content digest are part of its identity.

An ExternalFile DCI consists of

  • a DataCanon relative path, and
  • a SHA-256 content digest.

The path identifies an external resource within a namespace determined by the representation or application resolving the DCI.

The content digest is the SHA-256 digest of the exact byte sequence contained in the external file.

The path MUST be relative.

Two ExternalFile DCIs are equal if and only if

  • their paths are equal, and
  • their content digests are equal.

Thus, both the relative path and content digest participate in ExternalFile identity.

The contents of an ExternalFile are opaque to DataCanon. DataCanon assigns no meaning to the byte sequence and does not prescribe how an application interprets it.

The content digest binds the ExternalFile DCI to a particular byte sequence. A file found at the specified path whose SHA-256 digest differs from the content digest is not the external file identified by that DCI.

The existence or availability of the external file does not alter the identity of the ExternalFile DCI. A missing or inaccessible external file prevents the external content from being retrieved or verified but does not change the DCI.


ExternalArray

The ExternalArray DCT represents dense, regular multidimensional machine data stored externally using the DataCanon Array byte representation.

An ExternalArray DCI consists of

  • a DataCanon relative path,
  • a portable Array type,
  • a finite multidimensional shape, and
  • a SHA-256 content digest.

The path identifies an external resource within a namespace determined by the representation or application resolving the DCI.

The path MUST be relative.

The Array type and shape satisfy the same requirements as the corresponding constituents of an Array DCI.

The logical external element byte stream MUST be the DataCanon-defined byte representation of the Array elements for the specified Array type and shape.

The content digest is the SHA-256 digest of that logical byte stream, before storage compression. Persistence may store the stream directly or in a losslessly compressed representation. Compression does not add a DCI constituent; the complete relative path, including any suffix, still participates in identity.

Two ExternalArray DCIs are equal if and only if

  • their paths are equal,
  • their Array types are equal,
  • their shapes are equal, and
  • their content digests are equal.

Thus, relative path, Array type, shape, and content digest all participate in ExternalArray identity.

ExternalArray and Array are distinct DCTs. An ExternalArray DCI and an Array DCI are therefore never equal, even when the external byte sequence represents the same numerical data as the Array DCI.

The content digest binds the ExternalArray DCI to a particular logical element byte stream. After any persistence-defined decompression, a stream whose SHA-256 digest differs from the content digest does not satisfy that DCI.

The existence or availability of the external data does not alter the identity of the ExternalArray DCI. Missing or inaccessible external data prevents the Array contents from being retrieved or verified but does not change the DCI.

Relative Paths

External DCTs identify external resources using a DataCanon relative path.

A DataCanon relative path is a finite nonempty ordered sequence of path components.

Each path component is a nonempty finite sequence of Unicode scalar values.

The components "." and ".." are not permitted.

The characters / and \ are not permitted within a path component.

Two DataCanon relative paths are equal if and only if they contain the same number of components and corresponding components are equal according to String equality.

A DataCanon relative path is independent of host operating-system path syntax.

The namespace used to resolve a DataCanon relative path is not part of DCI identity. The mechanism by which a representation or implementation resolves a DataCanon relative path to an external resource is outside the core DataCanon specification.

Canonical Encoding

Every DataCanon Instance has exactly one canonical encoding as a finite sequence of bytes.

The canonical encoding provides a representation-independent encoding of DCI identity.

Let E(x) denote the canonical encoding of DCI x. For any DCIs x and y,

x = y  if and only if  E(x) = E(y)

where equality on the left is DCI equality and equality on the right is byte-for-byte equality.

The canonical encoding MUST be

  • deterministic — a DCI always produces the same byte sequence;
  • injective — unequal DCIs produce different byte sequences;
  • implementation independent — the encoding does not depend on implementation-language representations;
  • unambiguous — the encoded DCT and all information constituting the DCI can be recovered uniquely from the byte sequence;
  • recursive — constituent DCIs are encoded according to the same canonical encoding rules;
  • stable — the encoding of a Version 1 DCI does not change between conforming Version 1 implementations.

The canonical encoding is not intended as a human-readable representation, general-purpose interchange format, or optimized persistence format.

Its purpose is to provide a minimal, stable, and unique byte representation of DCI identity.

Version Context

The canonical encoding contains no version identifier or framing header. The applicable DataCanon specification version is determined by the context in which the encoding is used. Persistence and interchange formats MAY provide version information independently of the canonical encoding.

Future specifications MUST NOT silently reinterpret Version 1 identifiers or payload encodings when those bytes are interpreted as Version 1 canonical encodings.

Unsigned Integer Encoding

Canonical encoding uses a variable-length representation of nonnegative mathematical integers for lengths, counts, ranks, dimensions, and other nonnegative integer metadata explicitly designated by this specification.

A nonnegative integer is encoded using unsigned LEB128.

The binary representation of the integer is divided into groups of seven bits, beginning with the least-significant bits.

Each group is encoded as one byte. The low seven bits contain the group value. The high bit is the continuation bit.

The continuation bit is

  • 1 when another group follows, and
  • 0 for the final byte.

The encoding MUST use the minimum possible number of bytes. Redundant high-order zero groups are not permitted.

Thus, every nonnegative mathematical integer has exactly one canonical unsigned integer encoding.

Examples include

IntegerEncoding
000
101
202
1277f
12880 01
12981 01
255ff 01
25680 02

There is no upper bound on the represented integer.

Signed Integer Encoding

Mathematical integers are first mapped bijectively to nonnegative mathematical integers using zigzag encoding.

For mathematical integer (n), define

$$ Z(n) = \begin{cases} 2n, & n \ge 0,\ -2n - 1, & n < 0. \end{cases} $$

Thus,

IntegerZigzag value
00
-11
12
-23
24
-35
36

The canonical signed integer encoding of (n) is the canonical unsigned integer encoding of (Z(n)).

This representation has no implementation-defined integer width and therefore supports the unlimited-precision Integer DCT.

Unicode String Information Encoding

Let S(t) encode a finite sequence of Unicode scalar values t as its UTF-8 byte count using U, followed by the exact UTF-8 bytes:

S(t) = U(number_of_UTF8_bytes) || UTF8(t)

This is the payload encoding used by String, without the String DCT identifier. It is also used for Record names, field names, and relative-path components. All UTF-8 validity rules apply; no Unicode normalization is performed.

Encoding Structure

The canonical encoding of every DCI consists of

  1. a one-byte DCT identifier, followed by
  2. the canonical payload defined for that DCT.

Thus,

E(x) = DCT(x) || payload(x)

where || denotes byte concatenation.

The DCT identifier determines how the following payload is interpreted.

Direct information is encoded using the payload rules prescribed by its containing DCT. No DCT identifier is included unless explicitly specified.

Every constituent that is itself a DCI is encoded using its complete canonical encoding, including its own DCT identifier.

Consequently, nested DCIs are self-describing with respect to their DCTs.

Lengths and counts required to delimit variable-size payloads are encoded using the canonical unsigned integer encoding defined above.

DCT Identifiers

DataCanon Version 1 assigns the following one-byte identifiers:

DCTIdentifier
Null00
Boolean01
Integer02
Float03
String04
Fraction05
Complex07
List08
Tuple09
Set0a
Mapping0b
UnorderedMapping0c
Record0d
Array0e
ExternalFile0f
ExternalArray10
Multiset11
Sum12
Product13
Power14
Quotient15
Bytes16
Constant17
OrderedProduct18

The DCT identifiers are part of the DataCanon Version 1 specification and MUST NOT be reassigned within Version 1.

Identifier 06 is reserved and MUST NOT be used for a Version 1 DCI. A canonical decoder MUST reject that identifier.

The numerical ordering of DCT identifiers has no semantic meaning.

Primitive Types

Null

The canonical encoding of a Null DCI consists solely of the Null DCT identifier.

00

Because Null has exactly one DCI, no payload is required.

Boolean

The canonical encoding of a Boolean DCI consists of

  1. the Boolean DCT identifier, followed by
  2. one byte identifying the Boolean value.

The Boolean payload is

00    false
01    true

Thus,

01 00    false
01 01    true

are the complete canonical encodings.

No other Boolean payload byte is valid.

Integer

The canonical encoding of an Integer DCI consists of

  1. the Integer DCT identifier, followed by
  2. the canonical signed integer encoding of its mathematical integer value.

Thus,

02 || I(n)

is the canonical encoding of Integer DCI (n), where I is the signed integer encoding defined above.

There is no implementation-defined integer width.

Float

The canonical encoding of a Float DCI consists of

  1. the Float DCT identifier, followed by
  2. the eight bytes of its IEEE-754 binary64 representation in big-endian byte order.

Thus every Float DCI occupies exactly nine bytes.

The payload preserves the exact IEEE-754 representation, including the sign of zero.

Because NaN and positive and negative infinity are not Float DCIs in Version 1, their IEEE-754 representations are not valid Float payloads.

String

The canonical encoding of a String DCI consists of

  1. the String DCT identifier,
  2. the number of bytes in the UTF-8 encoding of the String, encoded using the canonical unsigned integer encoding, and
  3. the UTF-8 bytes of the String.

Thus,

04 || U(N) || UTF8

where N is the number of bytes in UTF8 and U is the canonical unsigned integer encoding.

Because a String DCI is a sequence of Unicode scalar values, its UTF-8 representation is uniquely determined.

DataCanon performs no Unicode normalization before encoding.

Bytes

The canonical encoding of a Bytes DCI consists of

  1. the Bytes DCT identifier,
  2. the number of bytes, encoded using the canonical unsigned integer encoding, and
  3. the exact byte sequence.
16 || U(N) || BYTES

No text encoding or transformation is applied. Every byte value from 0 through 255 is permitted. An empty Bytes DCI is encoded as 16 00.

Constant

A Constant DCI is encoded as its DCT identifier 17 followed by exactly one constant identifier byte: 00 for pi, 01 for e. No length, String DCT identifier, or name text is included. All other constant identifier bytes are invalid and MUST be rejected. A missing identifier byte is invalid.

pi: 17 00
e:  17 01

Fraction

A Fraction DCI is encoded as

05 || I(numerator) || I(denominator)

I is the canonical signed integer encoding defined above. Both fields are mathematical integers; the denominator MUST be positive. Neither field has an Integer DCT identifier. No reduction is performed.

Atomic Types

Power

A Power DCI is encoded as

14 || E(base) || I(numerator) || I(denominator)

The base is a complete Scalar Numerical DCI. The positive mathematical integer exponent fields use the signed integer encoding without Integer DCT identifiers. Both are always encoded, including denominator one, and are not reduced or otherwise normalized.

Quotient

A Quotient is encoded as its identifier followed by the complete encodings of its Scalar Numerical numerator and denominator in that order:

15 || E(numerator) || E(denominator)

No constituent is omitted or mathematically transformed.

Complex

The canonical encoding of a Complex DCI consists of

  1. the Complex DCT identifier,
  2. the canonical encoding of its real component, and
  3. the canonical encoding of its imaginary component.

Thus,

07 || E(real) || E(imaginary)

Because each component is encoded as a complete DCI, its DCT participates directly in the canonical encoding.

For example, a Complex whose real component is an Integer necessarily has a different canonical encoding from one whose otherwise numerically equal real component is a Float.

Composite Types

List

The canonical encoding of a List DCI consists of

  1. the List DCT identifier,
  2. the number of items encoded using the canonical unsigned integer encoding, and
  3. the canonical encoding of each item in semantic order.

Thus, for a List containing (N) items,

08 || U(N) || E(item_0) || E(item_1) || ... || E(item_(N-1))

An empty List therefore consists of

08 00

Because each item is self-describing and the number of items is encoded explicitly, no additional separators or item lengths are required.


Tuple

The canonical encoding of a Tuple DCI consists of

  1. the Tuple DCT identifier,
  2. the number of items encoded using the canonical unsigned integer encoding, and
  3. the canonical encoding of each item in semantic order.

Thus, for a Tuple containing (N) items,

09 || U(N) || E(item_0) || E(item_1) || ... || E(item_(N-1))

An empty Tuple therefore consists of

09 00

List and Tuple use the same payload structure but have different DCT identifiers and therefore cannot have identical canonical encodings.


Set

The canonical encoding of a Set DCI consists of

  1. the Set DCT identifier,
  2. the number of members encoded using the canonical unsigned integer encoding, and
  3. the canonical encodings of its members in lexicographic byte order.

To encode a Set, the canonical encoding of each member is first computed independently.

The member encoding byte sequences are then sorted lexicographically, comparing each byte as an unsigned integer from 0 through 255.

For a Set containing (N) members whose sorted canonical encodings are (e_0, e_1, \ldots, e_{N-1}),

0a || U(N) || e_0 || e_1 || ... || e_(N-1)

Because Set members are distinct DCIs and canonical encoding is injective, no two member encodings may be identical.

An empty Set therefore consists of

0a 00

The byte ordering used during Set encoding has no semantic meaning and does not introduce order into the Set DCI.

Multiset

The canonical encoding of a Multiset DCI consists of

  1. the Multiset DCT identifier,
  2. the total number of member occurrences encoded using the canonical unsigned integer encoding, and
  3. the complete canonical encoding of every member occurrence, sorted in nondecreasing lexicographic byte order.

Each byte is compared as an unsigned integer from 0 through 255. Duplicate member encodings MUST be retained, once for each occurrence. The count is the number of occurrences, not the number of distinct members.

For a Multiset containing (N) occurrences with sorted canonical encodings (e_0, e_1, \ldots, e_{N-1}),

11 || U(N) || e_0 || e_1 || ... || e_(N-1)

An empty Multiset consists of

11 00

The sorting rule provides a unique encoding without introducing semantic member order. Nested members use their complete canonical encodings and are not flattened.

OrderedProduct

The encoding is the identifier 18, a canonical unsigned operand count, and complete operand encodings in their stored order:

18 || U(N) || E(factor_0) || ... || E(factor_(N-1))

No sorting occurs. Every operand MUST be a Numerical DCI. Duplicate factors and nested OrderedProducts are preserved. The empty instance is 18 00.

Sum and Product

A Sum or Product is encoded as its DCT identifier, the total number of operand occurrences using canonical unsigned integer encoding, and every complete operand encoding in nondecreasing lexicographic byte order:

12 || U(N) || e_0 || e_1 || ... || e_(N-1)    Sum
13 || U(N) || e_0 || e_1 || ... || e_(N-1)    Product

Each byte is compared as an unsigned integer. Duplicate operand encodings MUST be retained. Sorting does not assign semantic order or flatten nested nodes.

Empty Sum and Product encodings are 12 00 and 13 00, respectively.

Mapping

The canonical encoding of a Mapping DCI consists of

  1. the Mapping DCT identifier,
  2. the number of entries encoded using the canonical unsigned integer encoding, and
  3. the canonical encodings of each entry in semantic entry order.

Each entry is encoded as

E(key) || E(value)

Thus, for a Mapping containing (N) entries,

0b
|| U(N)
|| E(key_0) || E(value_0)
|| E(key_1) || E(value_1)
|| ...
|| E(key_(N-1)) || E(value_(N-1))

where the entries appear in the order belonging to the Mapping DCI.

An empty Mapping therefore consists of

0b 00

No separate entry delimiter or entry-length encoding is required because each key and value is itself an unambiguously decodable DCI.


UnorderedMapping

The canonical encoding of an UnorderedMapping DCI consists of

  1. the UnorderedMapping DCT identifier,
  2. the number of entries encoded using the canonical unsigned integer encoding, and
  3. the canonical encodings of its entries in canonical key order.

To encode an UnorderedMapping, the canonical encoding of each key is first computed independently.

Entries are then sorted lexicographically by their canonical key encodings, comparing each byte as an unsigned integer from 0 through 255.

Each entry is encoded as

E(key) || E(value)

Thus, for an UnorderedMapping containing (N) entries whose canonically ordered keys are (k_0, k_1, \ldots, k_{N-1}),

0c
|| U(N)
|| E(k_0) || E(value_0)
|| E(k_1) || E(value_1)
|| ...
|| E(k_(N-1)) || E(value_(N-1))

Because keys within an UnorderedMapping are distinct DCIs and canonical encoding is injective, no two key encodings may be identical. The canonical key ordering therefore cannot contain ties.

An empty UnorderedMapping therefore consists of

0c 00

The byte ordering used during encoding has no semantic meaning and does not introduce entry order into the UnorderedMapping DCI.


Record

A Record DCI with N fields is encoded as

0d
|| S(record_name)
|| U(N)
|| S(field_name_0) || E(field_value_0)
|| S(field_name_1) || E(field_value_1)
|| ...
|| S(field_name_(N-1)) || E(field_value_(N-1))

S is the Unicode string information encoding. Record and field names have no String DCT identifiers. Field values are complete DCIs. Fields retain their semantic order; names MUST be unique according to Unicode scalar sequence equality. A Record with no fields is 0d || S(record_name) || 00.

Array

The canonical encoding of an Array DCI consists of

  1. the Array DCT identifier,
  2. a one-byte Array type identifier,
  3. the Array rank encoded using the canonical unsigned integer encoding,
  4. each dimension of the Array shape encoded using the canonical unsigned integer encoding, and
  5. the canonical element byte stream.

Thus, for an Array of rank (R) with shape

$$ (d_0,d_1,\ldots,d_{R-1}), $$

the canonical encoding is

0e
|| ARRAY_TYPE
|| U(R)
|| U(d_0) || U(d_1) || ... || U(d_(R-1))
|| ELEMENT_BYTES

The number of Array elements is determined by the shape and is therefore not encoded separately.

For a rank-zero Array, the rank is zero and no dimensions follow. A rank-zero Array contains exactly one element.

Array Type Identifiers

DataCanon Version 1 assigns the following one-byte Array type identifiers:

Array typeIdentifier
bool00
int801
int1602
int3203
int6404
uint805
uint1606
uint3207
uint6408
float1609
float320a
float640b
complex640c
complex1280d

The Array type identifiers are part of the DataCanon Version 1 specification and MUST NOT be reassigned within Version 1.

The numerical ordering of Array type identifiers has no semantic meaning.

Element Ordering

Array elements are encoded in C-contiguous logical order.

For an Array with shape

$$ (d_0,d_1,\ldots,d_{R-1}), $$

the rightmost index varies fastest.

The canonical element ordering depends only on the logical Array shape and element values. It does not depend on the memory layout, strides, contiguity, or storage representation used by an implementation.

Boolean Elements

Each bool element is encoded as exactly one byte:

00    false
01    true

No other byte value is valid.

Integer Elements

Fixed-width integer elements are encoded using exactly their specified number of bits.

Signed integer types use two’s-complement representation.

Unsigned integer types use ordinary unsigned binary representation.

Types wider than one byte are encoded in big-endian byte order.

Thus, each element occupies

Array typeBytes
int81
int162
int324
int648
uint81
uint162
uint324
uint648

Floating-Point Elements

Floating-point elements are encoded using their exact IEEE-754 representations.

float16, float32, and float64 use binary16, binary32, and binary64, respectively.

Types wider than one byte are encoded in big-endian byte order.

The exact bit representation is preserved, including the sign of zero.

NaN and positive and negative infinity are not valid elements of portable floating-point Arrays in Version 1.

Complex Elements

A complex64 element consists of

  1. its real component encoded as a float32 element, followed by
  2. its imaginary component encoded as a float32 element.

A complex128 element consists of

  1. its real component encoded as a float64 element, followed by
  2. its imaginary component encoded as a float64 element.

Thus, a complex64 element occupies exactly 8 bytes and a complex128 element occupies exactly 16 bytes.

Canonical Element Byte Stream

The canonical element byte stream is the concatenation of the canonical element encodings in C-contiguous logical order.

No separators, element counts, or element lengths occur within the byte stream because the Array type and shape determine both the size of each element and the total number of elements.

An Array containing zero elements therefore has an empty canonical element byte stream.

External Types

Relative Path Encoding

A relative path with N components is encoded as

U(N) || S(component_0) || S(component_1) || ... || S(component_(N-1))

Each component is Unicode string information, without a String DCT identifier. The path MUST be nonempty and each component MUST satisfy all relative-path invariants. No path separator is encoded.


SHA-256 Digest Encoding

A SHA-256 digest is encoded as exactly 32 bytes.

The bytes appear in the order defined by SHA-256.

No length field is encoded because the digest length is fixed.

The canonical encoding contains the digest bytes themselves, not a hexadecimal, Base64, or other textual representation.


ExternalFile

The canonical encoding of an ExternalFile DCI consists of

  1. the ExternalFile DCT identifier,
  2. the canonical encoding of its relative path, and
  3. the 32-byte SHA-256 digest of the external file contents.

Thus,

0f
|| PATH
|| SHA256

where PATH is the relative-path encoding defined above and SHA256 is the digest of the exact external file byte sequence.

No file contents are included in the canonical encoding of the ExternalFile DCI.

The content digest participates directly in ExternalFile identity. Different external byte sequences are expected to have different SHA-256 digests under the cryptographic collision-resistance assumption; this is not a mathematical guarantee.

The path also participates directly in ExternalFile identity. Two ExternalFile DCIs with identical external contents but different relative paths are distinct.

ExternalArray

The canonical encoding of an ExternalArray DCI consists of

  1. the ExternalArray DCT identifier,
  2. the canonical encoding of its relative path,
  3. the one-byte portable Array type identifier,
  4. the Array rank encoded using the canonical unsigned integer encoding,
  5. each dimension of the Array shape encoded using the canonical unsigned integer encoding, and
  6. the 32-byte SHA-256 digest of the external Array element byte stream.

Thus, for an ExternalArray of rank (R) with shape

$$ (d_0,d_1,\ldots,d_{R-1}), $$

the canonical encoding is

10
|| PATH
|| ARRAY_TYPE
|| U(R)
|| U(d_0) || U(d_1) || ... || U(d_(R-1))
|| SHA256

where

  • PATH is the canonical relative-path encoding,
  • ARRAY_TYPE is the one-byte portable Array type identifier,
  • U is the canonical unsigned integer encoding, and
  • SHA256 is the digest of the canonical Array element byte stream.

The logical external payload is exactly the canonical Array element byte stream defined for the specified Array type and shape. That stream contains no DCT identifier, Array type identifier, rank, shape, digest, header, or other framing metadata. A storage compression envelope is outside the logical stream.

The Array type and shape determine the exact logical byte length, after any decompression. SHA-256 is computed over that logical stream. Persistence defines uncompressed .bin, gzip .bin.gz, and HDF5 storage; the canonical DCI encoding itself is unchanged.

Two ExternalArray DCIs are equal if and only if their

  • relative paths,
  • Array types,
  • shapes, and
  • content digests

are equal.

ExternalArray and Array are distinct DCTs and therefore never have equal instances, even when an ExternalArray refers to the same numerical element values represented by an Array DCI.

The path and digest both participate in ExternalArray identity.

A resource found at the specified path whose byte sequence has a different SHA-256 digest does not satisfy the ExternalArray DCI.

A missing or inaccessible external resource does not change the identity of the ExternalArray DCI; it prevents the external contents from being retrieved or verified.

Canonical Encoding Validity

A byte sequence is a valid canonical encoding of a DCI if and only if it satisfies all encoding rules defined by this specification and encodes a valid instance of the identified DCT.

A decoder of canonical DataCanon encoding MUST reject any byte sequence that violates these requirements.

Complete Consumption

When a byte sequence is presented as the canonical encoding of exactly one DCI, decoding MUST consume the entire byte sequence.

Bytes remaining after a complete DCI has been decoded make the input invalid as the canonical encoding of exactly one DCI.

Thus,

E(x) || additional_bytes

is not the canonical encoding of x.

DCT Identifiers

The first byte of every DCI encoding MUST be a DCT identifier defined by the applicable version of the DataCanon specification.

An unassigned DCT identifier is invalid.

The payload following the identifier MUST satisfy the encoding rules of that DCT.

Canonical Unsigned Integer Encoding

Every unsigned LEB128 value MUST use the minimum number of bytes required to represent its value.

An encoding containing a redundant high-order zero group is invalid.

A continuation bit indicating another byte MUST be followed by another byte.

An unterminated unsigned integer encoding is invalid.

Canonical Signed Integer Encoding

A signed Integer payload MUST consist of the canonical unsigned integer encoding of a value produced by the zigzag mapping.

Because the zigzag mapping is bijective over the mathematical integers, every canonically encoded nonnegative zigzag value determines exactly one mathematical integer.

Floating-Point Values

A Float payload MUST contain exactly eight bytes encoding a finite IEEE-754 binary64 value.

A payload representing NaN or positive or negative infinity is invalid in Version 1.

The same requirement applies at the corresponding width to portable floating-point Array elements.

Strings

A String payload MUST contain

  1. a canonical unsigned integer specifying the number of UTF-8 bytes, followed by
  2. exactly that number of bytes.

Those bytes MUST form a valid UTF-8 encoding of a sequence of Unicode scalar values.

Overlong UTF-8 encodings, surrogate code points, malformed UTF-8 sequences, and other byte sequences not representing Unicode scalar values are invalid.

No Unicode normalization is performed.

Bytes

A Bytes payload MUST contain a canonical unsigned integer byte count followed by exactly that number of bytes. Every byte value is valid; UTF-8 validation does not apply. Truncated payloads and nonminimal length encodings are invalid.

Fraction

A Fraction payload MUST contain two minimally encoded signed integer fields. The denominator MUST be positive. Missing fields, nonminimal encodings, and nonpositive denominators are invalid.

Atomic Types

The payload of an atomic DCI MUST contain exactly the number and DCTs of constituents required by its DCT.

Each constituent MUST itself have a valid canonical encoding and MUST satisfy all invariants required by the containing DCT.

For example, Power exponent fields MUST be positive mathematical integers. Direct fields use their prescribed payload encodings, not nested DCI encodings.

Ordered Composite Types

The encoded item, entry, or field count MUST equal the number of constituents that follow.

Each constituent DCI MUST have a valid canonical encoding.

For Mapping and Record, all uniqueness requirements defined by the corresponding DCT MUST also be satisfied.

Compositional numerical types

Every Sum and Product operand MUST have a valid complete canonical encoding. Sum operands MUST be Numerical DCIs; Product operands MUST be Scalar Numerical DCIs, including recursively scalar Sum and OrderedProduct operands. Operand encodings MUST be in nondecreasing lexicographic byte order; equal adjacent encodings are permitted and MUST retain their multiplicities.

An OrderedProduct contains its declared number of Numerical DCIs in order, without sorting or deduplication.

A Power MUST contain a Scalar Numerical base DCI followed by two minimally encoded positive signed integer fields. A Quotient MUST contain two Scalar Numerical DCIs in numerator/denominator order.

Decoding MUST preserve constituent types, exponent structure, multiplicity, and nesting without evaluation or mathematical normalization.

Set

A Set payload MUST contain exactly the encoded number of members.

Each member MUST be a valid canonically encoded DCI.

Member encodings MUST occur in strictly increasing lexicographic byte order.

Because equal DCIs have identical canonical encodings, strict ordering also ensures that no duplicate Set members occur.

A Set whose members are not in canonical byte order is not canonically encoded.

Multiset

A Multiset payload MUST contain exactly the encoded number of member occurrences. Each occurrence MUST be a valid canonically encoded DCI.

Member encodings MUST occur in nondecreasing lexicographic byte order. Adjacent equal encodings are valid and represent repeated members. Decreasing member encodings are invalid.

A decoder MUST preserve every occurrence and MUST NOT deduplicate or flatten members.

UnorderedMapping

An UnorderedMapping payload MUST contain exactly the encoded number of entries.

Each key and value MUST be a valid canonically encoded DCI.

Entries MUST occur in strictly increasing lexicographic order of their complete canonical key encodings.

No two entries may have equal keys.

An UnorderedMapping whose entries are not in canonical key order is not canonically encoded.

Record

A Record payload MUST contain exactly the encoded number of fields.

The Record name and every field name MUST use valid Unicode string information encoding without a DCT identifier.

Field names MUST satisfy the uniqueness requirement of the Record DCT.

Field values MUST be valid canonically encoded DCIs.

Array

An Array payload MUST use a portable Array type identifier defined by this specification.

The encoded rank MUST equal the number of encoded dimensions.

Each dimension MUST be a canonically encoded nonnegative integer.

The number of elements is determined by the product of the dimensions, with an empty shape defining a rank-zero Array containing one element.

The canonical element byte stream MUST contain exactly

number_of_elements * bytes_per_element

bytes for the specified Array type.

Every element encoding MUST satisfy the requirements of that Array type.

In particular, floating-point and complex elements MUST NOT contain NaN or positive or negative infinity in Version 1.

The Array decoder consumes exactly the number of element bytes determined by its type and shape.

Relative Paths

A canonically encoded DataCanon relative path MUST contain at least one component.

The encoded component count MUST equal the number of components that follow.

Each component MUST be valid Unicode string information satisfying the DataCanon relative path requirements.

In particular,

  • a component MUST NOT be empty,
  • a component MUST NOT equal "." or "..", and
  • a component MUST NOT contain / or \.

ExternalFile

An ExternalFile payload MUST contain

  1. a valid DataCanon relative path, followed by
  2. exactly 32 bytes representing a SHA-256 content digest.

The external file itself is not required to be available in order for the ExternalFile DCI or its canonical encoding to be valid.

Availability and verification of the referenced external resource are separate operations.

ExternalArray

An ExternalArray payload MUST contain

  1. a valid DataCanon relative path,
  2. a portable Array type identifier,
  3. a valid rank and shape, and
  4. exactly 32 bytes representing the SHA-256 digest of the external canonical Array element byte stream.

The expected external byte-stream length is determined by the Array type and shape.

The external resource itself is not required to be available in order for the ExternalArray DCI or its canonical encoding to be valid.

Availability, byte-length verification, and content-digest verification of the referenced resource are separate operations.

Decoding

Canonical decoding is the inverse of canonical encoding.

For every DCI x,

decode(E(x)) = x

A conforming canonical decoder MUST reject any byte sequence that is not the canonical encoding of exactly one valid DCI.

Canonical decoding performs no mathematical simplification, type coercion, Unicode normalization, or other transformation of the represented DCI.

Fingerprinting

Every DataCanon Instance has a deterministic fingerprint.

The fingerprint of a DCI is the SHA-256 digest of its canonical encoding.

For DCI x with canonical encoding E(x),

$$ F(x) = \operatorname{SHA256}(E(x)). $$

A fingerprint is a sequence of exactly 32 bytes. Its conventional textual representation is the corresponding sequence of 64 lowercase hexadecimal characters. The textual representation is not itself the fingerprint.

Because equal DCIs have identical canonical encodings, equal DCIs necessarily have identical fingerprints.

Fingerprint equality does not define DCI equality. DCI equality is defined by the corresponding DCT.

The canonical encoding, rather than the fingerprint, is the complete representation of DCI identity.

Cryptographic Digests and Identity

DataCanon uses SHA-256 in two distinct roles:

  • to identify the contents of external resources, and
  • to compute fingerprints of DataCanon Instances.

SHA-256 is not mathematically injective. Distinct byte sequences may therefore have identical SHA-256 digests.

The equality of DataCanon Instances is defined exactly by their DCTs and constituents. For external DCTs, the content digest is a constituent of the DCI; the external byte sequence itself is not.

Consequently, the identification of external contents by their digests, and the use of fingerprints as practical identifiers, rely on the cryptographic collision resistance of SHA-256.

The cryptographic assumptions do not alter the definitions of DCI equality or canonical encoding.

Appendix A: Canonical Encoding Test Vectors

This appendix specifies fixed Version 1 canonical byte encodings for selected DCIs. Hexadecimal byte pairs are separated by spaces for readability; the spaces are not part of the encoded byte sequences. The test vectors provide independent conformance checks and do not change the encoding rules above.

DCICanonical encoding (hexadecimal bytes)
Null00
Boolean(false)01 00
Boolean(true)01 01
Integer(0)02 00
Integer(-1)02 01
Integer(42)02 54
Integer(128)02 80 02
Float(+0.0)03 00 00 00 00 00 00 00 00
Float(-0.0)03 80 00 00 00 00 00 00 00
String(“A”)04 01 41
OrderedProduct(empty)18 00
OrderedProduct(Integer 2, Integer 3)18 02 02 04 02 06
Constant(pi)17 00
Constant(e)17 01
Bytes(empty)16 00
Bytes(hexadecimal 00 ff)16 02 00 ff
Fraction(numerator=2, denominator=4)05 04 08
Complex(real=Integer(1), imaginary=Float(-0.0))07 02 02 03 80 00 00 00 00 00 00 00
List()08 00
List(Integer(1), Boolean(true))08 02 02 02 01 01
Tuple()09 00
Set()0a 00
Multiset()11 00
Sum()12 00
Product()13 00
Sum(Integer(1), Integer(1))12 02 02 02 02 02
Product(Integer(2), Integer(1))13 02 02 02 02 04
Power(base=Integer(5), numerator=1, denominator=3)14 02 0a 02 06
Quotient(Integer(2), Integer(3))15 02 04 02 06
Multiset(Integer(1))11 01 02 02
Multiset(Integer(1), Boolean(true), Integer(1))11 03 01 01 02 02 02 02
Multiset(Multiset(), Multiset())11 02 11 00 11 00
Set(Integer(1), Boolean(true))0a 02 01 01 02 02
Mapping()0b 00
Mapping(String(“a”) → Integer(1))0b 01 04 01 61 02 02
UnorderedMapping()0c 00
UnorderedMapping(String(“b”) → Integer(2), String(“a”) → Integer(1))0c 02 04 01 61 02 02 04 01 62 02 04
Record(name=“R”, field “x” → Integer(1))0d 01 52 01 01 78 02 02
Array(type=bool, shape=(2,), elements=[false, true])0e 00 01 02 00 01
Array(type=float64, shape=(1,), elements=[-0.0])0e 0b 01 01 80 00 00 00 00 00 00 00

The following additional vectors are displayed as concatenated fields to make their structure explicit.

ExternalFile with path components ( "x.bin" ) and a 32-byte all-zero digest:

0f
01
05 78 2e 62 69 6e
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

ExternalArray with path components ( "x.bin" ), Array type float64, shape (2,), and a 32-byte all-zero digest:

10
01
05 78 2e 62 69 6e
0b
01 02
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

The all-zero digests in the last two vectors are illustrative digest constituents. The test vectors specify DCI metadata encoding and do not assert the existence of external resources with those digests.

6.2 - Shorthand

DataCanon Shorthand — Version 1 Draft

Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1
Scope: Strict Shorthand parsing and identity-preserving rendering
Out of scope: Optional Shorthand preprocessing, YAML interchange, HDF5, and persistence conventions


Introduction

DataCanon Shorthand is a human-readable textual representation of DataCanon Instances (DCIs). It provides notation for all 24 DataCanon Types (DCTs) defined by DataCanon Core Version 1.

Shorthand does not define DCI identity. The core specification defines DCTs, equality, canonical binary encoding, and fingerprinting. A Shorthand document represents exactly one DCI; its whitespace and textual layout do not participate in that DCI’s identity.

A conforming Shorthand parser MUST recover the DCI specified by a valid Shorthand document. A conforming Shorthand renderer MUST produce valid Shorthand that a conforming parser recovers as a DCI equal to the original.

Shorthand is not a canonical textual encoding. Multiple texts may represent the same DCI, and no particular rendering layout is required.

Conformance and terminology

The terms MUST, MUST NOT, SHOULD, and MAY indicate requirements and recommendations for implementations of this specification.

The definitions, invariants, and equality relations of DataCanon Core Version 1 take precedence over any convenience afforded by Shorthand syntax. A syntactically well-formed expression that violates a DCT invariant is invalid Shorthand.

A conforming strict parser MUST:

  • parse exactly one complete DCI and reject unexpected trailing input;
  • interpret the constructs specified here according to their defined DCTs;
  • preserve constituent identity, including unreduced Fractions and floating-point signed zero;
  • reject invalid DCT constituents and malformed syntax; and
  • operate without performing substitutions, mathematical evaluation, or application-specific transformations.

A conforming renderer MUST preserve DCI equality across a strict render–parse round trip. It MAY choose whitespace, indentation, line breaks, and other identity-preserving layout conventions.

The separately specified Shorthand preprocessor MAY be applied before strict parsing. It is not part of the strict grammar.

Lexical rules

Input and whitespace

Shorthand is Unicode text. Strings may contain Unicode scalar values as defined by DataCanon Core Version 1.

Outside quoted strings, the recognized whitespace characters are the ASCII space (U+0020), horizontal tab (U+0009), carriage return (U+000D), and line feed (U+000A). Whitespace may appear between syntactic tokens except where this specification explicitly requires adjacent characters.

Strict Shorthand does not define comments. An empty or whitespace-only document is invalid.

Digits and identifiers

A digit is one of the ASCII characters 0 through 9.

An identifier begins with an ASCII letter (A–Z, a–z) or underscore (_), followed by zero or more ASCII letters, digits, or underscores.

The strict grammar recognizes only the identifiers explicitly assigned meaning below. Arbitrary identifiers are not variables and MUST be rejected in strict parsing.

Identifiers and keywords are case-sensitive. In particular, true, false, and null are lowercase.

Strings

A String literal is delimited by double quotation marks ("). Its contents are a sequence of Unicode scalar values, optionally using the following escapes:

Source charactersDecoded character
\"quotation mark
\\backslash
\nline feed
\rcarriage return
\thorizontal tab

No other backslash escapes are defined. A backslash followed by any other character MUST be rejected. Literal Unicode characters, including literal line breaks, may appear inside a quoted string, provided the decoded result is a valid DataCanon String. No Unicode normalization occurs.

A renderer MUST escape quotation marks and backslashes so that they cannot terminate or alter a String literal; it MAY escape the three supported control characters above.

Block Strings

In addition to ordinary quoted Strings, Shorthand supports literal and folded block Strings. Both forms represent the String DCT and do not introduce new DataCanon Types.

A literal block begins with """ followed immediately by LF:

"""
first line
second line
"""

This represents the String first line\nsecond line.

A folded block begins with """> followed immediately by LF:

""">
This is a long String
written across several
physical lines.
"""

This represents This is a long String written across several physical lines.

Structural indentation. The opening delimiter MUST be the first non-space content on its physical line. Its indentation consists exclusively of spaces and defines the block’s structural indentation. The closing """ MUST begin at exactly the same indentation. It MAY be followed by surrounding Shorthand syntax, such as a comma, colon, or closing collection delimiter.

Each nonempty content line MUST contain at least the structural indentation. Exactly that indentation is removed. Completely empty physical lines are permitted without indentation. Any additional indentation is content.

Literal blocks. After structural indentation is removed, all content characters are preserved, including leading and trailing whitespace and physical LF characters between content lines. The newline immediately following the opening delimiter and the newline immediately preceding the closing delimiter are structural and are not included in the String.

Folded blocks. After structural indentation is removed, whitespace-only lines are discarded, trailing whitespace is removed from nonblank content lines, and the remaining lines are joined with exactly one U+0020 SPACE. Additional leading whitespace on nonblank lines is preserved. Consecutive blank lines do not introduce additional spaces or newlines.

Neither block form interprets escape sequences. Backslashes are literal characters. Strings that cannot be represented using block syntax can always be expressed using ordinary quoted Strings.

Block formatting does not participate in DCI equality, canonical byte encoding, or fingerprinting. Different textual representations that produce the same String DCI have identical canonical identity.

Numerical literals

An integer literal consists of an optional leading - followed immediately by one or more ASCII digits. It denotes an arbitrary-precision mathematical integer in decimal notation. A leading + is not supported.

A floating-point literal consists of an optional leading -, a nonempty sequence of ASCII digits, and at least one of:

  • a decimal point followed by one or more ASCII digits; or
  • an exponent introduced by e or E, followed by an optional + or - and one or more ASCII digits.

A floating-point literal may have both a decimal part and an exponent, such as 1.25e-3. The scalar syntax does not require support for a trailing decimal point without following digits (for example, 1.). Array element syntax has a separate allowance for that form.

A floating-point literal denotes the finite IEEE-754 binary64 value obtained by correctly rounding its decimal value. The parser MUST reject values that cannot produce a valid finite Float DCI. Positive and negative zero are distinct.

A Fraction literal consists of an integer literal, /, and one or more ASCII digits denoting a positive denominator, with no intervening whitespace. The numerator and denominator are preserved exactly; no reduction occurs.

Examples:

TextDCTInterpretation
42Integerinteger 42
-0Integerinteger 0
-0.0Floatnegative binary64 zero
1e3Floatbinary64 1000.0
2/4Fractionnumerator 2, denominator 4

Strict Shorthand constructs

Primitive DCTs

DCTSyntaxExample
Nullnullnull
Booleantrue or falsetrue
Integerinteger literal-42
Floatfloating-point literal3.5
Stringquoted String literal"hello"

Named mathematical constants

Pi denotes Constant pi; E denotes Constant e. Both tokens are case-sensitive and are accepted wherever a Numerical DCI is permitted. They are constant tokens, not function calls or variables.

The explicit constructor is constant("pi") or constant("e"), with exactly one quoted String argument. The String supplies the Constant’s direct name information; it does not become a constituent DCI. Unknown names and bare name arguments such as constant(pi) MUST be rejected.

The default renderer emits Pi or E; longhand emits constant("pi") or constant("e"). Numerical exponent syntax such as E**2 constructs a Power DCI, not another spelling of Constant e. Quoted "Pi" remains a String. Array element syntax does not accept constants or structured numerical DCIs.

Bytes

A Bytes DCI is written using one quoted hexadecimal String:

bytes("")
bytes("00ff80")

The decoded String MUST contain an even number of ASCII hexadecimal digits (0–9, a–f, or A–F). Each pair denotes one byte in sequence order. An empty String denotes an empty byte sequence. Separators, whitespace within the decoded String, 0x prefixes, and non-ASCII digits are invalid.

Exactly one String argument is required. Whitespace between syntactic tokens and an optional trailing comma are permitted. A renderer emits lowercase hexadecimal digits. The text describes the exact opaque byte sequence; it does not perform character decoding or interpret a file format.

Fraction

A Fraction is written as a Fraction literal, for example 1/3 or 2/4, or using the explicit constructor fraction(numerator, denominator). Both constructor arguments MUST be integer literals; the denominator MUST be positive. Exactly two arguments are required, and an optional trailing comma is permitted. Constituents are preserved without reduction. Fraction is distinct from Integer and Float even when their mathematical values agree.

Sum and Product

sum()
sum(1, 2, 3)
product()
product(1/2, sqrt(3))

Sum arguments are Numerical DCIs; Product arguments are Scalar Numerical DCIs. Empty and singleton instances are valid, repeated operands are retained, and an optional trailing comma is permitted. Operand order is not semantic; multiplicity and nesting are semantic.

Bounded infix forms construct the same flat nodes:

1 + 2 + 3            -> sum(1, 2, 3)
1/2 * sqrt(3)        -> product(1/2, sqrt(3))
1 + 2 * 3           -> sum(1, product(2, 3))

Multiplication binds more tightly than addition. Explicit constructor operands are not flattened: sum(1, 2) + 3 represents sum(sum(1, 2), 3). The numerical forms construct structure; they perform no arithmetic.

Binary subtraction and general infix division are not supported. Negative numeric literals remain available, for example 1 + -2. Parentheses retain Tuple syntax; grouping is expressed through constructors, not grouping parentheses. Thus (1 + 2) is a singleton Tuple containing a Sum.

OrderedProduct and array expressions

ordered_product(item, ...) constructs an OrderedProduct DCI, preserving operand order and nesting. Zero or more Numerical DCI operands are accepted; an optional trailing comma is permitted. No rank, shape, or mathematical compatibility checks are performed during parsing.

The infix + always constructs Sum and accepts all Numerical DCIs. Infix * constructs Product when every factor is a Scalar Numerical DCI; otherwise it constructs OrderedProduct. Operand classification is structural, not numerical evaluation. Existing precedence is unchanged: ** binds before *, which binds before +. Complex DCIs and collection DCIs other than Array remain invalid numerical operands.

2 * array([[1, 0], [0, 1]])
2 * array([[0, 1], [1, 0]]) + 3 * array([[1, 0], [0, -1]])
ordered_product(2, 3)

Explicit product(...) remains scalar-only. Scalar-only OrderedProducts MUST render using ordered_product(...), because their infix form would construct a different DCT. Renderers MUST also retain explicit constructors wherever needed to preserve nested Sum, Product, and OrderedProduct boundaries. Longhand always uses ordered_product(...); ordinary Shorthand may use * when reparsing preserves the exact DCI. ordered_product is a collection layout name accepted by per-type layout configuration.

Power

power(base, numerator)
power(base, numerator, denominator)

The base is a Scalar Numerical DCI; numerator and denominator are positive mathematical integers written as integer literals. The omitted denominator defaults to one. Optional trailing commas are permitted. Exponent constituents are not reduced.

A bounded infix form accepts a positive Integer literal without parentheses, or a positive Integer/Fraction literal inside exponent-specific parentheses:

5**2
5**(2)
5**(1/2)

An infix power MUST NOT have a Fraction base. Expressions such as 1/2**3 and 1/2**(1/3) are invalid because they resemble arithmetic division with a different precedence convention. Use the explicit constructor instead:

power(1/2, 3)
power(1/2, 1, 3)

This restriction applies only to the infix spelling. Fraction remains a valid Power base, and Fraction literals remain permitted in parenthesized exponents.

Power binds more tightly than multiplication and addition. The two stars MUST be adjacent. Whitespace around the operator and inside exponent parentheses is permitted. Exponent parentheses are part of this construct and do not introduce general expression grouping.

Unparenthesized rational exponents, Float exponents, and chains such as 5**2**3 are invalid. Nested powers use explicit power(...) constructors. sqrt(base) is a convenience spelling for power(base, 1, 2) and constructs a Power DCI without evaluation. The denominator always participates in the resulting DCI, whether or not it was written explicitly.

Quotient

quotient(numerator, denominator)
quotient(2, sqrt(3) + sqrt(2))

Both arguments are Scalar Numerical DCIs. Exactly two arguments are required; an optional trailing comma is permitted. The parser performs no division, cancellation, or denominator rationalization. Fraction literal syntax remains unchanged and constructs Fraction, not Quotient.

Complex

A Complex is written:

complex(real, imaginary)

Each component MUST parse to a Scalar Numerical DCI. The two components retain their DCTs and their individual identities.

Example:

complex(1/2, 1/2 * sqrt(3))

This notation is distinct from the special complex-number syntax used inside Arrays.

List and Tuple

A List uses square brackets or list(...); a Tuple uses parentheses or tuple(...). Constructor arguments are the items themselves, rather than a single collection to convert. Elements are arbitrary DCIs separated by commas. Both forms may be empty and may have an optional trailing comma.

[]
[1, "x", 2/4]
(1, 2)
(1)
()

A single-element Tuple is (1); no trailing comma is required. List and Tuple remain distinct DCTs regardless of their contents.

Set and Mapping

Curly braces denote a nonempty Set or a Mapping, distinguished by the presence of : following the first parsed value.

{1, 2, 3}
{"a": 1, "b": 2}
{}

{} is the empty Mapping. The empty Set is written set(). The identifier emptyset is not part of strict Shorthand.

A Set may also be written set(item, ...), including set() and singleton Sets. Constructor arguments are individual members. An optional trailing comma is permitted in the constructor form.

A Mapping may also be written mapping(key: value, ...), including mapping(). The constructor follows the same entry order, key uniqueness, and optional trailing-comma rules as the brace form.

Set members are arbitrary DCIs separated by commas. They MUST be pairwise distinct under DCI equality. Set member order is not semantic. The brace form does not admit a trailing comma; the set(...) constructor does.

A Mapping consists of comma-separated key: value entries. Keys and values are arbitrary DCIs, and keys MUST be pairwise distinct under DCI equality. Entry order is semantic. A Mapping may have an optional trailing comma.

Multiset

A Multiset is written as comma-separated DCI members enclosed in multiset(...):

multiset()
multiset(1, 1, 2)
multiset("Sr", [1, 2], [1, 2])
multiset(multiset(1, 1), 2)

Members may belong to any DCT. Repeated members are permitted and MUST be retained. Member order is not semantic; multiplicity and nesting are semantic. For example, multiset(1, 1, 2) and multiset(2, 1, 1) represent equal DCIs, while multiset(1, 2, 2) represents a different DCI.

An optional trailing comma is permitted. multiset() is the empty Multiset. A Multiset is distinct from a Set, List, or Tuple with otherwise equal members.

A renderer MAY choose any member order. It MUST preserve every occurrence and MUST NOT flatten nested Multisets. Canonical byte sorting imposes no textual member-order requirement.

UnorderedMapping

An UnorderedMapping is written:

umapping("b": 2, "a": 1)
umapping()

It follows Mapping entry syntax and uniqueness requirements, but entry order is not semantic. An optional trailing comma is permitted.

Record

A Record is written with a quoted name followed by zero or more quoted field names and DCI values:

record("Point", "x": 1.0, "y": 2.0)
record("Empty")

The Record name and each field name are DataCanon Strings. Field names MUST be distinct. Field order and the Record name participate in DCI identity. A trailing comma after the Record name or final field is permitted.

Record names and field names need not be valid Shorthand identifiers because they are quoted Strings.

Arrays

Array construct

An Array is written:

array(DATA)
array(DATA, dtype="TYPE")
array(DATA, shape=(DIMENSIONS))
array(DATA, dtype="TYPE", shape=(DIMENSIONS))

The dtype and shape options may occur in either order, at most once each. An optional comma before the closing ) is permitted.

DATA is either a single Array scalar (rank zero) or a nested bracketed collection of Array scalar elements. Array elements are not DCIs. A nested Array body is not a DataCanon List.

Examples:

array(1.0)
array([1, 2, 3])
array([[1.0, 2.0], [3.0, 4.0]])
array([1.0, -0.0], dtype="float32")
array([], dtype="float64", shape=(0, 3))

The bracketed numerical body uses comma-separated entries and rectangular nesting. The current strict parser does not permit a trailing comma inside a bracketed Array body.

Array scalar syntax

Array scalar elements may be true, false, real numeric literals, or complex numeric literals of the form:

REAL+MAGNITUDEj
REAL-MAGNITUDEj

REAL and MAGNITUDE use Array numeric syntax. The real part may be negative; the imaginary magnitude is unsigned. No whitespace is permitted within the complex literal. Exponents are permitted in either component, and the decimal point in an Array numeric literal may have no following digit (1.).

Examples:

array([true, false])
array([1., 2.5])
array([1.0+2.0j, 3.0-4.0j])
array([1e-3+2e4j, -3e2-4e-1j])

1.0+2.0j is not a valid top-level Complex DCI expression.

Portable Array types

The resulting Array MUST have one of the 14 portable Array types defined by DataCanon Core Version 1:

bool, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float16, float32, float64, complex64, and complex128.

A dtype option is a quoted type name. The portable type names above are the interoperable spelling; the Python reference parser also accepts dtype aliases recognized by NumPy when they produce a portable type. Whether to standardize aliases beyond the portable names is open for Version 1 finalization.

Inference and explicit conversion

When dtype is omitted, the reference behavior is NumPy’s array construction from the parsed Array scalar values. When shape is omitted, the reference behavior is the shape inferred from that construction.

When dtype is present, the reference behavior is NumPy Array construction with that dtype. NumPy’s conversion behavior, including any permitted rounding or lossy conversion, is intentionally retained. Shorthand does not impose a separate losslessness rule on explicit dtype conversion. The final Array MUST nevertheless satisfy the portable Array DCT’s invariants, including finiteness of floating-point and complex elements.

When shape is present, the constructed Array is reshaped using NumPy-compatible reshape semantics; the requested shape MUST be valid, have nonnegative dimensions, and preserve the element count.

Reference-version item pending: Before this draft becomes a final cross-language conformance standard, the NumPy reference version MUST be fixed. Julia and other implementations need only reproduce the resulting DCI, not use NumPy internally. The detailed inference and conversion algorithm is deliberately not reproduced in this draft.

Array identity and rendering

Array type, shape, and the exact portable element values participate in identity. Renderers MUST include dtype and shape annotations whenever omitting them would cause a conforming parser to infer a different DCI.

A renderer MUST preserve the bit identities of floating-point Array elements, including positive and negative zero, and the corresponding components of complex Array elements.

The canonical element byte stream and canonical Array encoding are defined only by DataCanon Core Version 1, not by Shorthand formatting.

External resources

Relative paths

Shorthand writes a DataCanon relative path as one quoted String whose components are joined by the ASCII slash (/). The parser splits that String on / to reconstruct the abstract DataCanon relative path.

Each component MUST satisfy the relative-path invariants of DataCanon Core Version 1. In particular, empty components, . and .., and components containing backslashes are invalid.

Parsing an external DCI does not access its external resource.

ExternalFile

external_file("inputs/INCAR", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")

The first argument is a relative path String. The second is a quoted hexadecimal SHA-256 digest decoding to exactly 32 bytes. A renderer SHOULD use 64 lowercase hexadecimal characters. A conforming parser MUST accept that spelling and MAY accept uppercase hexadecimal digits. Whitespace within a hexadecimal digest String is not part of the portable normative spelling.

ExternalArray

external_array("data/forces.bin", "float64", (100, 3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")

The arguments, in order, are a relative path String, a portable Array type String, a Tuple of nonnegative Integer dimensions, and a quoted SHA-256 digest decoding to exactly 32 bytes.

The external byte stream is defined by DataCanon Core Version 1. Shorthand carries the path, Array type, shape, and digest, but not the external contents.

Rendering requirements

A conforming renderer MUST satisfy, for every valid DCI x:

parse_shorthand(render_shorthand(x)) = x

Here equality means DCI equality as defined by DataCanon Core Version 1. The equation describes the behavior of conforming parser and renderer operations, not mandated function names in other programming languages.

Shorthand and longhand

Longhand is the form of Shorthand that uses explicit constructors for structured values and ordinary literals for Null, Boolean, Integer, Float, and String. For example, sum(1, 1.0, fraction(1, 2)) is longhand: the integer and floating-point literals already identify their DCTs. Longhand does not wrap literals as integer(1), float(1.0), or string("hello"); those wrappers are not part of the Version 1 grammar.

The distinction is syntactic, not the Core classification of primitive, atomic, and composite DCTs. Fraction and Constant are primitive but use fraction(...) and constant(...), respectively, in longhand. Longhand is a subset of the same grammar, accepted by the same parser. Shorthand permits constructors and shorter spellings to be freely mixed. No additional fully wrapped rendering mode is defined.

The Python renderer accepts longhand=False (the default) or longhand=True. With False, it uses shorter spellings where appropriate, falling back to constructors to preserve structure. With True, it produces longhand. The option MUST be a Boolean. Longhand selection is independent of layout layout, alignment, and indentation. Both settings produce valid strict Shorthand representing equal DCIs.

DCTShorthand examplesLonghand
Fraction1/2fraction(1, 2)
List[1, 2]list(1, 2)
Tuple(1, 2)tuple(1, 2)
Set{1, 2}set(1, 2)
Mapping{"a": 1}mapping("a": 1)
Sum1 + 2sum(1, 2)
Product2 * sqrt(3)product(2, power(3, 1, 2))
Power5**2, 5**(1/3), or sqrt(5)power(5, 2), power(5, 1, 3), or power(5, 1, 2)

Longhand applies recursively to constituent DCIs. Null, Boolean, Integer, Float, and String retain their literal spellings. Fraction uses fraction(...) even though it is classified as primitive; Bytes retains bytes("..."). Constructs having only one spelling retain that spelling. Array bodies continue to use their specialized element syntax; their brackets do not represent List DCIs. Shape Tuples may use either Tuple spelling.

Shorthand rendering MUST use constructors wherever necessary to preserve identity. In particular, empty and singleton Sums and Products retain constructors; nested Sums and Products MUST NOT be flattened by rendering. A Sum used as a Product operand requires an explicit constructor because Shorthand does not provide general grouping parentheses. Fraction bases, signed literal bases, and complicated bases may use power(...) to avoid precedence ambiguity. Multiline output may also use constructors for clearer layout.

For example:

1 + sum(2, 3)
2 * product(3, 4)
2 * sum(3, 4)
power(1/2, 3)

Longhand selection and any constructor fallback do not participate in DCI identity or canonical binary encoding.

Renderers MAY choose their own whitespace, indentation, line breaking, column alignment, and compactness policies. The Python binding’s formatting options are not distinct dialects or normative cross-language layout requirements.

The Python binding provides independent layout, align, indent, and padding options, plus custom_layout per-type overrides. For multiline output, standard places both delimiters on separate lines, semi-compact shares the opening line with the first item or entry, and compact also shares the closing line with the final content. inline forces the selected value and all its descendants onto one line. An enclosing inline requirement overrides child layout selections and disables table alignment within that subtree.

Indentation controls structural nesting on new lines, defaulting to two spaces. Padding controls same-line gaps beside collection and constructor delimiters, defaulting to one space, including flat values. Shared-opener continuation items align with the first item at relative column len(opening) + padding; indentation does not add another gap there. Standard multiline delimiters receive no padding because they occupy separate lines. Compact shared closers receive the requested padding before them. Both options are nonnegative integers. Primitive literal forms and specialized Array element syntax remain unchanged; Array constructors can still be rendered inline.

Lists, Tuples, Sets, and Multisets normally become multiline when they directly contain a List or Tuple, or an item already spanning multiple lines. Inline layout overrides that rule. Rectangular List/Tuple rows can be aligned inside any of those four collection types. Empty collections retain their empty forms. All layout choices preserve DCI identity and the same strict grammar; the Python API specifies their precise presentation behavior.

Renderers MUST NOT normalize numerical structures mathematically, erase Float signed zero, reorder ordered collections, or change any other constituent that participates in DCI identity.

A renderer of an unordered collection MAY choose any entry/member order accepted by the strict parser. The canonical ordering of the binary encoding does not impose a textual ordering requirement.

Invalid input and error handling

A conforming parser MUST reject malformed syntax, unsupported identifiers, invalid numeric values, invalid escape sequences, duplicate keys or fields, invalid Array shapes or types, invalid external paths or digests, and input remaining after a complete DCI has been parsed.

An implementation MAY use its own error types and diagnostics. No specific error-message text is required by this specification.

Implementations MAY impose resource limits on text size, nesting depth, Array allocation, or integer magnitude. Such limits are implementation constraints and do not change DCI identity.

Relationship to other DataCanon specifications

  • DataCanon Core V1 defines the DCTs, equality, canonical encoding, and fingerprinting.
  • Shorthand Preprocessing V1 (separate document) will define compact List notation, general substitutions, and restricted scalar eval(...) transformations before strict parsing.
  • YAML Interchange V1 (separate document) may embed strict Shorthand using the $ convention.
  • HDF5 V1 and Persistence V1 (separate documents) will define storage and external-resource conventions.

Neither the preprocessor nor a persistence mechanism alters the strict Shorthand grammar defined here.

Appendix A — Conformance examples

The conformance examples identify the DCT or property that a conforming strict parser MUST recover. They are not prescribed renderer output.

InputExpected result
nullNull
falseBoolean false
-42Integer -42
-0.0Float negative zero
bytes("")Empty Bytes
bytes("00FF")Bytes containing 00 ff
"a\\nb"String with a line feed
2/4Fraction with numerator 2, denominator 4
sum(1, 1, 2)Sum retaining duplicate terms
1/2 * sqrt(3)Product containing Fraction and Power
sqrt(3)Power with numerator 1 and denominator 2
power(5, 2)Power with denominator 1
quotient(1, sum(2, 3))Quotient with a Sum denominator
complex(1, 2.0)Complex(Integer, Float)
[]Empty List
()Empty Tuple
(1)One-element Tuple
{}Empty Mapping
set()Empty Set
{1, 2}Two-member Set
multiset()Empty Multiset
multiset(1, 1, 2)Multiset with three occurrences
multiset(2, 1, 1)Equal to multiset(1, 1, 2)
multiset(1,)Multiset with one occurrence
{"a": 1, "b": 2}Ordered Mapping
umapping("b": 2, "a": 1)UnorderedMapping
record("Point", "x": 1)Record
array([1.0, 2.0])Array with inferred portable type and shape

The following are invalid strict Shorthand:

1 2
'hello'
+1
1/0
1/-2
-sqrt(2)
power(5, 0)
power(5, 1, 0)
5**2**3
1/2**3
1/2**(1/3)
quotient(1)
sum(true)
{1, 1}
{"a": 1, "a": 2}
array([1.0 + 2.0j])

Appendix B — Outstanding Version 1 finalization items

The finalization items are deliberately recorded rather than silently resolved by assumptions about other languages:

  1. Pin the NumPy reference version governing Array inference, dtype conversion, and shape construction. This is the principal remaining cross-language portability dependency.
  2. Decide whether NumPy dtype aliases beyond the 14 portable Array type names are normative input or only permissive Python extensions.
  3. Confirm the exact treatment of optional noncanonical hexadecimal digest spellings, if interoperability with hand-authored external references warrants it.
  4. Publish cross-implementation vectors pairing Shorthand input with canonical Core V1 bytes, especially for Arrays, compositional numerical types, and external references.

None of these items changes the 24 core DCT definitions or the Core V1 canonical byte encoding.

6.3 - Shorthand Preprocessing

DataCanon Shorthand Preprocessing — Version 1 Draft

Status: Draft for independent implementation review
Depends on: DataCanon Shorthand Version 1; DataCanon Core Version 1
Scope: Optional textual transformation of augmented Shorthand into strict Shorthand
Out of scope: DCI construction, canonical encoding, persistence, application-specific mathematical aliases


Introduction

DataCanon Shorthand preprocessing is an optional text-to-text transformation intended for human-authored Shorthand. It supports three conveniences:

  1. compact List notation;
  2. named textual substitutions; and
  3. explicitly requested scalar arithmetic with eval(...).

The preprocessor does not construct, inspect, or validate DataCanon Instances (DCIs). Its output is intended to be parsed by a strict Shorthand parser, which remains responsible for all syntax and DCT validity checks. Preprocessing does not change DataCanon Core identity or canonical encoding.

An implementation MAY offer a combined parsing interface. A strict Shorthand parser MUST NOT implicitly enable preprocessing. A human-facing command-line interface MAY preprocess Shorthand input by default, provided it documents that behavior and offers an explicit mode requiring strict Shorthand without preprocessing. Such a frontend does not change the strict grammar or the requirements on persisted strict Shorthand representations.

Conformance and terminology

MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.

A conforming preprocessor accepts Unicode text and optionally a mapping from substitution names to replacement text. For inputs within its supported language and resource limits, it MUST apply the transformations in this order:

  1. Expand compact Lists, throughout the document, including substitution definitions.
  2. Resolve substitutions, processing inline definitions in source order using external names and earlier inline definitions. Evaluate explicit eval(...) expressions in each resolved definition before making it available to later definitions, then substitute names in the document body.
  3. Evaluate explicit eval(...) expressions remaining in the body, replacing each with an ordinary Shorthand numeric literal.

The result is text, not a DCI. An output that fails strict Shorthand parsing is not necessarily a preprocessing error: textual substitution values are not individually required to be valid Shorthand.

The preprocessor MUST NOT silently perform arbitrary mathematical multiplication, interpret application-defined symbols such as I or 2I, or evaluate expressions that are not explicitly enclosed in eval(...).

Lexical conventions

Strings and protected regions

A double-quoted string is protected during all three transformations. Compact List delimiters, substitution names, |, and eval(...) appearing inside a quoted string are treated as ordinary text.

The preprocessor recognizes a backslash as escaping the immediately following character for the purpose of locating the end of a quoted string. Validation of permitted String escapes belongs to the strict Shorthand parser.

For example:

"<1 2>"
"eval(1+2)"
"a | b"

are left unchanged by preprocessing.

Bytes literals

The hexadecimal String argument of bytes("...") is protected like every other quoted String. Preprocessing MUST NOT substitute names, expand Compact Lists, or evaluate text inside that String. Bytes parsing and hexadecimal validation belong to the strict Shorthand parser.

Substitution identifiers

A substitution name consists of an ASCII letter or underscore, followed by zero or more ASCII letters, digits, or underscores:

[A-Za-z_][A-Za-z0-9_]*

Names are case-sensitive. The preprocessor does not attach DataCanon meaning to them.

Delimiters and nesting

The preprocessor recognizes quoted strings and balanced (), [], and {} when locating top-level delimiters and splitting compact List contents. It recognizes matching parentheses when locating the end of an eval(...) expression.

No comments are defined by this preprocessing specification.

Compact List notation

General form

A compact List is enclosed in angle brackets:

<contents>

It expands to ordinary Shorthand List syntax using square brackets. A single item remains a one-element List:

<6>              -> [6]
<1 2 3>          -> [1, 2, 3]
<1 2; 3 4>       -> [[1, 2], [3, 4]]
<1 2, 3 4>       -> [[1, 2], [3, 4]]

Compact Lists may occur anywhere outside quoted strings, including inside Array syntax, Mapping values, and inline substitution definitions. Nested angle-bracket groups are expanded from the inside outward.

An empty compact List (<> or a whitespace-only group) is invalid. Use ordinary [] for an empty List.

Splitting rules

Within each compact List group, delimiters are considered at the top level of that group: separators inside quoted strings or balanced (), [], and {} do not split an item.

The reference transformation applies the following separator precedence, from highest to lowest:

  1. line feed (\n);
  2. semicolon (;);
  3. comma (,);
  4. horizontal whitespace (space, tab, carriage return).

At the first level having more than one nonempty part, each part is recursively transformed and the results are enclosed in [...]. If no level splits the contents, the contents form a single item. The outermost compact group always yields a List.

This is a textual convenience, not Array inference. In particular, <1 2; 3 4> expands to a List of Lists. array(<1 2; 3 4>) may subsequently be parsed as an Array by strict Shorthand.

Examples:

<[1,2] 3>             -> [[1,2], 3]
<eval(1 + 2) 0>       -> [eval(1 + 2), 0]
<"a > b" 3>           -> ["a > b", 3]

The eval(...) in the second example is evaluated only in the third preprocessing stage.

Compatibility note: the current reference implementation discards empty parts when splitting compact Lists. The treatment of repeated/trailing separators should be fixed explicitly before Version 1 is finalized; independent implementations SHOULD not rely on such inputs in the meantime.

Textual substitutions

External substitutions

An application MAY supply a mapping of substitution names to replacement text. Every name MUST satisfy the identifier rule above, and every replacement MUST be text.

For example, with external substitutions S = '"Sr"' and P = '[0.5, 0.5, 0.5]':

{"species": S, "position": P}

becomes:

{"species": "Sr", "position": [0.5, 0.5, 0.5]}

Substitution replaces complete identifier-like tokens outside quoted strings; it MUST NOT replace a substring of a longer ASCII identifier. For example, a substitution for a does not change alpha or a2.

Replacement text is inserted literally. It is not parsed or rendered as a DCI, and its whitespace is not normalized.

Inline substitutions

A document MAY begin with inline definitions separated from the document body by a single top-level vertical bar (|):

a=1/2 b=sqrt(3) | [a, b]

becomes:

[1/2, sqrt(3)]

A top-level | is one that is outside quoted strings and outside balanced (), [], and {}. A vertical bar within a quoted string is not a delimiter.

The definition region consists of one or more whitespace-delimited assignments of the form name = replacement-text. Each subsequent top-level name = begins another definition. Definitions may span lines and may contain bracketed structures or quoted strings. Replacement text extends up to the next definition or the | delimiter, with surrounding whitespace removed.

Definition names MUST be unique. An inline name MUST NOT conflict with an externally supplied name. Empty replacement values, text before the first definition, and more than one top-level | are invalid.

If no top-level | is present, the entire text is the document body; only external substitutions are applied.

Expansion order and dependencies

External substitutions are supplied as literal replacement text; the external mapping is not itself processed as a sequence of dependent definitions.

Inline definitions MUST be processed in source order. For each definition:

  1. Replace external names and earlier inline names in a single textual pass.
  2. Evaluate explicit eval(...) expressions in the resulting replacement text.
  3. Store that resolved text for use by later definitions and the document body.

An unquoted reference to the current or a later inline name MUST be rejected, even in an unused definition. This includes such a reference exposed by an external replacement. Quoted occurrences are ordinary String contents, not dependencies. Names not defined anywhere in the document are left as text; the strict parser remains responsible for rejecting unknown constructs.

For example:

a        = 4.593
c_over_a = 0.644
c        = a * c_over_a |
[c]

produces:

[4.593 * 0.644]

No multiplication is performed. To obtain a machine numerical value instead, write c = eval(a * c_over_a). The stored replacement for c is then 2.957892. A later definition such as d = eval(c * 2) uses that numerical result, rather than creating a nested eval(...) call. Explicit evaluation in an unused inline definition is also performed and MUST reject invalid arithmetic.

After all inline definitions have been resolved, external names are replaced once in the body, followed by resolved inline names once. Explicit eval(...) expressions remaining in the body are evaluated in the final stage. Replacement text inserted during one pass is not recursively scanned during that pass. There is no dependency graph resolution or repeated expansion to convergence.

Compact Lists are expanded before definitions are resolved. Consequently, a=1/2 * sqrt(3) row=<a 0> | row produces [1/2 * sqrt(3), 0] without splitting the spaces inside the replacement into additional List items.

Expression boundaries

Substitution is literal text replacement. It does not add parentheses or constructor calls, and it does not guarantee that an inserted infix fragment will form one constituent in the surrounding expression. For example:

a=1 + 2 b=3 * a | b
    -> 3 * 1 + 2

To preserve a Sum as one Product constituent, use explicit constructor syntax:

a=sum(1, 2) b=product(3, a) | b
    -> product(3, sum(1, 2))

Within eval(...), ordinary arithmetic grouping parentheses may be included in the replacement text. Outside eval(...), strict Shorthand parentheses denote Tuples, not grouping. Applications SHOULD use substitutions at syntactic value boundaries rather than relying on token concatenation.

Multiset input

Multiset constructor syntax belongs to strict Shorthand and requires no preprocessing. Compact angle-bracket notation still expands to Lists, not Multisets.

Substitutions may be used within a Multiset constructor without removing repeated occurrences:

a=3 | multiset(a, a, 2)
    -> multiset(3, 3, 2)

The preprocessor does not inspect Multiset membership or equality. The strict parser constructs and validates the resulting Multiset DCI.

Scalar eval(...)

Purpose and placement

After resolving the names in an inline definition, and again after substitution in the document body, the preprocessor locates unquoted eval(...) expressions, allowing whitespace between eval and (. It finds the matching closing parenthesis, including nested parentheses, and replaces the complete expression with a numeric literal.

The evaluator is not a general Python evaluator. It supports only real scalar arithmetic. Arrays, DCIs, symbols, arbitrary functions, attribute access, indexing, comprehensions, Boolean expressions, comparisons, and arbitrary code execution are excluded.

Permitted expression forms

Within eval(...), the following Python-like forms are supported:

  • integer and floating-point numeric literals;
  • parentheses;
  • unary + and -;
  • binary +, -, *, /, and **;
  • sqrt(expression) with exactly one positional argument.

Operator precedence and associativity follow Python expression semantics. In particular, exponentiation binds more tightly than unary minus on its left (-2**2 is -4), and / performs true division (1/2 is 0.5). The caret ^ is not exponentiation and is unsupported.

The sqrt operation has the semantics of Python’s real-valued math.sqrt, including rejection of negative arguments. No names other than the permitted sqrt call are evaluated.

Results

Intermediate arithmetic uses Python-compatible arbitrary-precision integers and IEEE-754 binary64 floating-point behavior. Every intermediate and final value MUST be a real int or float (not Boolean, complex, or another type). Nonfinite floating-point values are invalid.

If the final value is a finite Float with exactly integral value, it is converted to an Integer before rendering. This conversion uses no numerical tolerance. Consequently:

eval(1/2)                       -> 0.5
eval(1/2 + 1/2)                 -> 1
eval(2**10)                     -> 1024
eval(sqrt(9) + 1)               -> 4
eval(-2**2)                     -> -4
eval(5+(1.89-2.53)*6/3.72)      -> 3.967741935483871

The replacement MUST be a valid strict Shorthand Integer or Float literal representing the computed result. A Python-compatible shortest round-tripping decimal representation is RECOMMENDED for finite binary64 results; an implementation MUST preserve the intended binary64 value when subsequently parsed as strict Shorthand.

The evaluator MUST NOT use unrestricted host-language eval on untrusted input. Implementations MAY use an expression parser or a restricted abstract syntax tree evaluator.

Resource limits

Implementations MAY reject expressions that exceed reasonable limits on length, nesting depth, intermediate integer size, or exponentiation magnitude. The limits are resource-protection policies, not extensions to the permitted arithmetic language.

The current Python reference implementation uses a maximum expression length of 1,000 characters, maximum AST recursion depth of 64, and a 4,096-bit intermediate integer limit, with additional exponentiation checks. The thresholds are reference-implementation limits, not required values for other languages.

Portability note: fully identical last-bit results for every floating-point arithmetic expression and sqrt across languages/platforms are not yet guaranteed by this draft. A Version 1 conformance profile should fix the reference numerical semantics and test vectors before claiming universal byte-identical evaluated results.

Error handling and integration with strict Shorthand

A conforming preprocessor MUST reject malformed compact List delimiters, invalid or duplicate substitution definitions, conflicting external/inline names, self-references or forward references between inline definitions, unterminated quoted strings encountered during scanning, malformed eval(...) syntax, and unsupported or invalid evaluated operations.

A preprocessor MAY use implementation-specific exception classes and messages. It does not need to detect ordinary strict Shorthand errors such as unknown DCT identifiers or invalid Fraction denominators. Those errors belong to the strict parser.

For a conforming combined interface, preprocessing followed by strict parsing MUST be equivalent to calling the two operations separately:

parse_shorthand(text, preprocess=True, substitutions=S)
    = parse_shorthand(preprocess_shorthand(text, substitutions=S))

The notation above describes behavior, not mandatory function names in other languages.

Strict parsing without preprocessing MUST remain available and MUST reject preprocessor-only syntax that is not valid strict Shorthand.

Relationship to other DataCanon specifications

  • DataCanon Core Version 1 defines DCIs, equality, canonical encoding, and fingerprinting.
  • DataCanon Shorthand Version 1 defines the strict textual grammar and identity-preserving rendering.
  • This specification defines optional text transformations performed before strict Shorthand parsing.
  • YAML interchange, HDF5 persistence, and other representations are not required to perform preprocessing automatically.

The Python reference implementation provides preprocess_shorthand(...) and the explicit parse_shorthand(..., preprocess=True) option. The Python parser and ordinary .dcs persistence reader remain strict by default. The dc command-line frontend preprocesses .dcs and standard-input Shorthand by default; --strict disables preprocessing. Queries are not preprocessed. Other implementations MAY expose different interfaces while preserving the same transformations and access to strict parsing.

Appendix A — Preprocessing conformance examples

The output strings below illustrate the intended transformations. Where leading/trailing whitespace is not material to strict Shorthand parsing, it is omitted from the examples.

InputPreprocessed output
<1 2 3>[1, 2, 3]
<6>[6]
<1 2; 3 4>[[1, 2], [3, 4]]
<1 2, 3 4>[[1, 2], [3, 4]]
<[1,2] 3>[[1,2], 3]
<eval(1 + 2) 0>[3, 0]
"<1 2>""<1 2>"
a=1/2 * sqrt(3) | <a 0>[1/2 * sqrt(3), 0]
a=1/2 | eval(a + 1)1.5
a=2 b=a * 3 | b2 * 3
a=eval(2 + 3) b=eval(a * 2) | b10
eval((2 + 3)*4)20
eval(sqrt(9) + 1)4

With external substitutions S='"Sr"' and P='[0.5, 0.5, 0.5]':

{"species": S, "position": P}
    -> {"species": "Sr", "position": [0.5, 0.5, 0.5]}

Invalid examples include:

<>                               # empty compact List
<1 2                           # unterminated compact List
a=1 | b=2 | [a]                # multiple top-level delimiters
a=1 a=2 | [a]                  # duplicate inline name
a=a | a                        # self-reference
a=b b=2 | a                    # forward reference
eval(1/0)                       # division by zero
eval(2^3)                       # unsupported operator
eval([1,2])                     # non-scalar expression
eval(sqrt(-1))                  # invalid real square root
eval(__import__("os"))         # arbitrary code is not allowed

Appendix B — Outstanding Version 1 finalization items

The following details should be resolved deliberately rather than inferred from implementation accidents:

  1. Compact List empty-part behavior: whether repeated or trailing separators are rejected, ignored, or permitted as empty items.
  2. Inline definition boundaries: precise treatment of unusual top-level name = sequences within replacement text.
  3. Numerical conformance: a designated reference Python version/semantics for eval(...), including float formatting and sqrt edge cases, plus cross-language numerical test vectors.
  4. Identifier scanning consistency: align the preprocessor’s eval token recognition with the ASCII identifier convention adopted for strict Shorthand and substitution names.

The finalization items do not affect DataCanon Core identity or canonical bytes. They affect only the optional preprocessing language and can be settled independently.

6.4 - YAML Interchange

DataCanon YAML Interchange — Version 1 Draft

Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1; DataCanon Shorthand Version 1
Scope: Interchange between YAML documents and DataCanon Instances (DCIs)
Out of scope: Canonical binary encoding rules, HDF5 persistence, Shorthand preprocessing, and application-specific YAML tags


Introduction

DataCanon YAML Interchange permits ordinary YAML values to represent DataCanon Instances (DCIs), while preserving DataCanon-specific structures through an embedded Shorthand convention called the $ airlock.

The interchange is not a new DataCanon Type system and does not define DCI identity. All DCI validity and equality requirements come from DataCanon Core Version 1. Embedded values use strict DataCanon Shorthand Version 1, without preprocessing.

A conforming writer MUST produce YAML from which a conforming reader reconstructs an equal DCI. YAML documents need not be byte-for-byte identical across implementations. Indentation, quoting, flow versus block style, and other identity-preserving YAML presentation choices are not standardized.

A conforming reader is required to support the portable YAML subset specified here; it is not required to accept every construct permitted by the broader YAML language.

Conformance and terminology

The terms MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.

Let Y(x) denote any conforming YAML rendering of DCI x, and let P_Y denote YAML interchange parsing. The primary conformance requirement is:

P_Y(Y(x)) = x

where equality is DataCanon equality. A conforming implementation MUST preserve, among other things:

  • distinctions between DCTs, such as Integer and Float, or List and Array;
  • unreduced Fraction and unevaluated numerical structure;
  • the sign of floating-point zero;
  • order in ordered collections;
  • member and key identity in unordered collections; and
  • the paths, types, shapes, and digests of external DCIs.

The interchange reader MUST NOT implicitly perform Shorthand substitutions, compact List expansion, or eval(...) processing on $ content.

Portable YAML data model

YAML-native values

The following YAML-native values are mapped to DCIs:

YAML valueDataCanon result
NullNull DCI
BooleanBoolean DCI
IntegerInteger DCI
Floating-point numberFloat DCI
StringString DCI, subject to the $ convention
SequenceList DCI
MappingMapping DCI

YAML sequences MUST be interpreted as Lists, never inferred to be Arrays, regardless of their dimensions or numerical contents.

YAML mappings MUST preserve the order in which their entries are presented when converted to Mapping DCIs. YAML mapping keys and values are converted recursively using the same rules as other YAML values, subject to the portable-subset limitations below.

The interchange defines no native YAML representation for Bytes, Fraction, Sum, Product, Power, Quotient, Complex, Tuple, Set, Multiset, UnorderedMapping, Record, Array, ExternalFile, or ExternalArray. Such DCIs are represented using the $ airlock.

YAML scalar resolution

For the portable subset, implementations MUST agree on whether a scalar is Null, Boolean, Integer, Float, or String before applying the $ convention. The Python reference implementation uses PyYAML’s SafeLoader scalar resolution and SafeDumper emission behavior.

In particular, the following scalar forms have the indicated interpretations:

YAML sourceResulting DCI
nullNull
trueBoolean true
falseBoolean false
42Integer 42
-3Integer -3
1.25Float 1.25
-0.0Float negative zero
"42"String 42
"true"String true
"null"String null

The reference loader also recognizes some additional YAML scalar spellings, including yes, no, on, and off as Booleans. Such spellings are not recommended for portable hand-authored documents. A conforming writer SHOULD use the unambiguous forms in the table and MUST quote strings that would otherwise be resolved as a different YAML scalar type.

The exact PyYAML reference version and the complete boundary of portable input scalar spellings remain Version 1 finalization items. Independent implementations SHOULD use the reference behavior and the conformance vectors rather than silently applying a different YAML schema. In particular, YAML 1.1-style and YAML 1.2-style implicit resolution are not always interchangeable.

Any resolved numeric value MUST satisfy the invariants of its corresponding DataCanon DCT. In particular, nonfinite floating-point values are invalid DataCanon Floats.

YAML features outside the portable subset

The portable subset does not standardize arbitrary YAML tags, application-defined object construction, timestamps, binary tags, merge keys, recursive aliases, or duplicate YAML mapping keys. Implementations MAY reject such input. An implementation’s acceptance of these constructs does not expand the required portable subset.

The Python reference implementation may accept additional YAML forms through yaml.safe_load; this does not make those forms normative DataCanon interchange syntax. Implementations SHOULD reject or clearly document any input whose conversion would silently lose DCI-relevant information.

The $ airlock

Purpose

The embedded DataCanon marker is $. After optional leading whitespace, the marker MUST be followed by at least one whitespace character before the strict Shorthand value. Whitespace is recognized as Unicode whitespace, consistently with removal of leading whitespace. A decoded YAML String beginning with this marker and boundary represents one embedded strict DataCanon Shorthand DCI, unless escaped as defined below. A marker at the end of the String is also reserved, but is invalid as embedded DC because the required DCI is missing.

The marker performs neither substitution nor evaluation. $100, ${HOME}, $HOME, $1/2, and $$ 1/2 are ordinary Strings because the initial $ has no whitespace boundary. A dollar sign elsewhere in a String has no special meaning. Only $ is recognized as the embedded DataCanon marker.

Examples:

fraction: $ 2/4
product: $ 1/2 * sqrt(3)
array: $ array([1.0, 2.0])

The resulting values are respectively a Fraction DCI, a Product DCI, and an Array DCI. They are not YAML Strings in the resulting DataCanon Mapping.

Quoting alone does not escape the marker. The prefix is recognized in the decoded YAML string value, not by searching raw YAML source characters. Consequently, YAML quoting and block-scalar formatting do not alter the airlock semantics.

Embedded Shorthand parsing

For an unescaped airlock String:

  1. Remove leading whitespace before $.
  2. Remove the $ prefix, whose whitespace boundary MUST have been recognized.
  3. Remove leading whitespace from the remainder.
  4. Parse the remainder as one complete strict Shorthand DCI.

The embedded text MUST satisfy the strict Shorthand specification, including complete consumption. An invalid embedded DCI makes the interchange input invalid.

For example, the following YAML scalar can be emitted as a literal block:

|-
  $
  {
    "a": 1,
    "b": 2
  }

Its decoded YAML String begins with $ and contains a Shorthand Mapping. The |- marker is YAML presentation syntax, not part of the embedded Shorthand.

Escaping literal Strings

The prefix $ is reserved at the whitespace boundary defined above, or at the end of the String, including after leading whitespace. To preserve an ordinary DataCanon String that would otherwise be interpreted as embedded Shorthand, a writer MUST add one backslash immediately before the prefix, after any existing backslashes.

More precisely, for a decoded YAML String, divide its beginning into:

  • zero or more leading whitespace characters;
  • zero or more consecutive backslashes; and
  • the remaining text.

If the remaining text begins with $ followed by whitespace, or consists solely of $:

  • zero preceding backslashes means the remainder after $ is embedded Shorthand;
  • one or more preceding backslashes means an ordinary String: remove exactly one of those backslashes and preserve all other characters.

If that boundary condition is not satisfied, the String is unchanged, including all preceding backslashes. In particular, \$100 and \${HOME} are preserved literally.

Thus, at the level of decoded YAML String values (not YAML source quoting):

YAML String valueDataCanon result
$ 1/2Fraction DCI 1/2
\$ 1/2String $ 1/2
\\$ 1/2String \$ 1/2
\$ 1/2String $ 1/2
$100String $100
${HOME}String ${HOME}
\$100String \$100
ordinary textString ordinary text

The writer MUST apply this escaping convention to String DCIs in all positions, including mapping keys. YAML itself may additionally quote or escape backslashes; those YAML-level escapes are decoded before applying the DataCanon rule.

Airlock rendering

To represent a DCI through the airlock, a writer MUST:

  1. Render the DCI as valid strict Shorthand that preserves its identity.
  2. Prefix the resulting text with $ and whitespace separating the prefix from the Shorthand value.
  3. Emit the result as a YAML String scalar.

A writer MAY use inline YAML scalar presentation or YAML literal-block presentation. A writer SHOULD use a block scalar when the embedded Shorthand contains line breaks.

The Python reference implementation uses $ for single-line Shorthand and $\n for multiline Shorthand. Its embedded Shorthand uses explicit standard layout. Callers may select alignment and same-line delimiter padding without changing DCI identity. Other conforming renderers MAY choose any identity-preserving Shorthand layout.

The reference writer emits nonempty native YAML Lists and Mappings in block style and does not wrap single-line scalars to a fixed width. Empty collections retain [] and {}. When alignment is requested, rectangular groups of nonempty, flat Tuple rows within a native YAML List share column widths; ragged rows and mixed List/Tuple rows retain ordinary rendering. Native YAML Lists remain native Lists. Same-line padding applies inside embedded Shorthand, including keys, and does not control YAML indentation. YAML syntax and scalar-resolution rules may still require quoting. Layout, alignment, padding, and quoting are presentation choices, not identity constituents.

An airlock String MUST NOT be interpreted using optional Shorthand preprocessing.

DataCanon-to-YAML rendering

Native primitive DCIs

Null, Boolean, Integer, Float, and String DCIs SHOULD be rendered using their corresponding native YAML scalar forms, subject to identity preservation.

A writer MUST distinguish native Integer and Float values, including integral-valued Floats such as 1.0. It MUST preserve floating-point signed zero. It MUST quote or otherwise mark Strings so that YAML scalar resolution does not change their DCT.

A String DCI additionally undergoes the $ prefix-escaping rule above.

List DCIs

A List DCI MUST be rendered as a YAML sequence whose members are recursively rendered in their semantic order. An empty List is represented as an empty YAML sequence.

A Tuple DCI MUST NOT be rendered as an ordinary YAML sequence, because that would change its DCT. It uses the airlock instead.

Mapping DCIs

A Mapping DCI MAY be rendered as a native YAML mapping only if all of its keys and values can be recursively converted and the converted keys can be represented without loss or collision.

For compatibility with the Python reference implementation, the native-mapping decision is made by constructing the recursively converted key/value pairs, then checking that every converted key is hashable in the reference mapping model and that no two converted keys compare equal as mapping keys.

If any converted key is unhashable, or any two keys collide under the reference mapping’s equality rules, the writer MUST render the entire Mapping DCI through the $ airlock instead of silently dropping or merging entries.

In particular, the following distinct DataCanon keys collide as Python dictionary keys:

  • Boolean true and Integer 1;
  • Integer 1 and Float 1.0;
  • Float 0.0 and Float -0.0.

Such a Mapping MUST use the airlock, even though it is a perfectly valid DataCanon Mapping.

A writer MUST preserve the semantic entry order of an ordered Mapping, whether using native YAML or the airlock. An UnorderedMapping DCI always uses the airlock; it MUST NOT silently become an ordered Mapping DCI.

Bytes DCIs

Bytes DCIs MUST use the $ airlock, for example:

payload: $ bytes("00ff80")

The complete byte sequence is embedded in the document. A native YAML String is not inferred to be Bytes. YAML binary tags remain outside the portable subset; the explicit Shorthand construct provides the portable representation.

Other DCTs

Every DCI not explicitly given a faithful native YAML representation above MUST be rendered through the $ airlock. This includes Bytes, Fraction, Sum, Product, Power, Quotient, Complex, Tuple, Set, Multiset, UnorderedMapping, Record, Array, ExternalFile, and ExternalArray.

The airlock preserves the DCI, not necessarily any referenced external payload. An ExternalFile or ExternalArray airlock value contains its Shorthand path and digest metadata; resource retrieval and verification are governed separately by the DataCanon persistence specification.

YAML-to-DataCanon parsing

A conforming interchange reader MUST:

  1. Parse the YAML document into the portable YAML value model, preserving scalar types and mapping order.
  2. Convert native Null, Boolean, Integer, and Float values to their corresponding DCIs.
  3. Process each YAML String using the $ airlock and escaping rules.
  4. Convert YAML sequences recursively to List DCIs.
  5. Convert YAML mappings recursively to ordered Mapping DCIs.
  6. Validate the resulting DCIs under DataCanon Core Version 1.

The reader MUST NOT infer Arrays from nested YAML sequences. It MUST NOT simplify embedded numerical structures. It MUST NOT apply the conversion rules for conventional programming-language types to coerce distinct DCTs into one another.

A reader MAY report a malformed document as a YAML parsing error, an invalid embedded Shorthand error, or a DataCanon invariant error. Exact error-message wording is not normative.

Identity and interoperability

The YAML interchange is not a canonical textual encoding. Different YAML byte sequences can represent equal DCIs. Conforming implementations MUST agree on the resulting DCI for documents within the portable subset.

In particular, a conforming implementation MUST preserve the DCI across a YAML write–read round trip, including any $ embedded values:

P_Y(Y(x)) = x

No requirement is imposed on matching YAML whitespace, key quoting, block versus flow layout, or exact embedded Shorthand formatting.

A YAML document does not contain canonical DataCanon bytes merely by virtue of representing a DCI. Fingerprints are always computed from the DCI’s canonical binary encoding, not from YAML source text.

Security and resource handling

YAML readers SHOULD use safe data-only parsing and MUST NOT execute arbitrary application objects or code merely because a YAML tag or scalar requests it.

Embedded $ content MUST be processed by strict Shorthand parsing; it MUST NOT invoke the Shorthand preprocessor or arbitrary evaluation.

Implementations MAY enforce resource limits on YAML document size, nesting depth, aliases, and embedded Shorthand size. Such limits are implementation policy and do not change DCI equality.

Reading a YAML document containing ExternalFile or ExternalArray DCIs MUST NOT require the referenced resources to exist or be loaded. Resource resolution, integrity checking, and copying are separate operations.

Conformance examples

The following examples illustrate the portable interchange semantics. The exact YAML presentation produced by a writer may differ.

Native values and nested Lists

name: calculation
energy: -12.5
positions:
  - [0.0, 0.0, 0.0]
  - [0.5, 0.5, 0.5]

The result is a Mapping DCI containing String, Float, and nested List DCIs. The positions are not an Array DCI.

Embedded mathematical structures

fraction: $ 2/4
product: $ 1/2 * sqrt(3)
complex: $ complex(1/2, 1/2 * sqrt(3))

The result contains Fraction, Product, and Complex DCIs, respectively. The Fraction remains unreduced.

Literal reserved prefix

message: "\\$ 2/4"

After YAML string decoding and DataCanon prefix unescaping, message is a String DCI whose value is $ 2/4, not a Fraction DCI.

Mapping-key collision

A Mapping DCI containing both Boolean true and Integer 1 as keys cannot be safely represented as a native YAML mapping under the reference key semantics. A writer may emit the entire Mapping as a block-scalar airlock value:

|-
  $
  {
    true: "boolean",
    1: "integer"
  }

The reader recovers the two distinct keys and their values.

UnorderedMapping

value: $ umapping("a": 1, "b": 2)

The value is an UnorderedMapping DCI, not a native YAML Mapping DCI.

Multiset

A Multiset uses the $ airlock rather than a native YAML sequence:

samples: $ multiset(1, 1, 2)

The value contains three member occurrences, including two equal Integer members. A writer and reader MUST preserve multiplicity and nested structure. Reordering members does not change Multiset identity.

An ordinary YAML sequence such as [1, 1, 2] remains a List DCI, not a Multiset. Multisets may also be represented through the airlock in mapping-key positions, subject to the same native-key collision rules as other DCIs.

External resource

payload: $ external_file("inputs/config.txt", "0000000000000000000000000000000000000000000000000000000000000000")

The value is an ExternalFile DCI with a relative path and 32-byte digest. No file is accessed while parsing this value.

Invalid and nonportable cases

The following MUST be rejected as valid portable interchange, or are explicitly outside its guaranteed portable subset:

  • A $ scalar containing malformed or incomplete strict Shorthand: invalid.
  • An embedded DCI violating a core invariant, such as a zero Fraction denominator: invalid.
  • A native YAML floating-point value that is NaN or infinity: invalid as a DataCanon Float.
  • A native YAML value that cannot be mapped to a DataCanon DCT under the portable subset: outside the portable subset.
  • Arbitrary executable YAML tags or application-object constructors: outside the portable subset and MUST NOT be executed by a conforming safe reader.
  • Duplicate YAML mapping keys or input relying on merge-key resolution: outside the portable subset; behavior is not guaranteed by this draft.

Version 1 finalization items

The following are intentionally left for a final compatibility pass rather than expanded into a separate YAML standard:

  1. Reference YAML scalar resolver: Pin the PyYAML reference version and establish portable scalar-resolution vectors, particularly for legacy Boolean spellings, exponent notation, and timestamp-like Strings.
  2. Mapping-key collisions: Verify cross-language behavior against a compact set of distinct DCI keys that collide in Python’s conventional dictionary model.
  3. YAML parser subset: Decide whether duplicate YAML mapping keys should be explicitly rejected by all conforming readers rather than left outside the portable subset.
  4. Additional interchange vectors: Test YAML produced by Python against an independent implementation, checking resulting DCI canonical bytes rather than YAML text.

The finalization items do not alter the DataCanon Core DCTs or canonical binary encoding.

6.5 - HDF5

DataCanon HDF5 — Version 1 Draft

Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1
Scope: Identity-preserving HDF5 representation of DataCanon Instances (DCIs), including embedded external resources
Out of scope: Canonical byte encoding, Shorthand and YAML syntax, filesystem packaging conventions, and application-specific source-resolution APIs


Introduction

DataCanon HDF5 defines a portable representation of DataCanon Instances (DCIs) in HDF5 files. Its purpose is to permit independent implementations, including Python and Julia, to exchange structured scientific data while preserving DataCanon identity.

This specification defines the logical HDF5 objects, required names, attributes, data types, ordering conventions, and handling of externally referenced resources. It does not define new DataCanon Types or change their equality relations. Those are defined by DataCanon Core Version 1.

A conforming HDF5 writer MUST produce a file from which a conforming reader reconstructs an equal DCI. Files representing equal DCIs need not have identical HDF5 bytes. Compression, chunking, storage order, object addresses, and other physical details MAY differ without changing DCI identity.

This specification describes the interoperable layout used by the Python reference implementation (h5py and NumPy). It is written in terms of HDF5 objects and values, not Python classes.

Conformance

The terms MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.

Let H(x) be a conforming HDF5 representation of DCI x, and P_H its interpretation as a DCI. The primary requirement is:

P_H(H(x)) = x

Equality here is DataCanon equality, including DCT distinctions, structural numerical constituents, signed floating-point zeros, Array types and shapes, ordering of ordered collections, and external-resource metadata.

A reader MUST NOT change a DCI merely to fit the conventions of its implementation language. A reader MAY reject malformed or unsupported HDF5 input; it MUST NOT silently substitute a different DCI.

File layout

A DataCanon HDF5 file contains a required top-level object named root and may contain a top-level group named _external.

/
├── root                 required; representation of one DCI
└── _external            optional; embedded external byte resources
    ├── 0
    │   ├── path
    │   │   ├── 0
    │   │   └── ...
    │   └── data
    └── ...

The HDF5 file root group MAY have a fingerprint attribute as described below.

The root object MUST represent exactly one DCI, which may contain arbitrarily nested DCIs. It may be an HDF5 group or dataset, depending on its DCT.

Except for the structural helper groups explicitly identified in this specification, each HDF5 object representing a DCI MUST have a scalar string attribute named kind, with the exact value specified for its DCT. Structural helper groups (for example, mapping-entry groups and path groups) do not represent DCIs and do not require kind.

Object names and DCT kind strings are case-sensitive.

General storage conventions

Ordered children

Where a group represents an ordered sequence, its children are named by consecutive zero-based decimal indices:

0, 1, 2, ..., N-1

Indices MUST be written without leading zeros, except for 0. Semantic order is numeric index order, not HDF5 group iteration order or lexicographic name order. Empty sequences have no indexed children.

The same convention applies to List and Tuple items, Set and Multiset member occurrences, Mapping and UnorderedMapping entries, Record fields, path components, and Array shape dimensions stored as children.

Strings

A String DCI is represented as a one-dimensional HDF5 dataset of unsigned 8-bit integers containing the exact UTF-8 bytes of the string, with kind = "string".

No terminating NUL is added. Embedded NUL bytes are permitted as part of the UTF-8 encoding of a valid String DCI. An empty String is represented by an empty dataset of shape (0,).

A reader MUST decode the bytes as valid UTF-8 and apply the String invariants from Core Version 1. No Unicode normalization is performed.

This byte-array representation is intentional: it avoids restrictions of HDF5 variable-length string interfaces on embedded NUL characters.

Bytes

A Bytes DCI is represented as a rank-one HDF5 dataset of unsigned 8-bit integers containing the exact byte sequence, with kind = "bytes". An empty Bytes DCI has dataset shape (0,). No text decoding, terminating NUL, digest, or framing metadata is applied to its contents.

A reader MUST reject Bytes objects that are groups, have a null dataspace, have rank other than one, or use a datatype other than unsigned 8-bit integers. Enumeration datatypes, including Boolean enums, are not valid Bytes datasets.

The kind distinguishes Bytes from String and uint8 Array even when their stored element bytes happen to agree. Bytes payloads are stored directly in the DCI hierarchy, not in the external-resource collection.

Arbitrary-precision integers

An Integer DCI is represented as a scalar HDF5 UTF-8 string dataset containing its value in base-ten notation, with kind = "integer".

A conforming writer MUST use ordinary decimal notation: optional leading - for negative values, followed by ASCII digits; no leading +, exponent, separators, or unnecessary leading zeros. Zero is written as 0. The value is not restricted to a fixed machine-integer width.

A reader MUST recover the mathematical integer exactly. This representation is deliberately different from an HDF5 native integer dataset.

Fixed-width numerical values

Float DCIs use scalar HDF5 IEEE-754 binary64 datasets. The exact finite binary64 value, including the sign of zero, MUST be preserved.

For native numerical datasets, HDF5 datatype endianness and storage layout MAY differ, provided the logical values and their required IEEE-754 bit identities are recovered exactly. Nonfinite values are not valid Version 1 DCIs.

Fixed-domain information fields

Prescribed integer and Unicode fields are not DCI objects and carry no required kind attribute. Writers MUST omit kind on those fields. Integer information uses scalar UTF-8 decimal datasets with the same payload rules as Integer; string information uses rank-one uint8 UTF-8 datasets with the same payload rules as String. Readers validate the payload directly rather than recursively interpreting the field as a DCI. Additional nonsemantic attributes may be ignored.

This applies to Fraction numerator/denominator, Power exponent fields, Record and field names, path components (including embedded-resource paths), external Array type names, and external Array shape dimensions. The Power base, Record field values, Complex components, Quotient operands, and collection members remain full DCI representations with required kind attributes.

Required structure

The tables below define the layout produced by a conforming writer. A conforming reader MUST recognize these layouts and MUST reject input that cannot be interpreted as a valid DCI without loss of identity. Additional application-specific HDF5 metadata MAY be present, but MUST NOT change the interpretation of the specified DataCanon objects.

Primitive DCTs

DCTkindHDF5 representation
NullnullGroup, no DCI constituents
BooleanbooleanScalar Boolean dataset (HDF5 enum FALSE=0, TRUE=1)
IntegerintegerScalar UTF-8 decimal-string dataset
FloatfloatScalar IEEE-754 binary64 dataset
StringstringRank-one uint8 dataset containing UTF-8 bytes
BytesbytesRank-one uint8 dataset containing arbitrary bytes
FractionfractionGroup with direct numerator and denominator integer information

The Boolean dataset MUST represent exactly false or true; arbitrary nonzero integers are not a valid substitute for the Boolean DCT. Implementations using the HDF5 Boolean enumeration convention MUST recognize FALSE and TRUE.

Constant

A Constant is a group with kind = "constant" and exactly one child dataset, name. The dataset contains direct String information, stored as rank-one uint8 UTF-8 bytes using the String information rules; it is not a String DCI. Its decoded value MUST be exactly pi or e. Unknown names, missing fields, extra child objects, and incorrect dataset layouts MUST be rejected.

Fraction

A Fraction is a group with kind = "fraction" and two fixed integer fields:

fraction/
├── numerator       Decimal integer information
└── denominator     Decimal integer information

The denominator MUST be positive. Neither constituent is reduced or normalized.

Atomic DCTs

Power

A Power is a group with kind = "power", one base DCI, and two fixed integer fields:

power/
├── base               Scalar Numerical DCI
├── numerator          Positive decimal integer information
└── denominator        Positive decimal integer information

All children MUST be present, including denominator one. A reader MUST verify the base DCT and exponent positivity. Exponent constituents MUST NOT be reduced and nested base structure MUST be preserved.

Quotient

A Quotient is a group with kind = "quotient":

quotient/
├── numerator          Scalar Numerical DCI
└── denominator        Scalar Numerical DCI

Both children MUST be present and have permitted numerical DCTs. Readers MUST preserve their roles and structure without division or simplification. The Core Quotient invariants determine validity; HDF5 introduces no additional mathematical denominator check.

Complex

A Complex is a group with kind = "complex":

complex/
├── real            Scalar Numerical DCI
└── imaginary       Scalar Numerical DCI

The real and imaginary components are themselves complete DCI representations. A Complex DCI is distinct from an Array element of a complex Array type.

Composite collections

List, Tuple, and Set

List, Tuple, and Set DCIs use groups whose kind values are respectively list, tuple, and set. Each contains indexed child DCIs named 0, 1, and so forth.

collection/            kind = "list", "tuple", or "set"
├── 0                  DCI
├── 1                  DCI
└── ...

For List and Tuple, numeric child order is semantic and MUST be preserved. For Set, member order is not semantic; a conforming writer MAY choose any indexed member order, and a reader MUST construct a Set DCI with distinct members according to Core Version 1.

The HDF5 representation of a Set does not need to use canonical byte ordering. Canonical ordering belongs to the canonical binary encoding, not to HDF5 storage.

Sum and Product

Sum and Product are groups with kind = "sum" and kind = "product", respectively. Each operand occurrence is a complete DCI child named by its consecutive zero-based index.

An empty instance has no indexed children. Each duplicate occurrence MUST be stored separately. Child order is not semantic and need not match canonical byte order. Every Sum operand MUST be a Numerical DCI. Every Product operand MUST be a Scalar Numerical DCI, including the recursive restrictions on Sum and OrderedProduct. Array rank and shape are not Sum invariants. Readers MUST preserve nested nodes and MUST NOT deduplicate or simplify them.

OrderedProduct

An OrderedProduct is a group with kind = "ordered_product". Each operand is stored as a complete indexed DCI child named 0, 1, and so forth. The consecutive index order is semantic and MUST be retained. Every operand MUST be a Numerical DCI. Empty, repeated, scalar-only, and nested operands are permitted. No sorting or evaluation is performed. No Array rank or shape compatibility condition is imposed on this representation.

Multiset

A Multiset is a group with kind = "multiset". Each indexed child represents one complete member DCI:

multiset/              kind = "multiset"
├── 0                  DCI
├── 1                  DCI
└── ...

Child names follow the consecutive numeric-index convention. An empty Multiset is a group with no indexed children. Each repeated occurrence MUST have its own indexed child; readers MUST NOT deduplicate equal members.

Numeric child order does not participate in Multiset identity. A conforming writer MAY choose any member order, and canonical byte ordering is not required for HDF5 storage. Nested Multisets MUST retain their DCT and structure.

External DCIs within Multiset members MUST be discovered for payload collection and source-context attachment, including recursively nested members. Repeated references to one relative path preserve all member occurrences while sharing one embedded payload under the existing path-consistency rules.

Mapping and UnorderedMapping

Mapping and UnorderedMapping DCIs use groups with kind = "mapping" and kind = "unordered_mapping", respectively. Each indexed child is a structural entry group containing two complete DCIs:

mapping/               kind = "mapping" or "unordered_mapping"
├── 0/
│   ├── key            DCI
│   └── value          DCI
├── 1/
│   ├── key            DCI
│   └── value          DCI
└── ...

For Mapping, numeric entry order is semantic. For UnorderedMapping, entry order is not semantic. Keys MUST be distinct according to DataCanon equality, which need not coincide with the equality rules for keys of a conventional dictionary type in the implementation language.

Entry groups do not require a kind attribute.

Record

A Record is a group with kind = "record":

record/
├── name               UTF-8 byte information
└── fields/
    ├── 0/
    │   ├── name       UTF-8 byte information
    │   └── value      DCI
    ├── 1/
    │   ├── name       UTF-8 byte information
    │   └── value      DCI
    └── ...

The Record name is semantic. Fields are ordered numerically; their names MUST be unique according to String equality. The fields group and its indexed entry groups are structural helpers without required kind attributes.

Array

An Array DCI is represented as a native HDF5 numerical dataset with kind = "array". Its HDF5 datatype, dataspace shape, and element values together determine the DataCanon Array type, shape, and elements.

A conforming reader and writer MUST support the 14 portable Array types of Core Version 1:

DataCanon Array typeHDF5 logical representation
boolBoolean enum with FALSE=0, TRUE=1
int8, int16, int32, int64Signed integers of corresponding widths
uint8, uint16, uint32, uint64Unsigned integers of corresponding widths
float16, float32, float64IEEE-754 binary16, binary32, binary64
complex64Compound datatype with fields r and i, each IEEE-754 binary32
complex128Compound datatype with fields r and i, each IEEE-754 binary64

For the complex types, the compound fields r and i represent real and imaginary components respectively, in that order. The field names and component widths match the Python h5py reference representation and are part of the portable interchange layout. A reader MUST reconstruct the corresponding DataCanon complex Array type, not a Record or a structured Array of unrelated fields.

The Array rank and shape are taken from the HDF5 dataspace. Rank-zero Arrays use scalar dataspaces. Zero-sized dimensions are permitted and MUST be preserved. A rank-zero Array is not the same DCI as a rank-one Array containing one element.

The element ordering is the logical ordering of the HDF5 dataspace, consistent with the Core Version 1 Array definition. An implementation MUST preserve the exact logical numerical elements, including signed floating-point zeros and the real and imaginary components of complex elements. HDF5 chunking, compression, and physical data ordering do not participate in DCI identity.

A conforming writer MUST NOT store a portable Array as a nested collection of DCI groups merely because that is easier for its implementation. The native numerical dataset is a defining feature of this representation.

External DCIs

General principle

An ExternalFile or ExternalArray remains an external DCT even when its bytes are embedded in an HDF5 file. Embedding payloads MUST NOT transform ExternalArray into Array or ExternalFile into String or Array.

The semantic constituents are represented in the root DCI tree. The external payload bytes are stored separately in the top-level _external group.

Relative path representation

A DataCanon relative path is represented as a structural group named path, containing indexed UTF-8 byte datasets for its path components:

path/
├── 0                 UTF-8 byte information
├── 1                 UTF-8 byte information
└── ...

A path MUST have at least one component and MUST satisfy the Core Version 1 relative-path invariants. The path group itself has no kind attribute. The path is not stored as a host-operating-system path string.

Digest representation

A SHA-256 content digest is stored in a dataset named digest containing exactly 32 unsigned 8-bit integers, in digest byte order. The digest is not stored as hexadecimal text.

ExternalFile

An ExternalFile is a group with kind = "external_file":

external_file/
├── path/              Relative-path component group
└── digest             uint8 dataset, shape (32,)

The content digest is the SHA-256 digest of the exact external file bytes.

ExternalArray

An ExternalArray is a group with kind = "external_array":

external_array/
├── path/              Relative-path component group
├── array_type         UTF-8 byte information
├── shape/             Indexed decimal integer datasets
└── digest             uint8 dataset, shape (32,)

The array_type UTF-8 byte dataset contains one of the 14 portable Array type names. The shape group contains indexed decimal integer datasets, one per dimension; an empty group denotes rank zero.

The content digest is the SHA-256 digest of the canonical Array element byte stream, not the complete canonical encoding of an Array DCI. The logical element bytes contain no dtype, shape, header, or other framing information. Directory gzip envelopes are not embedded as the logical payload.

Embedded external payloads

Structure

When external resources are present, a conforming HDF5 writer embeds their verified bytes under the top-level group _external:

_external/
├── 0/
│   ├── path/           Indexed UTF-8 byte datasets
│   └── data            Rank-one uint8 dataset
├── 1/
│   ├── path/
│   └── data
└── ...

Each indexed _external child is a structural group, not a DCI. Its path group uses the same relative-path component encoding as above. Its data dataset contains the logical resource bytes as a rank-one uint8 dataset, including empty payloads when applicable. ExternalFiles contribute their opaque bytes; ExternalArrays contribute their verified, uncompressed canonical element streams.

The numeric index of an embedded payload is not semantic. A reader locates an embedded resource by matching the full sequence of relative-path components, not by assuming that a particular DCI points to a particular numeric index.

ExternalArray compression

The reference writer applies HDF5 gzip compression (level 6, chunked storage) to embedded ExternalArray payload datasets whose logical path ends in .bin.gz. It does not store a gzip-file envelope in the dataset. Dataset reads return the canonical element bytes directly; the content digest covers those bytes. Scalar Arrays and empty Arrays still use rank-one byte payload datasets, so the same storage rule applies. .bin ExternalArray payloads and native Array DCIs remain uncompressed by default.

Compression filters and physical layout are nonsemantic. Readers reconstruct the logical bytes through HDF5 and validate them normally, regardless of the physical filters in a valid input dataset. Readers MUST NOT perform another gzip decompression based on the logical resource path. Directory export uses the path’s .bin/.bin.gz suffix to recreate the required storage representation.

Uniqueness and consistency

A conforming writer MUST embed each distinct referenced relative path at most once. If multiple external DCIs refer to the same relative path, their digests MUST agree and the referenced bytes MUST be consistent with each DCI’s requirements.

If two references use the same path but require conflicting bytes or digests, a conforming writer MUST reject the attempted file creation rather than silently select one payload.

An embedded ExternalArray logical payload MUST satisfy its declared Array type and shape, including element-byte length, canonical Boolean bytes, and finite floating-point/complex elements. A compressed ExternalArray and an opaque ExternalFile MUST NOT share a relative path; their directory storage requirements conflict even if their digests agree.

Writing and verification

Before embedding external bytes, a writer MUST verify that their SHA-256 digest equals the digest stored in the corresponding DCI. It MUST NOT embed unverified bytes as though they satisfy the reference.

The Python reference writer collects and verifies all referenced external payloads before creating or overwriting the destination HDF5 file. This preflight behavior is recommended because it prevents missing or conflicting resources from destroying an existing file. Other implementations MAY use different transactional mechanisms providing equivalent integrity safeguards.

A writer may obtain bytes from a verified in-memory cache, an existing filesystem resource, or another supported representation containing the resource. The choice of source does not change the resulting DCI.

Reading and deferred access

Reading the HDF5 DCI hierarchy MUST reconstruct ExternalFile and ExternalArray DCIs from their semantic metadata without automatically materializing their external payload bytes.

A conforming reader SHOULD retain sufficient nonsemantic source context to resolve embedded resources on later request. Such context does not participate in DCI equality or canonical encoding.

When an implementation does retrieve an embedded payload, it MUST verify its digest against the DCI before accepting the bytes. For ExternalArray it MUST additionally validate the declared Array type, shape, byte length, and element validity.

A missing or corrupt embedded payload does not change the identity of the metadata DCI; it prevents successful retrieval or verification of the referenced content.

The general semantics of resolving external resources across different storage representations belong to the separate DataCanon Persistence specification.

Optional fingerprint attribute

A writer MAY store a scalar file-root attribute named fingerprint. Its value is the 64-character lowercase hexadecimal textual representation of the SHA-256 fingerprint of the DCI stored at /root, as defined by Core Version 1.

The attribute is metadata, not an additional constituent of the DCI. Its presence or absence does not affect identity.

The Python reference reader does not currently use this attribute to validate the file. A conforming reader MAY verify it and MUST NOT treat a mismatching fingerprint as evidence that two unequal DCIs are equal.

Reading, writing, and validity

A conforming writer MUST produce the DCI layouts and semantic types specified above. A conforming reader MUST support every required Version 1 layout and reconstruct the same DCI identity.

Readers MUST reject absent /root, unknown kind values, missing required constituents, invalid constituent DCTs, invalid relative paths, and numerical or structural data that cannot represent a valid DCI. They MUST NOT silently repair or simplify invalid DCIs.

A reader MAY ignore additional nonsemantic attributes or groups, provided they do not create ambiguity about the required DCI structure. The strictness of rejecting all extraneous structural objects is not yet fully specified; conforming writers SHOULD emit only the defined structural children.

External payload verification is distinct from decoding the DCI hierarchy. A reader need not access embedded payload bytes merely to construct a metadata-only external DCI.

Illustrative HDF5 trees

Scientific Mapping

/
└── root/                        kind = "mapping"
    ├── 0/
    │   ├── key                  kind = "string", UTF-8 bytes "lattice"
    │   └── value                kind = "array", shape (3, 3), float64
    └── 1/
        ├── key                  kind = "string", UTF-8 bytes "scale"
        └── value/               kind = "fraction"
            ├── numerator        decimal integer text "1"
            └── denominator      decimal integer text "2"

ExternalArray

/
├── root/                        kind = "external_array"
│   ├── path/
│   │   ├── 0                   UTF-8 bytes "external_dc"
│   │   └── 1                   UTF-8 bytes "forces.bin"
│   ├── array_type              UTF-8 bytes "float64"
│   ├── shape/
│   │   ├── 0                   decimal integer text "100"
│   │   └── 1                   decimal integer text "3"
│   └── digest                  uint8[32]
└── _external/
    └── 0/
        ├── path/
        │   ├── 0               UTF-8 bytes "external_dc"
        │   └── 1               UTF-8 bytes "forces.bin"
        └── data                uint8[2400]

The example assumes float64 elements (8 bytes each), shape (100, 3), and therefore 2,400 canonical element bytes.

Cross-implementation conformance

Implementations SHOULD test at least the following cases:

  1. All eight primitive DCTs, including both named Constants, unreduced Fractions, empty and arbitrary Bytes sequences, arbitrary-precision Integers, Unicode Strings, embedded NULs, and positive versus negative Float zero.
  2. All three atomic DCTs: Power with a Scalar Numerical base and positive integer exponent fields, Quotient with Scalar Numerical operands, and Complex with components of different DCTs.
  3. All collection DCTs, including empty collections, ordered versus unordered semantics, Multiset and numerical Sum/Product multiplicities and nesting, and keys that would collide under ordinary programming-language equality. OrderedProduct tests MUST distinguish reversed operand order and preserve Array operands without rank checks.
  4. Record names and field names containing Unicode and spaces; preservation of field order.
  5. All 14 portable Array types; scalar Arrays; empty dimensions; signed zeros; complex components; native HDF5 dataset interoperability.
  6. ExternalFile and ExternalArray metadata round trips without eager payload loading.
  7. Embedded payload retrieval, digest verification, shared paths, and rejection of conflicting paths.
  8. Optional fingerprint metadata, without making it part of identity.
  9. Rejection of malformed files, including missing /root and unknown kind attributes.

Cross-language test fixtures SHOULD include files written independently by Python and Julia. HDF5 file bytes need not match; reconstructed DCIs and their canonical encodings MUST match.

Version 1 finalization notes

The following points warrant focused interoperability testing before declaring this document final; they are not reasons to redesign the representation:

  • Native HDF5 datatypes: confirm that Julia’s HDF5 implementation reads and writes the h5py Boolean enum and r/i complex compound layouts with the intended semantics, especially across endianness and floating-point widths.
  • Reader strictness: the Python reference reader currently relies on HDF5 conversion and DCI constructors for some validation and does not explicitly reject every possible malformed HDF5 datatype or extra child. The normative reader requirements may therefore require additional validation tests or a narrower portable-input statement.
  • Embedded payload lookup: confirm that source-aware loading resolves the /_external entry by complete relative path, independently of enumeration order, and verifies digest and Array element validity on retrieval.
  • Version metadata: the current layout contains no mandatory HDF5 format-version attribute. If one is introduced later, it should be nonsemantic and backward-compatible with existing Version 1 files.
  • Exact string datatype details: the Integer DCI uses an HDF5 scalar UTF-8 variable-length string, while String DCI uses a uint8 byte dataset. Independent implementations must not confuse the two.

The finalization items should be settled by conformance tests and targeted reference-implementation changes, rather than by altering the DataCanon core identity model.

6.6 - Persistence

DataCanon Persistence — Version 1 Draft

Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1; DataCanon Shorthand Version 1; DataCanon HDF5 Version 1
Related: DataCanon YAML Interchange Version 1; DataCanon Shorthand Preprocessing Version 1
Scope: Storage-format selection, reading and writing, external-resource namespaces, deferred retrieval, integrity verification, and identity-preserving transfer
Out of scope: DCT definitions, canonical byte encoding, Shorthand grammar, HDF5 object layout, YAML scalar semantics, and implementation-language APIs


Introduction

DataCanon Persistence defines how DataCanon Instances (DCIs) are read, written, and transferred among supported storage representations while preserving their identity. It also defines how the relative paths belonging to ExternalFile and ExternalArray DCIs are interpreted within different resource namespaces.

DataCanon Core Version 1 alone defines DCI equality, canonical encoding, and fingerprints. Persistence MUST NOT redefine or alter those properties.

A DCI may be represented in memory, as canonical binary bytes, as Shorthand text, or within HDF5. External resources may be stored as separate filesystem files or embedded in an HDF5 container. The source from which an implementation can retrieve such resources is not part of the DCI.

This specification concerns the behavior of conforming implementations, not their choice of internal classes, APIs, or filesystem utilities. The Python reference implementation provides operations commonly named read and write, but those names are not normative.

Conformance and terminology

The terms MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.

A representation is a stored form from which a DCI can be reconstructed. A resource namespace is the context in which an external DCI’s DataCanon relative path is resolved. A source is an implementation-level handle or description of such a namespace. A payload is the byte sequence referred to by an external DCI.

A metadata read reconstructs a DCI without necessarily retrieving external payloads. Materialization retrieves and validates a referenced payload. A complete package consists of a DCI representation together with all external payloads needed to satisfy its external references.

For every supported format, a conforming writer and reader MUST satisfy the identity-preserving round trip:

read(write(x)) = x

where equality is DataCanon equality. The equation assumes that the write succeeds and the stored representation is subsequently available and intact. It does not imply that a metadata read materializes external resources.

Distinct representations of equal DCIs need not have equal file bytes. In particular, Shorthand formatting and HDF5 physical layout are not canonical encodings.

Supported representations and filenames

Version 1 defines the following conventional file suffixes:

SuffixRepresentationGoverning specification
.dcsStrict DataCanon Shorthand textShorthand Version 1
.dcbRaw canonical DCI byte encodingCore Version 1
.h5DataCanon HDF5 containerHDF5 Version 1

A .dcs file MUST contain exactly one valid strict Shorthand DCI expression, encoded as UTF-8 text. The file MAY contain permitted Shorthand whitespace. The file MUST NOT require substitutions, compact List expansion, or eval(...) to be interpreted.

A .dcb file MUST contain exactly one complete canonical Version 1 DCI encoding. It MUST NOT prepend a magic header, length prefix, or version marker to those bytes. Its bytes are precisely the canonical encoding defined by Core Version 1.

A .h5 file MUST follow the HDF5 Version 1 layout, including its required /root object and the rules for embedded external payloads.

The suffixes above are conventional format-selection signals, not semantic DCI constituents. An implementation MAY provide explicit format selection, and MAY recognize additional suffix aliases, but MUST interpret a selected format according to its governing specification.

YAML interchange is separately specified and is not a required format for the generic Version 1 persistence dispatch. An implementation MAY provide explicit YAML import and export operations. It MUST NOT silently treat arbitrary YAML as strict Shorthand or vice versa.

Reading a DCI

Identity-preserving metadata reads

A conforming read operation MUST reconstruct the DCI described by the selected representation and MUST validate its semantic constituents according to Core Version 1.

In particular, a reader MUST preserve:

  • the DCT of every constituent, including distinctions between Integer, Float, Fraction, Sum, Product, Power, Quotient, List, Tuple, and Array;
  • ordered collection and Record field order;
  • unordered collection membership and key/value identity;
  • Multiset member multiplicities and nested structure;
  • unreduced Fraction and Power exponent structure, and unevaluated Sum, Product, and Quotient constituents;
  • floating-point bit identity, including signed zero;
  • Array type, shape, and portable element values; and
  • ExternalFile and ExternalArray relative paths, metadata, and SHA-256 digests.

A reader MUST NOT change an ExternalArray into an Array, or an ExternalFile into another DCT, merely because the referenced bytes are available.

Contained byte payloads

Bytes DCIs contain their entire byte sequences. Writers MUST preserve those bytes directly in the selected representation: raw length-delimited bytes in .dcb, hexadecimal Shorthand in .dcs, or kind = "bytes" datasets in .h5. No external-resource namespace or companion file is required for Bytes.

Reading a Bytes DCI reconstructs its byte sequence. Applications may attach interpretation metadata using a containing Record or another suitable DCT; persistence does not interpret or execute byte contents.

External references are not eagerly loaded

Reading a DCI MUST NOT require external resources to exist, be opened, or be verified. An ExternalFile or ExternalArray DCI is valid independently of the availability of its referenced payload.

Thus, a metadata read of a .dcs, .dcb, or .h5 representation containing external DCIs MUST be possible without first materializing those external resources, provided the DCI representation itself is valid.

For HDF5, reading the DCI hierarchy does not require reading the /_external/*/data payload datasets. An implementation MAY read nonsemantic metadata needed to establish future source context.

A reader MAY expose a separate operation that verifies or materializes external payloads. Such an operation is not part of ordinary metadata reading.

Source context

A reader SHOULD retain sufficient source context to resolve external resources later without requiring the application to resupply the location of the representation. This context MAY be associated with the returned DCI tree, with its external nodes, or with a separate source handle.

Source context is nonsemantic. It MUST NOT participate in DCI equality, canonical encoding, or fingerprinting. Attaching, replacing, or discarding source context MUST NOT change the DCI.

The source context may identify a directory containing external files, an HDF5 file containing embedded payloads, or another implementation-supported resource namespace.

The representation’s source context does not modify the relative path stored in any external DCI.

External resource namespaces

Abstract relative paths

ExternalFile and ExternalArray DCIs contain DataCanon relative paths, defined by Core Version 1 as nonempty ordered sequences of path components. A path is not a host operating-system pathname and does not include an absolute location or a source identifier.

Two equal external DCIs retain the same identity even when resolved against different source namespaces. The choice of namespace determines which bytes may be found; it does not determine DCI equality.

A resource is accepted as satisfying an external DCI only after the required digest and, for ExternalArray, element-stream checks succeed.

Directory-backed representations

For a .dcs or .dcb representation stored in a filesystem, the default resource namespace is the directory containing that representation file.

A DataCanon relative path with components ("inputs", "config.txt") is resolved by joining those components beneath the representation’s containing directory, using the host filesystem’s path conventions.

For example:

runs/run42/
├── calculation.dcs
└── inputs/
    └── config.txt

An ExternalFile whose DataCanon relative path is ("inputs", "config.txt") refers by default to runs/run42/inputs/config.txt when calculation.dcs is the source representation.

The same convention applies to a .dcb file in place of calculation.dcs. No particular name for the representation file or containing directory is required.

An implementation MAY accept an explicit directory source to override the default namespace. Such an override MUST NOT change the DCI’s stored relative path or its identity.

A conforming implementation MUST NOT interpret DataCanon relative path components . or .., absolute paths, or embedded separators as permissible alternate spellings. The Core Version 1 relative-path validation rules apply before resolution.

Host filesystem behavior, including permissions and symbolic links, is implementation-specific. Implementations SHOULD avoid unintended resolution outside an explicitly trusted resource namespace, particularly when handling untrusted input.

HDF5-backed representations

For a DCI read from a DataCanon HDF5 file, the default external resource namespace is that HDF5 container’s embedded /_external collection.

The reader resolves a DataCanon relative path by matching its complete ordered component sequence against the path stored with an embedded resource entry. The numeric name of the /_external entry is not semantic and MUST NOT be used as the resource identifier.

The HDF5 Version 1 specification defines the precise layout, uniqueness rules, and payload datasets. Persistence MUST NOT reinterpret those structures.

A source-aware implementation MAY support an explicitly supplied alternative resource namespace, but MUST NOT silently substitute a different payload for a missing or invalid embedded resource.

No source available

A DCI constructed directly in memory, or decoded from canonical bytes without an associated representation location, may have no source context.

Such a DCI remains valid. Materializing an uncached external resource requires an appropriate source to be supplied or otherwise made available. Failure to locate a resource MUST NOT mutate the DCI or change its fingerprint.

External payload materialization and verification

General requirements

Before returning or otherwise accepting retrieved external bytes as the content identified by an ExternalFile or ExternalArray DCI, an implementation MUST compute SHA-256 over the retrieved bytes and compare the resulting 32-byte digest to the DCI’s declared digest.

For ExternalArray, retrieved payload bytes mean the logical canonical element stream after any storage decompression. For ExternalFile, they mean the exact opaque stored bytes.

If the digest differs, the operation MUST fail. It MUST NOT return the mismatching bytes as verified content.

A missing, inaccessible, truncated, or corrupt resource MUST cause the attempted materialization to fail. Materialization failures do not invalidate the metadata DCI.

The cryptographic limitations of SHA-256 are those described in Core Version 1: digest equality is a practical integrity check based on collision resistance, not a proof of byte-sequence identity.

ExternalFile

An ExternalFile payload is an opaque byte sequence. Materialization MUST preserve its exact bytes. DataCanon Persistence does not mandate any text decoding, YAML parsing, or application-specific interpretation of those bytes.

An implementation MAY provide extension-based or user-supplied content loaders as a convenience. Such interpretation occurs after digest verification and is not part of DCI identity or portable persistence semantics.

ExternalArray

An ExternalArray payload is the canonical Array element byte stream, not the complete canonical encoding of an Array DCI. It contains no Array type, shape, or other framing metadata.

After any storage decompression, an implementation MUST validate the digest and the payload against the external DCI’s portable Array type and shape, including:

  • the exact required byte length;
  • canonical Boolean element bytes (00 and 01 only);
  • finite floating-point and complex elements; and
  • the element ordering and representation prescribed by Core Version 1.

A materialized Array value MAY be provided as an instance of a conventional numerical container type, such as a NumPy array in Python. This does not change the original ExternalArray DCI into an Array DCI.

ExternalArray storage compression

ExternalArray logical payloads are canonical Array element streams. Directory storage MUST use a relative path whose final component ends in .bin for an uncompressed stream or .bin.gz for a gzip-compressed stream. Suffixes are case-sensitive. Other suffixes are not supported by this directory storage profile. Core relative-path validity is unchanged: metadata decoding does not need to retrieve a resource or apply this storage profile.

The digest ALWAYS covers the uncompressed canonical element bytes. A reader MUST decompress .bin.gz resources before accepting their digest and Array contents. It MUST enforce the exact logical byte length determined by the Array type and shape; it SHOULD reject excess decompressed output while reading, rather than allocating an unbounded decompressed buffer. Missing resources, malformed or truncated gzip, digest mismatch, invalid Boolean bytes, nonfinite elements, and incorrect logical lengths cause materialization to fail.

Compressed and uncompressed paths identify distinct DCIs even if their logical payloads agree. A transfer MUST preserve the stored path and digest. In particular, HDF5-to-directory export MUST recreate a gzip file when the path ends in .bin.gz. A directory-to-HDF5 transfer embeds the verified logical stream, rather than a gzip envelope. HDF5 dataset filters handle storage compression transparently; readers MUST NOT decompress the returned logical bytes a second time because a resource path ends in .gz.

ExternalFile payloads remain opaque. A .gz ExternalFile is verified over its stored bytes and is not implicitly decompressed. One path MUST NOT be shared by an opaque ExternalFile and a compressed ExternalArray, even when their declared digests agree, because their directory storage requirements conflict. Uncompressed ExternalArrays may share paths with ExternalFiles when all byte and validity requirements agree. Writers MUST reject conflicting references before producing a complete package.

The Python conventional constructor accepts compression=False or True. A suffixless filename receives .bin or .bin.gz; an omitted filename is the logical digest in hexadecimal plus the selected suffix. Explicit suffixes must agree with the selected compression setting. The constructor finalizes the path before DCI construction. There is no additional semantic compression field. The reference writer uses gzip level 6 for directory streams and embedded HDF5 ExternalArray datasets selected by .bin.gz. Compression is off by default. Native Array datasets are unaffected by this option.

Cached data

An implementation MAY retain external payloads in memory, including data supplied when an external DCI is constructed. Such caches are nonsemantic.

A cache MUST NOT be trusted merely because it is associated with a DCI. Cached bytes MUST satisfy the same digest and, where applicable, ExternalArray validation requirements before they are returned as verified content or persisted as a valid external resource.

An implementation MAY reuse the result of a prior successful verification when it can ensure that the verified bytes have not changed.

Writing and packaging

Representation writing

A conforming writer MUST emit the selected representation of the DCI according to its governing format specification. It MUST NOT change semantic constituents, simplify mathematical structures, or replace external DCTs with their materialized values.

A writer MAY accept an instance of a conventional type and first convert it to a DCI under implementation-defined conversion rules. That conversion is outside this specification; the persistence requirements apply to the resulting DCI.

Filesystem-backed complete packages

When asked to produce a complete .dcs or .dcb package, a writer MUST make each referenced external payload available beneath the destination representation’s containing directory at the DataCanon relative path specified by its DCI.

The writer MUST verify each payload before accepting it for output. It MUST preserve the relative path, digest, Array type, and shape constituents of every external DCI.

A writer MAY obtain the payload from a verified cache, an existing filesystem source, or a supported container source. Copying the bytes to a new namespace MUST NOT change the DCI.

A writer MUST NOT silently omit required external payloads while claiming to have produced a complete package.

Writing only a .dcs or .dcb DCI representation without copying its external payloads MAY be supported as a metadata-only export. Such an export can be a valid representation of the DCI, but it is not necessarily a complete package. An implementation SHOULD make this distinction clear to its caller.

HDF5 complete packages

A conforming HDF5 writer MUST follow the HDF5 Version 1 rules for collecting, verifying, and embedding referenced external bytes in /_external. In particular, it MUST reject incompatible references to the same relative path.

A complete HDF5 package contains the DCI hierarchy and all required embedded external payloads. The semantic external DCI paths and digests remain unchanged by embedding.

A reader MAY reconstruct the DCI without accessing those embedded bytes. Completeness of the package and eagerness of the reader are separate concerns.

Multiset traversal

External-resource discovery MUST include every member occurrence of a Multiset, recursively through nested constituent structures. Retained source context MUST also reach external DCIs inside Multisets.

Persistence MUST preserve member multiplicity and nesting. Multiple occurrences of the same external reference do not require duplicate payload files or embedded payload entries: one resource location satisfies all consistent references to its relative path. This resource sharing MUST NOT remove or merge member occurrences in the represented DCI.

Repeated paths and conflicting references

Within one complete package, two external DCIs with the same relative path refer to one resource location. A conforming writer MUST ensure that all references to that path are consistent with one shared byte sequence.

In particular, if their declared digests differ, the writer MUST reject complete-package creation. Even when digests agree, every ExternalArray reference to that path MUST be compatible with the shared payload’s type, shape, and element-stream validity.

Identical payload bytes referenced by different relative paths remain different resource locations. A writer MUST preserve the paths; it MUST NOT silently merge their semantic references into one path.

Overwriting and failure safety

Writers SHOULD verify all required source payloads and detect conflicting references before overwriting a destination representation or external resource. A writer SHOULD use temporary files, atomic replacement where supported, or equivalent safeguards to avoid leaving partially written packages.

The exact transaction mechanism is implementation-specific. A failed write MUST NOT be reported as a successful complete-package write.

A writer SHOULD avoid destructive overwrites of unrelated files in the destination resource namespace. Applications are responsible for choosing appropriate destination directories and overwrite policies.

Identity-preserving transfers

A transfer between supported representations consists conceptually of:

  1. reconstructing a DCI from the source representation;
  2. retaining or supplying a source namespace for any external resources;
  3. verifying required payloads when preparing a complete destination package; and
  4. writing an identity-preserving destination representation and its required resources.

For example:

runs/run42/calculation.dcs
              |
              v
      DCI + source context
              |
              v
archive/calculation.h5

If the source DCI contains an ExternalArray, the destination HDF5 file embeds its verified element bytes but retains the ExternalArray DCT, relative path, Array type, shape, and digest.

The reverse transfer from HDF5 to .dcs or .dcb materializes verified embedded resources into files under the destination directory when a complete package is requested. Their relative paths are preserved.

For equal source and destination DCIs x and y:

E(x) = E(y)
F(x) = F(y)

where E and F are the canonical encoding and fingerprint defined by Core Version 1. The surrounding file bytes need not match across formats.

A transfer MUST NOT silently resolve conflicts by changing paths, digests, or DCTs. If an identity-preserving complete transfer cannot be performed, it MUST fail or explicitly produce a metadata-only export when the caller has selected that behavior.

Fingerprints and integrity metadata

A persistence format MAY carry a DCI fingerprint as nonsemantic metadata. The fingerprint is always SHA-256 of the DCI’s canonical encoding, not of the bytes of the .dcs or .h5 file.

The HDF5 Version 1 specification defines its optional file-root fingerprint attribute. This Persistence specification does not introduce an additional required fingerprint field for .dcs or .dcb.

A reader MAY check a stored fingerprint. It MUST NOT treat the fingerprint as an additional DCI constituent or use it to replace the DCT-defined equality relation.

The fingerprint of an ExternalFile or ExternalArray DCI covers its path and declared content digest, not the external payload bytes directly. External payload verification remains a separate operation.

Errors and resource limits

A conforming reader MUST reject invalid representation syntax, invalid canonical bytes, or invalid HDF5 DCI structure according to the governing format specification. It MUST NOT silently coerce malformed data into a different DCI.

An implementation MAY enforce practical limits on input size, nesting depth, Array allocation, number of external references, or payload size. Such limits do not redefine DataCanon identity.

External content MUST be treated as untrusted until verified. A verified digest does not make an external file’s application-level contents safe to execute or interpret. DataCanon Persistence does not require execution of external content or of any code contained in Shorthand or YAML input.

Error types, diagnostic wording, caching strategy, concurrency controls, and source-handle representation are implementation details.

Interoperability examples

Directory-backed Shorthand

experiment/
├── structure.dcs
└── data/
    └── forces.bin

structure.dcs contains an ExternalArray with relative path ("data", "forces.bin"), a portable Array type and shape, and the SHA-256 digest of forces.bin’s canonical element bytes.

Reading structure.dcs reconstructs the ExternalArray metadata without reading forces.bin. Materialization resolves the relative path under experiment/, verifies its digest and element validity, and produces the numerical data.

Raw canonical bytes

A .dcb file containing the canonical encoding of ExternalFileDC is valid even if the external file is absent. Decoding recovers the relative path and digest. The missing payload prevents materialization, not decoding or fingerprinting.

HDF5 embedded resources

experiment.h5
  /root                  DCI hierarchy
  /_external/0/path      Relative-path components
  /_external/0/data      Verified payload bytes

The HDF5 reader recovers external DCIs from /root without eagerly loading /_external/0/data. When materialization is requested, the resource is located by its stored path components and its digest is verified.

Cross-format transfer

experiment/structure.dcs
             |
             v
        experiment.h5
             |
             v
archive/structure.dcb
archive/data/forces.bin

A complete transfer preserves the DCI’s canonical encoding and fingerprint, despite differences in the storage representation and external-payload location.

Cross-implementation conformance

Independent implementations, including Python and Julia, SHOULD test:

  1. Round trips of all 24 DCTs through .dcs, .dcb, and .h5 where supported by the corresponding representation specification.
  2. Preservation of DCI identity and canonical fingerprints across format changes.
  3. Metadata-only reads of external DCIs when referenced payloads are missing.
  4. Directory-relative resolution for .dcs and .dcb sources.
  5. HDF5 path-based lookup of embedded payloads, independent of entry enumeration order.
  6. ExternalFile digest verification and ExternalArray digest, length, and element validation.
  7. Cached-data verification and explicit source overrides.
  8. Complete transfer between directory-backed and HDF5-backed representations.
  9. Detection of shared-path conflicts and rejection of incomplete complete-package writes.
  10. Distinction between valid metadata-only exports and complete packages.
  11. Multiset multiplicity and nesting across all formats, including deferred retrieval, shared resources, and conflicting external references within Multiset members.
  12. Preservation of Sum/Product multiplicity and nesting, unreduced Power exponent constituents, Quotient constituent roles, and Complex components using all permitted numerical DCTs.

Cross-language fixtures SHOULD compare reconstructed DCIs and canonical encodings, not the physical bytes of HDF5 files or the whitespace of Shorthand documents.

Version 1 finalization notes

The following points should be confirmed against the current Python reference reader.py, writer.py, and external.py before this draft is declared final:

  • Format dispatch: verify exactly which suffix aliases and explicit-format overrides are supported. This draft standardizes .dcs, .dcb, and .h5 as the portable convention.
  • Directory-backed packaging: verify whether the current generic writer always copies external payloads, offers metadata-only export, or supports both. The distinction in this draft is normative for implementations claiming complete-package support; it does not assert that the Python API currently exposes both modes.
  • Default source precedence: verify how cached bytes, retained _source context, explicitly supplied sources, and destination directories are selected. Source precedence is not specified as a universal rule here; every accepted payload must still be verified.
  • HDF5 source fallback: confirm whether an explicit alternate source can override an unavailable embedded payload, and whether that should be a portable option or remain application policy.
  • Filesystem transactionality: test that conflicts and missing resources are detected before destructive destination writes, especially for directory-backed output.
  • Source metadata propagation: confirm that external nodes read from any supported format retain enough nonsemantic source context for deferred materialization and later complete-package transfer.
  • Source namespace security: define implementation guidance for symlinks and path traversal through filesystem indirection without changing the Core relative-path DCT.
  • YAML persistence: decide whether .yaml/.yml should be added to generic persistence dispatch or remain explicit YAML interchange operations.

The finalization notes identify focused interoperability and implementation-audit items, not reasons to modify DCI identity or canonical bytes.

Draft encoding revision

The current draft encodes fixed integer and Unicode information directly, without redundant Integer or String DCT identifiers. Canonical bytes and fingerprints of affected DCIs differ from earlier drafts, including DCIs that contain affected values. No automatic detection or migration of previous draft .dcb encodings is defined. Regenerate those files from the corresponding DCI or another identity-preserving representation. HDF5 fixed fields retain their payload storage types and no longer require kind attributes.

6.7 - Conventional Types

DataCanon Conventional Types — Version 1 Draft

Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1
Related: DataCanon Persistence Version 1
Scope: Optional conventional numerical and collection types, their operations, and conversion relationships with DCIs
Out of scope: DCT definitions, canonical encoding, textual syntax, and complete symbolic algebra

Purpose and boundaries

DataCanon Core defines data and identity. Core does not perform arithmetic. An implementation can also provide conventional types used directly for data and computation in its programming language. Conventional types include built-in, library-provided, and user-defined types.

An Integer DCI and a conventional integer are different objects with different contracts. The same distinction applies to a Sum DCI and a conventional Sum: the DCI preserves its constituent information, while the conventional object supports arithmetic and construction-time simplification.

Many conventional types already exist in programming languages and libraries. This companion specification defines a common behavioral foundation for additional types, including Power, Sum, Product, OrderedProduct, Quotient, ComplexNumber, Multiset, and an unordered mapping. An implementation MAY use an existing type when it satisfies the relevant requirements.

Support for this companion specification is optional. Conformance to Core, Shorthand, or a persistence specification does not require conventional arithmetic. Supporting conventional arithmetic MUST NOT change DCI equality, canonical encoding, or fingerprints.

Conformance and language bindings

The terms MUST, MUST NOT, SHOULD, and MAY express requirements, recommendations, and permissions.

Conformance is declared per conventional type. An implementation MUST identify which types it supports and any dependencies needed for their operations. For example, a Power implementation need not provide Multiset, but a numerical implementation claiming the operations below must support their result types. Construction-only adapters MUST NOT claim the arithmetic conformance defined here.

A language binding specifies class or type names, constructors, operators or equivalent functions, error reporting, and conversions in a particular language. Names such as Power(base, numerator, denominator) in this document describe a language-independent interface. A binding need not reproduce Python classes, exceptions, method names, or operator overloading.

A conforming binding MUST document:

  • supported conventional types and scalar domains;
  • construction, arithmetic, and explicit numerical conversion interfaces;
  • mappings to and from DCIs, including possible loss and failure;
  • additional simplifications and limits of exact comparison; and
  • practical resource limits and how exhausted limits are reported.

Additional conventional types and stronger exact simplification MAY be provided. Extensions MUST preserve the requirements below. No universal canonical representation of conventional objects is defined: independently implemented conventional objects can have different stored forms while representing the same numerical value.

Common numerical model

Operands and scalar domains

Scalar real numerical operands comprise conventional integers, exact rational numbers, finite floating-point numbers, and supported conventional Product, Power, Quotient, and Constant objects, plus recursively scalar Sum and OrderedProduct objects. Complex numbers are handled separately. Sum and OrderedProduct also accept numerical Arrays and array-valued expressions. Product, Power, Quotient, and ComplexNumber components remain scalar-only. Booleans MUST NOT be accepted as arithmetic operands merely because a language treats them as integers.

The portable scalar domains are mathematical integers, rational numbers with positive denominators, and finite IEEE-754 binary64 numbers. A binding SHOULD reuse appropriate existing integer and rational types. Fixed-width bindings MUST detect unsupported ranges rather than silently wrap exact arithmetic. Additional floating-point formats MAY be supported as documented extensions.

A rational with denominator one MAY be stored as an integer. Exact rational arithmetic MUST preserve the mathematical result, subject to reported resource limits. Nonfinite floats MUST be rejected as operands and MUST NOT be silently introduced into valid numerical objects by arithmetic.

Stored structure and numerical value

Conventional numerical equality concerns numerical values, rather than DCI identity or equality of stored constituents. Construction can simplify stored structure, but construction does not promise a unique normal form for every mathematically equivalent expression.

An explicit constructor MUST retain the requested outer conventional type. For example, constructing a Power whose value is two still produces a Power. Arithmetic operations MAY instead return an integer, rational, float, or another supported numerical type when simplification permits it.

Sum and Product MUST retain the first-occurrence order of surviving operand groups for traversal and presentation. Combining an operand with an earlier matching group does not move that group. Equality MUST ignore the order of Sum terms and Product factors. A binding MUST NOT sort stored operands merely to obtain commutative equality.

Exactness and floating-point arithmetic

Construction and arithmetic MUST NOT approximate an unresolved rational power by a floating-point value. Explicit numerical conversion is a separate operation.

Existing floats remain floating-point operands. Rational and floating-point coefficient groups MUST remain separate during collection of terms and factors. An implementation MUST NOT replace a float with a large rational merely to obtain an exact stored coefficient.

Arithmetic combining floats uses the binding’s documented floating-point operations and rounding. Such arithmetic can round even though an unresolved Power remains exact. Retaining a float in a structured object does not create arbitrary-precision floating-point arithmetic.

Exact comparison MAY interpret a finite float as its exact binary rational value internally. Comparison MUST NOT modify the stored float or use a rounded numerical evaluation as evidence of equality.

Constant

A conventional Constant stores exactly one name, pi or e, representing the same exact mathematical constant as the corresponding Constant DCI. A binding MUST reject other names. The Python binding provides Constant("pi") and Constant("e"), with Pi and E as conventional instances.

Construction and arithmetic MUST preserve the named constant exactly, rather than replacing it with an approximate float. Explicit floating-point conversion MAY approximate its value. Named constants participate as real numerical operands in Sum, Product, Power, Quotient, and ComplexNumber.

Comparison MUST distinguish a named constant from any finite rational or float approximation. Bindings MUST recognize identical names, cancellation of an identical nonzero constant, and matching symbolic terms. Bindings are not required to decide arbitrary identities involving transcendental constants. For example, Pi + Pi == 2 * Pi, Pi / Pi == 1, and Pi != float(Pi). No stored approximation participates in exact comparison.

Power

A Power has a real numerical base and a positive rational exponent supplied as a positive integer numerator and positive integer denominator. The denominator defaults to one. The exponent pair MUST be reduced by its greatest common divisor during construction.

For real values, a negative base with an odd reduced denominator uses the real root. A negative base with an even reduced denominator has no real value and MUST be rejected when the sign is established. A binding MAY defer a domain decision for a compound base whose sign its exact machinery cannot establish; an attempted numerical evaluation outside the real domain MUST fail.

Construction MUST extract an exact denominator-th root of an integer or rational scalar base when the entire required root is exact. The extracted root becomes the base and the reduced numerator becomes the integer exponent. The integer exponent MUST then remain stored, rather than being evaluated solely by the Power constructor.

Input constructorRequired stored result
Power(5, 2, 4)Power(5, 1, 2)
Power(4, 1, 2)Power(2, 1, 1)
Power(16, 3, 2)Power(4, 3, 1)
Power(4, 8)Power(4, 8, 1)
Power(8, 1, 2)Power(8, 1, 2)
Power(-8, 2, 3)Power(-2, 2, 1)

For a float base, a replacement base MUST preserve the exact value of the stored power. An exact representable root MAY be extracted; an approximate root MUST NOT be substituted. For example, the Python binding extracts the square root of 4.0 as 2.0, retaining a float base.

Nested powers MAY be combined only when the real branch is preserved. Blindly multiplying exponents is incorrect for negative bases: the square root of the square of negative two has value positive two. Additional exact power simplification is allowed in surrounding arithmetic; retaining an exponent in a directly constructed Power does not prohibit its evaluation in a Sum or Product.

A binding MAY provide exponentiation by zero and negative rational exponents as arithmetic operations. Zero exponent gives one wherever the operation is defined. A negative exponent is represented using a reciprocal Quotient and a positive Power. Such operations do not permit zero or negative exponent constituents in a Power constructor.

OrderedProduct and array-valued Sum

OrderedProduct preserves a sequence of real scalar and Array-expression operands. Array operands in this section are instances of the numerical array type provided by the binding. Construction MUST NOT evaluate Array products, approximate exact coefficients, infer result shapes, or flatten nesting. Implementations MUST preserve multiplication order. Scalar-only OrderedProducts are also valid.

Bindings MAY declare scalar-only Sum support separately from Array-expression arithmetic. Unsupported operands MUST be rejected explicitly. For bindings supporting Array-expression arithmetic, the Version 1 contract is:

  • Sum accepts Arrays of any rank with exactly matching shapes. No broadcasting or automatic reshaping is performed.
  • OrderedProduct accepts rank-two Arrays, multiplying them in order. Real scalar factors scale the result and may appear anywhere in the sequence.
  • Nonzero scalar terms mixed into an Array-valued Sum are rejected during evaluation; a user can supply an explicit identity Array. Scalar zero is accepted as an additive identity, supporting ordinary summation interfaces.
  • Unsupported ranks, incompatible dimensions, and nonfinite floating-point results cause evaluation to fail. Structural DCI validity is unaffected.
  • Exact rational, radical, and named-constant coefficients MUST remain exact during evaluation. Explicit floating-point or machine-complex conversion may approximate them. Fixed-width integer multiplication MUST NOT silently wrap.

The Python binding supports integer, floating-point, and complex numerical Arrays for this arithmetic. Boolean Arrays are not arithmetic operands. Object Arrays may contain supported conventional scalar entries. Exact coefficients remain exact when scaling complex Array elements as well.

The Python binding offers .evaluate() on conventional numerical objects. Array-expression evaluation returns an object-dtype NumPy Array containing conventional scalar values, including ComplexNumber where needed. Such an object Array is not a portable DataCanon Array. To encode its result, explicitly choose a supported machine dtype, or convert its nested contents to Lists so individual exact values retain their DCTs.

np.asarray(expression, dtype=float) or dtype=complex explicitly requests a machine approximation. Without a dtype, exact object entries remain. Evaluation requires allocation; copy=False is rejected. An Array-valued expression cannot be converted to scalar float or int. Array division by a real scalar may construct reciprocal scaling; division by an Array is not supported.

Python * and @ involving a conventional array-expression object preserve factor order and construct OrderedProduct. Ordinary NumPy operations involving only ordinary NumPy Arrays retain NumPy’s own semantics. Expressions retain owned read-only copies of supplied Arrays; callers MUST NOT circumvent that protection. Equality of conventional array expressions compares whole shapes and scalar entries, returning one Boolean rather than an Array of Booleans.

Sum

The following normalization rules apply to scalar-only Sums. Array-valued Sums may retain operand boundaries until explicit evaluation as described above.

A Sum contains zero or more real numerical terms. Its value is their sum; an empty Sum has value zero. Construction MUST flatten directly nested Sums and combine exact rational constants.

Matching symbolic terms with exact rational coefficients MUST combine their coefficients. The mandatory comparison cases below establish a minimum set of recognized matches; complete algebraic matching is not required. Floating-point coefficient groups MUST be collected separately from exact rational groups.

For example, two occurrences of the square root of three, one multiplied by three, combine to four times the square root of three. A separate floating-point offset remains a float. Exact cancellation MAY remove terms and can leave an empty Sum; the explicit constructor still returns a Sum.

Construction MUST preserve surviving term-group order. An implementation MAY perform additional exact term simplifications within the documented comparison limits. It MUST NOT evaluate an irrational term to collect it with a float.

Product

A Product contains zero or more real numerical factors. Its value is their product; an empty Product has value one. Construction MUST flatten directly nested Products and combine rational scalar factors. Float factors form a separate coefficient group.

Construction MUST combine powers with an identical supported scalar base by adding their rational exponents when the operation preserves the real value. An exact zero factor reduces the product’s value to zero. A floating-point zero factor remains floating-point when no exact zero determines the result. Recognized nonzero identity factors MAY be omitted.

An explicit Product constructor need not distribute over Sum factors. The arithmetic multiplication operation MUST support distribution when a scalar Sum is an operand. Retaining a factored explicit Product and expanding multiplication are compatible: exact comparison recognizes the required distributive cases.

Quotient

A Quotient contains a real numerical numerator and denominator. Unlike the Fraction DCT, either operand can be a compound numerical object. Its value is the numerator divided by the denominator.

A denominator established to be zero MUST be rejected. A binding MAY defer a zero decision that lies outside its exact comparison capabilities; numerical division by zero MUST fail when encountered.

Construction MUST reduce a quotient of rational scalars, flatten directly nested Quotients, and cancel recognized matching nonzero factors. A zero numerator with an established nonzero denominator reduces to zero over one. Equal established nonzero numerator and denominator reduce to one over one. Construction retains the Quotient outer type.

A denominator containing a scalar Sum is permitted. Rationalizing such a denominator is not a construction requirement. For example, the reciprocal of the sum of the square roots of three and two can remain stored as that Quotient.

An explicit Quotient containing a float identifies division by the exact stored float. Arithmetic division involving floats MAY use ordinary rounded floating-point division or a rounded reciprocal according to the binding. The binding MUST document that distinction. An implementation MUST NOT claim that a rounded reciprocal is an exact algebraic rewrite.

Arithmetic and numerical conversion

Conforming numerical implementations MUST provide addition, subtraction, negation, multiplication, and division with supported real operands, including scalar operands on either side. Operator spelling is language-specific. Unsupported operands MUST produce an explicit failure or the host language’s unsupported-operation protocol; they MUST NOT be silently approximated.

Arithmetic MUST preserve exact integer, rational, and unresolved power values, apart from explicitly documented operations involving floats. A result MAY unwrap an empty or singleton Sum or Product to a conventional scalar or remaining numerical object. Explicit constructors retain their outer type.

A binding MUST provide an explicit conversion to its floating-point numerical type. The conversion evaluates the numerical value and permits approximation. Overflow, an invalid real domain, and division by zero MUST be reported rather than yielding an accepted nonfinite structured operand.

An integer conversion, if provided, MUST document its rounding rule. The Python binding evaluates and truncates toward zero, following Python’s int behavior. Evaluation of an irrational expression may first round; the conversion does not promise an exact floor or truncation algorithm for arbitrary algebraic numbers near an integer boundary.

Numerical equality

Numerical equality MUST use exact reasoning for supported comparisons. Equality MUST NOT be implemented by converting both operands to floats, by tolerance, or by comparing approximate decimal renderings.

The mandatory comparison foundation includes rational arithmetic, the actual binary rational values of finite floats, operand permutation, directly nested Sum/Product flattening, distribution for finite rational combinations, exact scalar root extraction, and the radical identities in the following table. R(n, d) denotes an exact conventional rational; sqrt(n) denotes Power(n, 1, 2), not a floating-point square-root function.

ComparisonRequired result
Power(4, 1, 2) == 2true
Power(0, 1, 2) == 0true
Power(4, 8) == 65536true
sqrt(12) == 2 * sqrt(3)true
sqrt(2) * sqrt(3) == sqrt(6)true
sqrt(3) * sqrt(3) == 3true
Quotient(1, Sum(sqrt(3), sqrt(2))) == sqrt(3) - sqrt(2)true
Sum(R(1, 2), 0.25) == R(3, 4)true
Quotient(1, 2) == 0.5true
Quotient(1, 10) == 0.1false
Sum(2**53, 1) == float(2**53)false

No complete decision procedure for all mathematical identities is required. For identities beyond the mandatory foundation, a binding MAY conservatively return false when equality is not established, or report an unsupported comparison. The choice MUST be documented. Such a false result is not a proof of mathematical inequality. A resource-limit failure MAY be reported explicitly.

Factorization algorithms, search budgets, and cached comparison forms are implementation choices. A binding MUST NOT publish a factorization cutoff as a universal mathematical definition of equality. Cross-language fixtures MUST include the mandatory cases and identify any additional promised cases.

Conventional numerical objects MAY be unhashable. If hashing is provided, objects that compare equal MUST hash equally under the language’s hash contract, including comparisons with existing scalar types. Hashing arbitrary stored syntax is insufficient for mathematical equality. DCI hashing is a separate matter governed by DCI identity.

ComplexNumber

A ComplexNumber contains real and imaginary conventional numerical components. The components MAY be integers, rationals, floats, Constant, Product, Power, Quotient, or recursively scalar Sum and OrderedProduct objects. Construction MUST preserve supported exact components rather than converting them unconditionally to floats.

Equality compares corresponding component values. A binding SHOULD support comparison with a real scalar by treating its imaginary component as zero, and with an existing machine complex type using its actual stored components.

Addition, subtraction, negation, multiplication, and division MUST follow the usual complex formulas using the binding’s component arithmetic. Division by an established zero complex value MUST fail. Native scalar arithmetic may affect result types and rounding; the binding MUST document that behavior. An explicit conversion to a machine complex type MAY approximate components.

Multiset

A conventional Multiset represents a finite unordered collection with multiplicity. Equality requires equal member multiplicities under the conventional member equality relation. Member order does not affect equality. Construction and iteration SHOULD preserve insertion order for presentation.

A binding MUST provide construction from a finite collection, iteration, total occurrence count, and insertion of one occurrence. Mutability and additional collection operations are binding choices.

Membership comparison uses conventional equality, not DCI equality. For example, a language that considers integer one and floating-point one equal can treat their occurrences as belonging to the same equality group. Converting the Multiset to a Multiset DCI still converts the actual stored members individually; distinct conventional storage can produce distinct DCIs.

A Multiset SHOULD support unhashable members when their equality is usable. A binding MUST document restrictions on member comparison, including members whose equality does not return a usable Boolean result. A mutable Multiset need not be hashable.

Unordered mapping

A conventional unordered mapping supplies the language’s ordinary key/value operations with equality independent of entry order. An existing mapping type MAY satisfy this contract. The Python binding uses UnorderedDict to identify conversion to the UnorderedMapping DCT, while ordinary dict converts to the ordered Mapping DCT.

Key hashability and equality follow the conventional mapping’s rules. A binding MUST NOT claim that an ordinary mapping can retain arbitrary distinct DCI keys after converting them to conventional objects. Key collisions and unsupported keys are possible conversion outcomes.

Conversion to and from DCIs

Conversion MUST distinguish an instance of a conventional type from an object implementing a DCT. Sharing a programming language does not make their rules identical.

DCTTypical conventional counterpart
Null, Boolean, Integer, Float, String, BytesExisting language or library scalar types
FractionExact rational type
ConstantNamed conventional Constant
Power, Sum, Product, Quotient, OrderedProductCorresponding conventional numerical types
ComplexStructured conventional ComplexNumber or a machine complex type
List, Tuple, SetExisting conventional collection types
MultisetConventional Multiset
Mapping, UnorderedMappingSuitable conventional mapping types or adapters
RecordBinding-defined record or application type
ArrayNumerical array type
ExternalFile, ExternalArrayResource adapters or verified materialized content

An implementation claiming conversions for a supported conventional type MUST document both directions independently. Conversion to a DCI MUST use the conventional object’s current stored information. It MUST NOT substitute an approximate evaluated scalar for a structured numerical object merely because the numerical values are close.

Conversion from a DCI MAY invoke a conventional constructor and simplify its information. Conversion through conventional objects is therefore not universally identity preserving. A conventional Power constructor can reduce an exponent or extract a root that its source Power DCI stores structurally. A conventional rational constructor can normalize its numerator and denominator.

Mappings and Sets can merge members under conventional equality, reject unhashable members, or fail under binding policy. A binding MUST document whether collisions fail or follow ordinary collection construction. Conversion MUST NOT be advertised as lossless when such outcomes are possible.

Not every valid numerical DCI has a defined real numerical value. A conversion to a conventional numerical type MAY fail even when decoding the DCI succeeds. An implementation MUST NOT repair the DCI to hide that failure.

Array type, shape, signed zeros, and copy or mutability policy MUST be stated by the binding. External payload retrieval and verification follow Persistence; no conversion rule in this document weakens digest or element validation. Metadata-only external adapters MAY be provided. Converting an external DCI into materialized conventional content does not change the original DCI.

Python binding examples

The Python reference binding exports the conventional numerical and collection classes from data_canon. Python’s fractions.Fraction supplies exact rationals.

from fractions import Fraction
from data_canon import (
    Power, Sum, Product, Quotient, ComplexNumber, Multiset, UnorderedDict,
    as_dc_instance, dc_instance_to_python,
)

root_two = Power(2, 1, 2)
root_three = Power(3, 1, 2)
assert Sum(root_three, 3 * root_three) == 4 * root_three
assert Product(root_two, root_three) == Power(6, 1, 2)
assert Quotient(1, Sum(root_three, root_two)) == root_three - root_two
assert Quotient(1, 10) != 0.1

power = Power(16, 3, 2)
assert (power.base, power.numerator, power.denominator) == (4, 3, 1)
retained = Power(4, 8)
assert (retained.base, retained.numerator, retained.denominator) == (4, 8, 1)
assert retained == 65536

assert ComplexNumber(Fraction(1, 2), root_three) == ComplexNumber(0.5, root_three)
assert Multiset(["O", "Ti", "O"]) == Multiset(["Ti", "O", "O"])
assert UnorderedDict({"a": 1, "b": 2}) == UnorderedDict({"b": 2, "a": 1})

Conventional construction and DCI construction have distinct outcomes:

from data_canon.instance import IntegerDC, PowerDC

source = PowerDC(IntegerDC(16), 3, 2)
conventional = dc_instance_to_python(source)
converted = as_dc_instance(conventional)
assert converted == PowerDC(IntegerDC(4), 3, 1)
assert converted != source

Application-defined Record conventions

Bindings MAY provide explicit application-defined conversions between conventional types and Record DCIs. Such conventions do not define additional DCTs. For example, an application can choose a date Record with year, month, and day fields, or include calendar metadata according to its requirements. This specification does not prescribe a date schema or calendar.

A binding offering custom conversion SHOULD support recursive conversion within supported constituent structures and document handler selection, validation, precedence, and error behavior. The application is responsible for its Record schema and for the meaning and validity of the conventional result. A valid Record DCI need not satisfy an application’s schema.

Application conversion MUST NOT change DCI equality or canonical encoding. Canonical decoding MUST reconstruct the DCI independently of application conversion handlers. A Record name alone MUST NOT cause implicit execution of application code during decoding. Custom conversion is a separate explicit operation. Registries and executable handlers are not DCI constituents.

The Python binding provides an explicit ConversionRegistry with exact conventional-type matching and exact Record-name matching. Other bindings may use idiomatic mechanisms with separately documented policies. Cross-language interoperability of an application convention requires agreement on the Record name, field names, DCTs, order, and application meaning; matching language class names is not sufficient.

Independent implementation review

Cross-language tests SHOULD cover the required stored Power results, mandatory equality cases, operand-order preservation, empty and singleton constructors, float separation and rounding, zero denominators, negative-base domains, collection multiplicities, and conversions that change DCI identity.

Tests MUST distinguish numerical agreement from equal stored conventional forms and from equal DCIs. Matching numerical values alone do not imply matching canonical bytes after conversion.

Before finalization, the Python reference implementation and independent bindings should be audited for component validation in ComplexNumber, documented behavior beyond the mandatory exact-comparison foundation, floating-point overflow and signed zero, and resource-limit reporting. Reference implementation behavior is evidence for conformance, not permission to silently relax a normative requirement.