Cross-language declarative file readers: design
================================================
:Status: Implemented (Phases 0-5; see coverage below)
:Date: 2026-07-30
:Scope: SurfaceTopography (Python) and SDSAlgorithms/libsdsio (C++)
Motivation
----------
SurfaceTopography contains readers for roughly forty topography file
formats. A growing subset of these is *declarative*: the file structure is
described by a tree of layout objects (``CompoundLayout``,
``BinaryStructure``, ``BinaryArray``, ``ZipContainer``, ...) that a generic
engine (``DeclarativeReaderBase``) executes. Much of the knowledge encoded
in these descriptions is hard-won reverse engineering (undocumented masks,
padding rules, magic constants) that exists nowhere else.
Standalone C++ applications built on SDSAlgorithms need the same readers.
The current approach in ``libsdsio`` exports the Python layout trees to
JSON and generates C++ code from them. This works for simple formats but
breaks down wherever the Python description contains a lambda: the JSON
contains an opaque placeholder, and the C++ generator compensates with
per-format special cases hard-coded into the generator itself, plus
hand-written per-format data-reading code that duplicates byte offsets,
data types, scale factors and undefined-data markers. Every such
duplication is a divergence waiting to happen.
The goal of this design is a **single, language-neutral source of format
knowledge**: a format description document that a Python engine and a C++
engine execute with identical results, verified by a shared conformance
corpus.
Goals
-----
1. One description per file format, containing *all* format knowledge:
parse layout, semantic channel bindings (grid size, physical sizes,
units, height scaling, undefined-data rules) and registration metadata
(magic bytes, file extensions, MIME types).
2. Two engines — SurfaceTopography (Python) and libsdsio (C++) — that
execute these descriptions with results equal within documented
tolerances, enforced by shared golden tests.
3. Incremental migration: every phase leaves both codebases better off
even if later phases never happen.
Non-goals
---------
* Covering *all* formats. Formats built on foreign container technologies
(HDF5-based DATX, Gwyddion's own serialization, text formats) remain
native code per language, or unsupported in C++.
* A hand-editable interchange syntax. The interchange JSON is generated,
not authored (see below).
* Bit-identical floating-point results. The conformance tests use
documented tolerances.
Architecture decisions
----------------------
Source of truth: Python authors, JSON is the contract
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Format descriptions are *authored* as Python layout objects in
SurfaceTopography — this keeps the fast iteration loop for reverse
engineering (interactive exploration against real files, tests against the
example corpus) and readable descriptions (operator-overloaded
expressions, helper functions, comments). The *canonical contract* is the
exported JSON document: a versioned artifact that provably contains
everything, enforced by a CI gate requiring that
1. every declarative reader exports with **zero** opaque placeholders, and
2. a reader rehydrated *from the JSON* parses the entire example corpus
identically to the authored one.
The JSON artifacts and conformance goldens are vendored into SDSAlgorithms
with a regeneration script and a staleness check in CI. A dedicated
format-description repository is a possible later refinement if a third
consumer appears.
Expressions instead of lambdas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Data-dependent quantities (array shapes, validation conditions,
conversions, conditional layouts) are expressed in a small, serializable
expression language (``SurfaceTopography.IO.expr``) instead of Python
lambdas. Expression objects are drop-in callable where the layout classes
accept lambdas, and serialize to a JSON AST that a foreign-language
interpreter evaluates. Named functions (``F.dtype``, ``F.transpose``,
``F.parse_datetime``, ...) come from a **closed registry** — this registry
is the exact list of primitives a foreign engine must provide, and
additions to it require a contract revision. See
:doc:`format_description_contract` for the normative definition.
Generic engines, descriptions as data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Per-format reader *code* dissolves into per-format *descriptions*:
* **Python**: ``DeclarativeReaderBase`` remains the single engine. A
format contributes a layout, a declarative channel-binding section and
registration metadata. Thin per-format classes remain as shims for
backward-compatible imports and as escape hatches, but contain no
logic.
* **C++**: the code generator is retired in favor of a runtime
``DeclarativeReader`` that ingests the JSON descriptions (embedded into
the binary at build time, optionally loadable from disk). One engine,
one expression evaluator, one format registry keyed by magic bytes.
Interpretation costs nothing measurable: parsing time is dominated by
bulk data reads, not dispatch.
The channel-binding layer is part of the description
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Experience from libsdsio shows that the semantic mapping — which parsed
field is the grid size, how physical sizes are computed, where the height
data lives, what marks an undefined pixel — is where hand-written C++
diverges first (hand-derived byte offsets, re-implemented mask rules).
This mapping therefore moves into the description as a declarative
``channels`` section using the same expression language. Bulk data access
is *lazy*: the metadata pass records array handles (offset, shape, dtype,
conversion); data is materialized only when a channel is read.
Capabilities
~~~~~~~~~~~~
Container and compression primitives (zip archives, zstd, zlib, XML)
each pull a dependency in C++. Descriptions declare the capabilities they
require; an engine built without a capability reports "format known,
capability not built" instead of failing obscurely.
Phased plan
-----------
Phase 0 — Contract specification (this document's companion)
Normative spec of the description document: JSON schema, expression
node catalog, function registry, semantic edge cases (byte order,
division semantics, encodings, NaN policy), the neutral parse-result
model, laziness semantics, capability names, conformance-golden
schema, error taxonomy, versioning policy. See
:doc:`format_description_contract`.
Phase 1 — Python: expressions to production (SurfaceTopography)
Harden ``expr.py`` (bytes literals, ``DictExpr``, ``Switch`` layout
primitive, unified ``(value, context)`` converter call sites).
Complete ``to_dict()``/``from_dict()`` for all layout classes. Add the
declarative channel-binding layer to ``DeclarativeReaderBase``. Port
readers: the six formats already exported by libsdsio first (ZMG, TMD,
AL3D, SUR, MetroPro, PLU), then ZON, MNT, and OIR as the hard-case
validation. Land the round-trip equivalence CI gate.
Phase 2 — Contract artifacts
Grow the export script into a proper exporter emitting format
descriptions and machine-readable conformance goldens (probe pixels,
masked statistics, undefined-pixel counts). Wire vendoring plus a
staleness CI check in SDSAlgorithms.
Phase 3 — C++: interpreter completion (SDSAlgorithms)
Expression evaluator over the AST (the ``ctx`` node maps onto the
existing ``AttrDict::get_nested``; operators reuse its numeric
coercion). Extend the runtime with ``If``, expression-driven
validation, lazy array handles and mask materialization. Generic
reader plus registry; delete the code generator and the hand-written
per-format ``topography()`` methods; replace pasted golden values with
a generic conformance runner.
Phase 4 — Containers and coverage
Add capability primitives to the C++ engine as demand dictates (zlib
for MNT, zip+zstd for ZON, XML), each behind a build option. Continue
porting readers; maintain an explicit coverage list.
Phase 5 — Governance
Documented workflow for new formats (author in Python → corpus tests →
export → C++ conformance runs without writing C++) and the schema
evolution policy.
Implementation status and coverage
----------------------------------
Fully declarative formats (exported to description documents, executed by
both engines, pinned by the conformance goldens): **ZMG, TMD, AL3D, SUR,
MetroPro, PLU, ZON**. Of these, ZON requires the ``zip``/``zstd``/``xml``
capabilities, which the C++ engine does not implement yet: it rejects the
description cleanly (``unsupported_capability_error``) while format
detection still works from the document.
Not yet declarative (Python-only, native readers): MNT (heuristic zlib
block chains), OIR (context restructuring pending a ``Switch``/``dict``
port), and the container/text formats named under Non-goals.
Key artifacts:
* Python engine: ``SurfaceTopography/IO/expr.py`` (expressions),
``description.py`` (serialization, generic reader factory),
``Reader.py`` (``DeclarativeReaderBase`` with ``_channel_bindings``,
``Switch``, ``Seek``), ``export.py`` (artifact exporter).
* CI gates: ``test/IO/test_description.py`` (zero-opaque, rehydrated
generic-reader equivalence over the corpus, deterministic export),
``test/IO/test_expr.py``, ``test/IO/test_declarative_reader_serialization.py``.
* C++ engine: ``src/libsdsio/declarative_reader.{h,cpp}`` in
SDSAlgorithms (expression evaluator, layout interpreter, lazy array
handles, registry with embedded descriptions);
``tests/cpp/test_declarative_reader.cpp`` is the golden conformance
runner. The former code generator (``generate_from_json.py``), exported
layout JSONs and hand-written per-format readers are retired.
* Contract artifacts: vendored in SDSAlgorithms under
``src/libsdsio/descriptions/`` and ``tests/goldens/``, regenerated only
by ``update_format_descriptions.py`` and pinned by
``tests/python/test_format_descriptions.py``.
Workflow for a new format
-------------------------
1. Reverse engineer and author the reader in SurfaceTopography as a
``DeclarativeReaderBase`` subclass: ``_file_layout`` and
``_channel_bindings`` built from expressions (no lambdas), ``_magic``
for detection. Iterate against real files; add a fixture to
``test/file_format_examples`` and per-format tests.
2. Register the format in ``SurfaceTopography/IO/export.py`` and in
``test/IO/test_description.py``. The CI gate now enforces that the
description is complete (zero opaque hooks) and that the rehydrated
generic reader reproduces the authored one.
3. In SDSAlgorithms, copy the fixture into ``tests/datafiles`` and run
``python update_format_descriptions.py``; commit the regenerated
descriptions and goldens. The C++ conformance runner picks the new
golden up automatically — no C++ code is written unless the format
needs a new capability or layout primitive.
4. If a new expression function or layout node is unavoidable, revise the
contract (bump the schema version per the change policy) and implement
the primitive in both engines in the same change set.
Risks
-----
Registry and expression creep
The function registry must remain a closed, reviewed list; otherwise
the expression language slowly becomes a bad general-purpose language
(the DFDL failure mode). Formats that cannot be expressed with the
existing primitives are candidates for native readers, not for
ad-hoc registry growth.
Semantic divergence at the edges
Byte order and alignment, floor vs. truncating division, string
encodings, NaN handling. Mitigation: these are specified normatively
in the contract, and the conformance goldens include masked
statistics over full arrays, not only probe pixels.
Hard formats
OIR-class formats (nested TLV, tag-driven layout switching) test the
limits of the declarative model. If a format does not go fully
declarative in Phase 1, the fallback — native reader on both sides —
is decided consciously there.
Prior art
---------
* `Kaitai Struct `_ — YAML-based binary format DSL
compiled to parsers in many languages; closest existing system, and the
design reference for the expression language.
* DFDL (Data Format Description Language) — the standards-body take;
cautionary example of expression-language growth.
* `Wuffs `_ — memory-safe generated
parsers for untrusted inputs; motivates generated/interpreted parsing
with systematic bounds checks over hand-written C++.