Task: Write the exporter from the same tables

Table of Contents

This page documents a task in the Decide the XML strategy for ORE documents story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

Write the document back from the same tables. The write direction need not mirror the read direction; it has to be valid, cheap and lossless. Three rules carry it: schema order, present fields only, and a decimal form that never exponentiates.

2. Status

Field Value
State DONE
Parent story Decide the XML strategy for ORE documents
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-17

3. Acceptance

  • Output validates against input.xsd for every document in the corpus.
  • A decimal writes as fixed point at the column's scale and never as an exponent.
  • Only text that needs escaping is escaped, decided by one scan.
  • Reading the output yields the same values as reading the original.

4. Plan

(Implementation strategy. Written when work starts; key decisions are distilled into the parent story's * Decisions at close, but the plan itself stays — it is the historical record of what we did.)

5. Plan

The direction is a walk of the table, not a replay of the reading. The emitter takes the same document_type the binder takes and walks the record in table order, which the generator emits in schema order. Nothing about the document that was read survives, so a record built by any other route writes the same way.

Three rules carry it.

  • Present fields only. A field whose presence bit is clear writes nothing. A required field whose bit is clear is an error, not a silent omission: the writing would not validate, so it is not made.
  • A decimal is plain text. The writer calls the decimal's own canonical rendering, which never exponentiates and never invents a column scale. A continuous quantity uses the shortest text that reads back to the same double, and may exponentiate because xs:float accepts it.
  • Escape in one scan. A text node escapes &, < and >; an attribute also escapes " and the whitespace a parser would normalize away. A value that needs none of them is copied whole.

The table grew one thing: a repeated member's operations. A repeated member is a std::vector whose element type only the generated code knows, so field now carries a vector_ops of three functions – append, count, at. The reader appends; the writer measures and indexes. Before this, the row carried only append, which is why the reading existed and the writing did not.

The writing found three defects in the generator. None of them was visible from the reading, because the reading tests counted documents rather than comparing values:

  • A repeated compositor repeats its members. conventions.xsd states the root as xs:choice minOccurs"0" maxOccurs="unbounded", so every member may appear any number of times. The generator read each member's own =maxOccurs and modelled Deposits as one optional record, so a document with twelve Deposits bound twelve values into one record and the writer emitted a single <Deposit> with the values concatenated. The compositor's repetition now reaches its members.
  • A repeated leaf carries its element's own kind. Every repeated leaf row said scalar_kind::text regardless of the element, so a repeated xs:integer wrote std::string values into a std::vector<std::int64_t>. It was silent heap corruption rather than a wrong answer, and the netting family's Currencies is the case: vector<std::int16_t> over an enumeration, read as strings.
  • An enumeration index needs its spellings. A repeated enumerated element now has scalar_kind::enumeration: the reader validates the spelling and stores the index, the writer renders the spelling back.

A generated record states its own zeros. An int16_t member with no initializer is indeterminate in a default-initialized record, so a record built by hand carried garbage into a writing and failed on an enumeration index that was never set. Every numeric member now states = 0=. The decimal already defaulted to zero.

6. Measurements

Every number this task claims about a tree, with the command that produced it and the commit it was measured at. A number without both is not a measurement, because a clean-up record describes a tree and trunk moves under a branch: a consumer count, a gate's component count and a suite's size all change with no code changing, and the reader of a bare number cannot tell which tree it came from. Where the raw output matters, put it in an evidence/ file beside the task and name that file in the Command column.

What Value Command Commit
Emitted documents that validate against input.xsd 246 of 246 ./scripts/validate_ore_examples.sh build/output/linux-clang-release-ninja/xml-roundtrip/emitted external/ore/xsd pending
XML suites, all green 537 cases, 63,635 assertions publish/bin/ores.ore.core.tests "[ore][xml]" pending
Corpus documents written back and re-read stably 246 publish/bin/ores.ore.core.tests "[ore][xml][emitter]" pending

7. Notes

8. Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
     

9. PRs

PR Title
#3447 [ore] Write the document back from the same tables

10. Review

Comment summary File Decision Notes
       

11. Result

Merged. The writer walks the same table the binder fills and writes only the fields whose row says they appeared. Its fourth acceptance criterion, that a reading of the output yields the same values, is proved by the round-trip task rather than here, because that is where the harness lives.

Emacs 29.3 (Org mode 9.6.15)