tesserae-dl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tesserae_dl-0.1.0/.gitignore +14 -0
- tesserae_dl-0.1.0/CHANGELOG.md +52 -0
- tesserae_dl-0.1.0/LICENSE +21 -0
- tesserae_dl-0.1.0/PKG-INFO +107 -0
- tesserae_dl-0.1.0/README.md +89 -0
- tesserae_dl-0.1.0/docs/api.md +206 -0
- tesserae_dl-0.1.0/docs/benchmarks.md +55 -0
- tesserae_dl-0.1.0/docs/decisions.md +50 -0
- tesserae_dl-0.1.0/docs/language.md +284 -0
- tesserae_dl-0.1.0/docs/versioning.md +41 -0
- tesserae_dl-0.1.0/pyproject.toml +64 -0
- tesserae_dl-0.1.0/src/tdl/__init__.py +67 -0
- tesserae_dl-0.1.0/src/tdl/_decode.py +31 -0
- tesserae_dl-0.1.0/src/tdl/_edit.py +151 -0
- tesserae_dl-0.1.0/src/tdl/_io.py +41 -0
- tesserae_dl-0.1.0/src/tdl/_parser.py +835 -0
- tesserae_dl-0.1.0/src/tdl/_source.py +35 -0
- tesserae_dl-0.1.0/src/tdl/errors.py +86 -0
- tesserae_dl-0.1.0/src/tdl/limits.py +15 -0
- tesserae_dl-0.1.0/src/tdl/origin.py +22 -0
- tesserae_dl-0.1.0/src/tdl/py.typed +0 -0
- tesserae_dl-0.1.0/src/tdl/tree.py +362 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/children-ordinary.expect.json +33 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/children-ordinary.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/close-with-space.expect.json +22 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/close-with-space.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/comma-separated.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/comma-separated.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/comments-around-root.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/comments-around-root.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/empty.expect.json +22 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/empty.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/lone-cr.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/lone-cr.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/nested.expect.json +34 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/nested.tdl +11 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/one-line.expect.json +29 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/one-line.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/repeated-names.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/repeated-names.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/trailing-comma.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/blocks/trailing-comma.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/comments/comment-between-entries.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/comments/comment-between-entries.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/comments/hash-in-slot.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/comments/hash-in-slot.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/comments/hash-in-word.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/comments/hash-in-word.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/comments/trailing.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/comments/trailing.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/comments/whole-line.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/comments/whole-line.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/edits/bom-kept.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/bom-kept.result.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/bom-kept.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/edits/comments-kept.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/comments-kept.result.tdl +15 -0
- tesserae_dl-0.1.0/tests/corpus/edits/comments-kept.tdl +14 -0
- tesserae_dl-0.1.0/tests/corpus/edits/crlf-kept.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/crlf-kept.result.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/crlf-kept.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/edits/other-key-before-fence.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/other-key-before-fence.result.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/other-key-before-fence.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/edits/refused-replace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/edits/refused-replace.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/edits/refused-without-name.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/refused-without-name.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/edits/replace-span.expect.json +8 -0
- tesserae_dl-0.1.0/tests/corpus/edits/replace-span.result.tdl +10 -0
- tesserae_dl-0.1.0/tests/corpus/edits/replace-span.tdl +10 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-after-last-version.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-after-last-version.result.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-after-last-version.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-after-tdl-only.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-after-tdl-only.result.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-after-tdl-only.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-replace-existing.expect.json +7 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-replace-existing.result.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/uid-replace-existing.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/with-property-add.expect.json +8 -0
- tesserae_dl-0.1.0/tests/corpus/edits/with-property-add.result.tdl +11 -0
- tesserae_dl-0.1.0/tests/corpus/edits/with-property-add.tdl +10 -0
- tesserae_dl-0.1.0/tests/corpus/edits/with-property-replace.expect.json +8 -0
- tesserae_dl-0.1.0/tests/corpus/edits/with-property-replace.result.tdl +10 -0
- tesserae_dl-0.1.0/tests/corpus/edits/with-property-replace.tdl +10 -0
- tesserae_dl-0.1.0/tests/corpus/edits/without-metadata.expect.json +6 -0
- tesserae_dl-0.1.0/tests/corpus/edits/without-metadata.result.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/edits/without-metadata.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-encoding.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-encoding.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-escape.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-escape.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-name-dash.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-name-dash.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-name-digit.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bad-name-digit.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash-in-call.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash-in-call.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash-in-list.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash-in-list.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash-metadata.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash-metadata.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/bare-hash.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/double-close.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/double-close.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/errors/duplicate-key-metadata.expect.json +15 -0
- tesserae_dl-0.1.0/tests/corpus/errors/duplicate-key-metadata.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/duplicate-key.expect.json +15 -0
- tesserae_dl-0.1.0/tests/corpus/errors/duplicate-key.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/errors/empty-slot.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/empty-slot.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/many-roots.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/many-roots.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/metadata-unclosed.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/metadata-unclosed.tdl +3 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-metadata.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-metadata.tdl +1 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-name.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-name.tdl +4 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-root.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-root.tdl +4 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-version.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/no-version.tdl +4 -0
- tesserae_dl-0.1.0/tests/corpus/errors/reserved-name-block.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/reserved-name-block.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/reserved-name-metadata.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/reserved-name-metadata.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/six-mistakes.expect.json +40 -0
- tesserae_dl-0.1.0/tests/corpus/errors/six-mistakes.tdl +13 -0
- tesserae_dl-0.1.0/tests/corpus/errors/slot-ends-at-first-close.expect.json +14 -0
- tesserae_dl-0.1.0/tests/corpus/errors/slot-ends-at-first-close.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-block.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-block.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-slot.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-slot.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-string-metadata.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-string-metadata.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-string.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unclosed-string.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-close.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-close.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-after-root.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-after-root.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-after-string.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-after-string.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-call-comma.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-call-comma.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-call-comment.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-call-comment.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-call-stray-brace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-call-stray-brace.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-else-no-brace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-else-no-brace.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-else-without-if.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-else-without-if.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-entry-start.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-entry-start.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-brace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-brace.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-in.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-in.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-items.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-items.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-key-slot.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-key-slot.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-key.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-key.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-variable.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-for-no-variable.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-if-no-brace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-if-no-brace.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-if-no-condition.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-if-no-condition.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-after-string.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-after-string.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-line.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-line.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-no-colon.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-no-colon.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-no-value.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-metadata-no-value.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-no-colon.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-no-colon.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-no-value-comment.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-no-value-comment.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-no-value.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-no-value.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-root-no-brace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-root-no-brace.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-root-not-a-name.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-root-not-a-name.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-stray-brace.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-stray-brace.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-unclosed-call.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-unclosed-call.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-unclosed-list.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unexpected-text-unclosed-list.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unsupported-version-not-a-number.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unsupported-version-not-a-number.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unsupported-version.expect.json +9 -0
- tesserae_dl-0.1.0/tests/corpus/errors/unsupported-version.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/basic.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/basic.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/bom.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/bom.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/crlf.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/crlf.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/extra-keys.expect.json +23 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/extra-keys.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/fence-trailing-space.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/fence-trailing-space.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/no-final-newline.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/no-final-newline.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/quoted-value.expect.json +20 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/quoted-value.tdl +6 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/version-comment.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/version-comment.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/version-is-text.expect.json +17 -0
- tesserae_dl-0.1.0/tests/corpus/metadata/version-is-text.tdl +5 -0
- tesserae_dl-0.1.0/tests/corpus/slots/one-slot.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/slots/one-slot.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/slots/punctuation-inside.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/slots/punctuation-inside.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/slots/quoted-close.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/slots/quoted-close.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/slots/slot-with-colon-in-list.expect.json +31 -0
- tesserae_dl-0.1.0/tests/corpus/slots/slot-with-colon-in-list.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/slots/spaced-braces.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/slots/spaced-braces.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/slots/template.expect.json +34 -0
- tesserae_dl-0.1.0/tests/corpus/slots/template.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/strings/double-and-single.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/strings/double-and-single.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/strings/escapes.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/strings/escapes.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/strings/hash-and-comment.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/strings/hash-and-comment.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/strings/slot-in-string-is-text.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/strings/slot-in-string-is-text.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/structure/else-after-comment.expect.json +35 -0
- tesserae_dl-0.1.0/tests/corpus/structure/else-after-comment.tdl +13 -0
- tesserae_dl-0.1.0/tests/corpus/structure/else-nested-if.expect.json +38 -0
- tesserae_dl-0.1.0/tests/corpus/structure/else-nested-if.tdl +11 -0
- tesserae_dl-0.1.0/tests/corpus/structure/for.expect.json +40 -0
- tesserae_dl-0.1.0/tests/corpus/structure/for.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/structure/if-else.expect.json +35 -0
- tesserae_dl-0.1.0/tests/corpus/structure/if-else.tdl +11 -0
- tesserae_dl-0.1.0/tests/corpus/structure/if-in-for.expect.json +41 -0
- tesserae_dl-0.1.0/tests/corpus/structure/if-in-for.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/structure/if.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/structure/if.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/structure/reserved-in-word.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/structure/reserved-in-word.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/values/call.expect.json +38 -0
- tesserae_dl-0.1.0/tests/corpus/values/call.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/list-empty.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/list-empty.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/list-multiline.expect.json +31 -0
- tesserae_dl-0.1.0/tests/corpus/values/list-multiline.tdl +10 -0
- tesserae_dl-0.1.0/tests/corpus/values/list.expect.json +37 -0
- tesserae_dl-0.1.0/tests/corpus/values/list.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/named-sides.expect.json +35 -0
- tesserae_dl-0.1.0/tests/corpus/values/named-sides.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/no-space-after-colon.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/no-space-after-colon.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-float.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-float.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-int.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-int.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-like-word.expect.json +30 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-like-word.tdl +8 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-negative.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/number-negative.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/percent.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/percent.tdl +7 -0
- tesserae_dl-0.1.0/tests/corpus/values/word-keywords.expect.json +36 -0
- tesserae_dl-0.1.0/tests/corpus/values/word-keywords.tdl +9 -0
- tesserae_dl-0.1.0/tests/corpus/values/word-with-spaces.expect.json +24 -0
- tesserae_dl-0.1.0/tests/corpus/values/word-with-spaces.tdl +7 -0
- tesserae_dl-0.1.0/tests/layouts.py +60 -0
- tesserae_dl-0.1.0/tests/test_acceptance.py +97 -0
- tesserae_dl-0.1.0/tests/test_corpus.py +153 -0
- tesserae_dl-0.1.0/tests/test_corpus_files.py +195 -0
- tesserae_dl-0.1.0/tests/test_edits.py +224 -0
- tesserae_dl-0.1.0/tests/test_errors.py +52 -0
- tesserae_dl-0.1.0/tests/test_limits.py +155 -0
- tesserae_dl-0.1.0/tests/test_parse.py +190 -0
- tesserae_dl-0.1.0/tests/test_reader_properties.py +110 -0
- tesserae_dl-0.1.0/tests/test_roundtrip.py +137 -0
- tesserae_dl-0.1.0/tests/test_threads.py +54 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the package. Versions follow [Semantic Versioning](https://semver.org); below
|
|
4
|
+
1.0 a minor version may break the API. The language version is separate and is 0.1 for every release
|
|
5
|
+
here (`docs/versioning.md`).
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
The first release: everything in the first-slice plan, steps 1 to 8.
|
|
10
|
+
|
|
11
|
+
### The language
|
|
12
|
+
|
|
13
|
+
- A file is a metadata block (`TDL:`, `Name:` and other keys) and exactly one root block. Blocks,
|
|
14
|
+
properties, `If`/`Else`/`For`, comments, and the values word, string, number, percentage, call,
|
|
15
|
+
list, `{{ slot }}` and template. The reference is `docs/language.md`; the 32 decisions behind it
|
|
16
|
+
are in `docs/decisions.md`.
|
|
17
|
+
- 22 error codes with a plain message, a hint and a position (18 language codes and four for
|
|
18
|
+
what the library will not read: `tdl-too-large`, `tdl-too-deep`, `tdl-unsupported-version`,
|
|
19
|
+
`tdl-bad-encoding`).
|
|
20
|
+
|
|
21
|
+
### The library
|
|
22
|
+
|
|
23
|
+
- `loads`, `load`, `parse`, `dumps`, `dump`. `parse` never raises for a syntax error: it returns the
|
|
24
|
+
best tree it could build and every error found, in source order, up to 50.
|
|
25
|
+
- A lossless tree: `Document`, `Metadata`, `Block`, `Property`, `If`, `For`, `Comment` and the value
|
|
26
|
+
classes, immutable and carrying where they came from. `dumps(loads(text)) == text`, byte for byte:
|
|
27
|
+
comments, blank lines, indentation, `\n` and `\r\n` per line, a byte-order mark and a missing
|
|
28
|
+
final newline are all kept.
|
|
29
|
+
- Edits that change only the bytes they name: `Document.with_metadata`, `without_metadata`,
|
|
30
|
+
`replace` and `Block.with_property`, and `Document.find` for a dotted path of `Name:` values. An
|
|
31
|
+
edit that would break the file raises and returns nothing.
|
|
32
|
+
- `Limits`: 1 MiB of text, 50 errors, 64 levels of nesting.
|
|
33
|
+
- Python 3.14 or later, no dependencies, typed (`py.typed`).
|
|
34
|
+
|
|
35
|
+
### The conformance corpus
|
|
36
|
+
|
|
37
|
+
- `tests/corpus/`: 54 valid files with their normalised content, 57 files with errors and their
|
|
38
|
+
positions, 13 edits with their results. It ships in the source distribution.
|
|
39
|
+
|
|
40
|
+
### Measured
|
|
41
|
+
|
|
42
|
+
- A 100 KB file reads in 13 ms and a file at the 1 MiB limit in 140 ms; time grows linearly, and no
|
|
43
|
+
input built to be slow (`benchmarks/bench.py`) is super-linear. See `docs/benchmarks.md`.
|
|
44
|
+
|
|
45
|
+
### Not in this release
|
|
46
|
+
|
|
47
|
+
- Checking keys or types against a schema, and anything that knows what a block means: that is the
|
|
48
|
+
program that reads the file.
|
|
49
|
+
- Building a tree in code and formatting it, `canonical()` on such a tree, `to_data()`, a command
|
|
50
|
+
line, a TextMate grammar and a language server (`docs/api.md`, open questions).
|
|
51
|
+
- `Block.properties`, `Block.blocks`, `Block.get`, `Block.walk` and `Document.imports`, listed in
|
|
52
|
+
`docs/api.md` but in no step of the first slice.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mind Derivative
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tesserae-dl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TDL, the Tesserae Data Language: a lossless parser for the files the Tesserae Shell reads.
|
|
5
|
+
Project-URL: Repository, https://github.com/mindderivative/tess-dl
|
|
6
|
+
Project-URL: Issues, https://github.com/mindderivative/tess-dl/issues
|
|
7
|
+
Author: Mind Derivative
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# TDL
|
|
20
|
+
|
|
21
|
+
TDL, the Tesserae Data Language, is a small structure-only language for the files the Tesserae
|
|
22
|
+
Shell reads: blocks, properties and values. `tdl` is the Python library that reads it into a tree
|
|
23
|
+
that keeps everything in the file (comments, layout, positions), reports every mistake with
|
|
24
|
+
`file:line:column`, and writes the text back unchanged except for an edit you ask for.
|
|
25
|
+
|
|
26
|
+
> **Status:** alpha, package 0.1.0 (not yet published), language version 0.1. The API may change
|
|
27
|
+
> before 1.0 (a minor version may break it; see [`docs/versioning.md`](docs/versioning.md)).
|
|
28
|
+
> The language is in [`docs/language.md`](docs/language.md), the API in [`docs/api.md`](docs/api.md),
|
|
29
|
+
> the reasons for the choices in [`docs/decisions.md`](docs/decisions.md).
|
|
30
|
+
|
|
31
|
+
`tdl` needs Python 3.14 or later and has no dependencies:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install tesserae-dl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import tdl
|
|
39
|
+
|
|
40
|
+
text = """\
|
|
41
|
+
---
|
|
42
|
+
TDL: 0.1
|
|
43
|
+
Name: Shell_Root
|
|
44
|
+
---
|
|
45
|
+
Shell {
|
|
46
|
+
Bar { Name: Top_Bar, Height: 32 } # pixels
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
doc = tdl.loads(text, path="Main_Shell.tdl") # raises TDLSyntaxError on the first mistake
|
|
51
|
+
doc.root.type # 'Shell'
|
|
52
|
+
doc.find("Shell_Root.Top_Bar").name # 'Top_Bar'
|
|
53
|
+
assert tdl.dumps(doc) == text # the exact text back, byte for byte
|
|
54
|
+
|
|
55
|
+
broken = text.replace("Height: 32", "Height 32")
|
|
56
|
+
result = tdl.parse(broken) # never raises for a syntax error
|
|
57
|
+
[str(e) for e in result.errors] # every error, in source order, up to 50
|
|
58
|
+
|
|
59
|
+
doc2 = doc.with_metadata("UID", "0192d3c4") # one edit: every other byte is unchanged
|
|
60
|
+
doc3 = doc.find("Shell_Root.Top_Bar").with_property("Height", "40")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `loads`, `load` (a binary file object), `parse`, `dumps`, `dump`.
|
|
64
|
+
- A tree of `Document`, `Metadata`, `Block`, `Property`, `If`, `For` and `Comment`, with `Word`,
|
|
65
|
+
`String`, `Number`, `Percent`, `Call`, `List`, `Slot` and `Template` for values. Nodes are
|
|
66
|
+
immutable, keep where they came from (`node.origin`), and are safe to share between threads.
|
|
67
|
+
- Edits return a new `Document` and change only the bytes they name: `with_metadata`,
|
|
68
|
+
`without_metadata`, `Block.with_property`, `replace`. An edit that would break the file raises.
|
|
69
|
+
- 22 error codes, each with a plain message, a hint and a position (`tdl-bare-hash`:
|
|
70
|
+
"This colour needs quotes. Quote the colour: "#ffffff""). The list is in
|
|
71
|
+
[`docs/language.md`](docs/language.md#errors).
|
|
72
|
+
- Limits (`tdl.Limits`): 1 MiB of text, 50 errors, 64 levels of nesting.
|
|
73
|
+
- Not in 0.1: checking keys or types (that is the program's, such as the shell), building a tree
|
|
74
|
+
in code and formatting it, `to_data()`, a command line. See [`docs/api.md`](docs/api.md).
|
|
75
|
+
|
|
76
|
+
A 100 KB file reads in about 13 ms and a file at the 1 MiB limit in about 140 ms
|
|
77
|
+
([`docs/benchmarks.md`](docs/benchmarks.md)).
|
|
78
|
+
|
|
79
|
+
PyPI name: `tesserae-dl`. Import name: `tdl`.
|
|
80
|
+
|
|
81
|
+
## The conformance corpus
|
|
82
|
+
|
|
83
|
+
`tests/corpus/` holds the files and expected results that define the language: valid files with
|
|
84
|
+
their normalised content, files with each error and its position, and edits with their result. It
|
|
85
|
+
ships in the source distribution (not in the wheel) as test data, and the Tesserae Shell reads it.
|
|
86
|
+
New cases come from a `TDL-REQ-###` through the maintainer; see
|
|
87
|
+
[`.agents/skills/conformance-corpus/SKILL.md`](.agents/skills/conformance-corpus/SKILL.md).
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
uv pip install -e . --group dev # into the active venv (Python 3.14+)
|
|
93
|
+
python tools/check.py # ruff, format check, mypy (strict), pytest: what CI runs
|
|
94
|
+
python benchmarks/bench.py # timing against the budget (docs/benchmarks.md)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Work is tracked on the [Tesserae Data Language board](https://github.com/users/mindderivative/projects/5)
|
|
98
|
+
(`python tools/board.py status`). Agent instructions are in [`AGENTS.md`](AGENTS.md); project skills
|
|
99
|
+
are in `.agents/skills/`.
|
|
100
|
+
|
|
101
|
+
## Versioning
|
|
102
|
+
|
|
103
|
+
See [`docs/versioning.md`](docs/versioning.md).
|
|
104
|
+
|
|
105
|
+
## Licence
|
|
106
|
+
|
|
107
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# TDL
|
|
2
|
+
|
|
3
|
+
TDL, the Tesserae Data Language, is a small structure-only language for the files the Tesserae
|
|
4
|
+
Shell reads: blocks, properties and values. `tdl` is the Python library that reads it into a tree
|
|
5
|
+
that keeps everything in the file (comments, layout, positions), reports every mistake with
|
|
6
|
+
`file:line:column`, and writes the text back unchanged except for an edit you ask for.
|
|
7
|
+
|
|
8
|
+
> **Status:** alpha, package 0.1.0 (not yet published), language version 0.1. The API may change
|
|
9
|
+
> before 1.0 (a minor version may break it; see [`docs/versioning.md`](docs/versioning.md)).
|
|
10
|
+
> The language is in [`docs/language.md`](docs/language.md), the API in [`docs/api.md`](docs/api.md),
|
|
11
|
+
> the reasons for the choices in [`docs/decisions.md`](docs/decisions.md).
|
|
12
|
+
|
|
13
|
+
`tdl` needs Python 3.14 or later and has no dependencies:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install tesserae-dl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import tdl
|
|
21
|
+
|
|
22
|
+
text = """\
|
|
23
|
+
---
|
|
24
|
+
TDL: 0.1
|
|
25
|
+
Name: Shell_Root
|
|
26
|
+
---
|
|
27
|
+
Shell {
|
|
28
|
+
Bar { Name: Top_Bar, Height: 32 } # pixels
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
doc = tdl.loads(text, path="Main_Shell.tdl") # raises TDLSyntaxError on the first mistake
|
|
33
|
+
doc.root.type # 'Shell'
|
|
34
|
+
doc.find("Shell_Root.Top_Bar").name # 'Top_Bar'
|
|
35
|
+
assert tdl.dumps(doc) == text # the exact text back, byte for byte
|
|
36
|
+
|
|
37
|
+
broken = text.replace("Height: 32", "Height 32")
|
|
38
|
+
result = tdl.parse(broken) # never raises for a syntax error
|
|
39
|
+
[str(e) for e in result.errors] # every error, in source order, up to 50
|
|
40
|
+
|
|
41
|
+
doc2 = doc.with_metadata("UID", "0192d3c4") # one edit: every other byte is unchanged
|
|
42
|
+
doc3 = doc.find("Shell_Root.Top_Bar").with_property("Height", "40")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `loads`, `load` (a binary file object), `parse`, `dumps`, `dump`.
|
|
46
|
+
- A tree of `Document`, `Metadata`, `Block`, `Property`, `If`, `For` and `Comment`, with `Word`,
|
|
47
|
+
`String`, `Number`, `Percent`, `Call`, `List`, `Slot` and `Template` for values. Nodes are
|
|
48
|
+
immutable, keep where they came from (`node.origin`), and are safe to share between threads.
|
|
49
|
+
- Edits return a new `Document` and change only the bytes they name: `with_metadata`,
|
|
50
|
+
`without_metadata`, `Block.with_property`, `replace`. An edit that would break the file raises.
|
|
51
|
+
- 22 error codes, each with a plain message, a hint and a position (`tdl-bare-hash`:
|
|
52
|
+
"This colour needs quotes. Quote the colour: "#ffffff""). The list is in
|
|
53
|
+
[`docs/language.md`](docs/language.md#errors).
|
|
54
|
+
- Limits (`tdl.Limits`): 1 MiB of text, 50 errors, 64 levels of nesting.
|
|
55
|
+
- Not in 0.1: checking keys or types (that is the program's, such as the shell), building a tree
|
|
56
|
+
in code and formatting it, `to_data()`, a command line. See [`docs/api.md`](docs/api.md).
|
|
57
|
+
|
|
58
|
+
A 100 KB file reads in about 13 ms and a file at the 1 MiB limit in about 140 ms
|
|
59
|
+
([`docs/benchmarks.md`](docs/benchmarks.md)).
|
|
60
|
+
|
|
61
|
+
PyPI name: `tesserae-dl`. Import name: `tdl`.
|
|
62
|
+
|
|
63
|
+
## The conformance corpus
|
|
64
|
+
|
|
65
|
+
`tests/corpus/` holds the files and expected results that define the language: valid files with
|
|
66
|
+
their normalised content, files with each error and its position, and edits with their result. It
|
|
67
|
+
ships in the source distribution (not in the wheel) as test data, and the Tesserae Shell reads it.
|
|
68
|
+
New cases come from a `TDL-REQ-###` through the maintainer; see
|
|
69
|
+
[`.agents/skills/conformance-corpus/SKILL.md`](.agents/skills/conformance-corpus/SKILL.md).
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
uv pip install -e . --group dev # into the active venv (Python 3.14+)
|
|
75
|
+
python tools/check.py # ruff, format check, mypy (strict), pytest: what CI runs
|
|
76
|
+
python benchmarks/bench.py # timing against the budget (docs/benchmarks.md)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Work is tracked on the [Tesserae Data Language board](https://github.com/users/mindderivative/projects/5)
|
|
80
|
+
(`python tools/board.py status`). Agent instructions are in [`AGENTS.md`](AGENTS.md); project skills
|
|
81
|
+
are in `.agents/skills/`.
|
|
82
|
+
|
|
83
|
+
## Versioning
|
|
84
|
+
|
|
85
|
+
See [`docs/versioning.md`](docs/versioning.md).
|
|
86
|
+
|
|
87
|
+
## Licence
|
|
88
|
+
|
|
89
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# TDL Library API
|
|
2
|
+
|
|
3
|
+
> Moved here from the Tesserae Shell design vault on 2026-11-02. This file is now the source of truth; the vault keeps a short pointer. Names of vault notes and decisions (D1 to D94, the ADR-0014 file layout and so on) refer to that vault and are not links.
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> The public Python API of the TDL library: repository and folder `tess-dl`, PyPI package `tesserae-dl`, import name `tdl` (ADR-0014 Shell File Language > Open Items, maintainer, 2026-11-02). The library reads TDL text into a tree that keeps everything in the file (comments, layout, positions), reports errors with `file:line:column`, and writes the text back unchanged except for a requested edit. It knows nothing about bars, monitors, tre, or the shell's keys. This note is a proposal for the maintainer, written so the `tess-dl` session can start from it. Names follow the Python standard library where a library does the same job (`json`, `tomllib`), because the maintainer asked that a person from another framework finds a familiar shape (ADR-0014 Shell File Language > Ninth-Round Decisions (2026-11-02) D68).
|
|
7
|
+
|
|
8
|
+
## Purpose and Scope
|
|
9
|
+
|
|
10
|
+
- Define the functions, classes, errors and guarantees that the shell, an editor tool and a test can rely on.
|
|
11
|
+
- Out of scope, by design: checking keys and types against a schema, typing a value (a length, a colour, a boolean), reading other files for `Import`, evaluating `{{ }}` slots (TEL does that), and talking to tre. All of those belong to the shell (Data Flow > Loading a Shell).
|
|
12
|
+
- The library takes text, not paths: file reading, `Import` and the config directory are the shell's (AGENTS > 3. Layering Principle, "Data in, not files").
|
|
13
|
+
|
|
14
|
+
## Where the Library Sits
|
|
15
|
+
|
|
16
|
+
```mermaid
|
|
17
|
+
flowchart LR
|
|
18
|
+
T[".tdl text"] --> P["tdl: parse"]
|
|
19
|
+
P --> D["Document tree\n(lossless, with positions)"]
|
|
20
|
+
D --> S["Shell: schema check,\ntyping, Import, UIDs"]
|
|
21
|
+
S --> E["TEL: compile each slot"]
|
|
22
|
+
E --> M["Shell model"]
|
|
23
|
+
M --> R["tre nodes and windows"]
|
|
24
|
+
D -. "edit (UID)" .-> W["tdl: dumps"]
|
|
25
|
+
W --> F[".tdl text, unchanged\nexcept the edit"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The tree is not something tre can render. It describes the file. The shell turns it into its own model and only then into tre nodes (Layers and Components).
|
|
29
|
+
|
|
30
|
+
## Behaviour
|
|
31
|
+
|
|
32
|
+
### The Entry Points
|
|
33
|
+
|
|
34
|
+
| Function | Does | Familiar from |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `tdl.loads(text, *, path=None, limits=None)` | Parses a string. Returns a `Document`. Raises `TDLError` on the first syntax error | `json.loads`, `tomllib.loads` |
|
|
37
|
+
| `tdl.load(fp, *, path=None, limits=None)` | Same, from a binary file object the caller opened (UTF-8; a byte-order mark is accepted). The library never opens a path itself | `tomllib.load` |
|
|
38
|
+
| `tdl.parse(text, *, path=None, limits=None)` | Parses and never raises for a syntax error. Returns a `ParseResult(document, errors)`: the best tree it could build (`None` when the metadata or root block cannot be read, a limit stops the file or the cap is reached) and every error found, in source order, up to `Limits.max_errors`. For editors and for the shell's "collect every error, then stop" rule | `ast.parse` (but tolerant) |
|
|
39
|
+
| `tdl.dumps(document)` | Returns the text of a document. For a document from `loads`, the result is exactly the input, byte for byte | `json.dumps` |
|
|
40
|
+
| `tdl.dump(document, fp)` | Writes that text to a binary file object | `json.dump` |
|
|
41
|
+
|
|
42
|
+
`path` is a label for error messages (`Top_Bar_Module.tdl`). It is never opened.
|
|
43
|
+
|
|
44
|
+
### Errors
|
|
45
|
+
|
|
46
|
+
One base class, `TDLError`, with a subclass `TDLSyntaxError`. Every error has:
|
|
47
|
+
|
|
48
|
+
| Field | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `code` | A stable slug such as `tdl-unclosed-block` ([TDL Language > Errors](language.md), D52) |
|
|
51
|
+
| `message` | One sentence saying what is wrong, in plain words |
|
|
52
|
+
| `hint` | What to write instead |
|
|
53
|
+
| `origin` | An `Origin`: `file`, `line`, `column` (both 1-based), and `end_line`, `end_column`, `offset` |
|
|
54
|
+
| `related` | Other `Origin`s the error mentions; a duplicate key names both places |
|
|
55
|
+
|
|
56
|
+
`str(error)` is `file:line:column: message (hint)`. The message follows the wording rules in [Wording and Familiar Names](#wording-and-familiar-names). The error list is the one in [TDL Language > Errors](language.md): the 17 accepted in D52, `tdl-unexpected-text` (G13) and four limit and input codes (`tdl-too-large`, `tdl-too-deep`, `tdl-unsupported-version`, `tdl-bad-encoding`, G18), all accepted by the maintainer on 2026-11-02, 22 in all.
|
|
57
|
+
|
|
58
|
+
> [!NOTE]
|
|
59
|
+
> **The suffix check is the shell's (maintainer, 2026-11-02, D91)**
|
|
60
|
+
> The library does not look at file names. Comparing the root block with the suffix (`_Shell.tdl`, `_Module.tdl`) is done by the shell, or by Tesserae UI for its own types, and the shell's error is `shell-root-suffix`. The library keeps 18 language errors and four limit and input codes (22 in all).
|
|
61
|
+
|
|
62
|
+
### Limits
|
|
63
|
+
|
|
64
|
+
`tdl.Limits(max_bytes=1_048_576, max_errors=50, max_depth=64)`. All three are decided: 1 MiB and 50 errors (Validation and Errors > Parsing) and depth 64, counting the root block as 1 (G11). A file over a limit stops with `tdl-too-large` or, for depth, `tdl-too-deep`. TEL uses a `Limits` class the same way.
|
|
65
|
+
|
|
66
|
+
### The Tree
|
|
67
|
+
|
|
68
|
+
All classes are importable from `tdl`. Nodes are immutable: they can be shared between threads, and an edit makes a new document.
|
|
69
|
+
|
|
70
|
+
| Class | Fields | Notes |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `Document` | `metadata`, `root`, `path`, `text`, `language_version` | `text` is the source |
|
|
73
|
+
| `Metadata` | mapping from key to `Value`; `origin` | Keeps unknown keys. Reading is by key: `doc.metadata["Name"]` |
|
|
74
|
+
| `Block` | `type`, `entries`, `origin`, `name` | `type` is the word before the brace. `name` is the value of `Name:` or `None` |
|
|
75
|
+
| `Property` | `key`, `value`, `key_origin`, `value_origin` | `Key: value` |
|
|
76
|
+
| `If` | `condition: Slot`, `body`, `else_body` (or `None`), `origin` | `If {{ c }} { } Else { }` |
|
|
77
|
+
| `For` | `variable: str`, `items: Slot`, `key: Slot`, `body`, `name`, `origin` | `For x In {{ xs }} Key {{ x.id }} { }` |
|
|
78
|
+
| `Comment` | `text`, `origin` | A `#` comment, kept where it stands |
|
|
79
|
+
|
|
80
|
+
`Block.entries` holds `Property`, `Block`, `If`, `For` and `Comment` in source order. Helpers: `Block.properties` (a mapping by key; a repeated key is an error, D9), `Block.blocks` (child blocks only), `Block.get(key, default=None)`, `Block.walk()` (every descendant), and `Document.find("Shell_Root.Main.Top_Bar")` (a dotted path of `Name:` values, D28).
|
|
81
|
+
|
|
82
|
+
`Children` is an ordinary block named `Children`; `Import: file` is an ordinary property. `Document.imports()` lists the names so the shell does not search for them, and the library does not open them.
|
|
83
|
+
|
|
84
|
+
### Values
|
|
85
|
+
|
|
86
|
+
The library gives each value a lexical kind and the text it came from. It does not decide what the text means; that is the schema's job in the shell (a proposal, [Open Questions](#open-questions) 1).
|
|
87
|
+
|
|
88
|
+
| Class | Written as | Fields |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `Word` | `TOP`, `true`, `Top_Bar`, `Battery` (unquoted) | `text`. Booleans, `null` and enums are words until the schema types them (D2, D33) |
|
|
91
|
+
| `String` | `"..."` or `'...'` | `value` (escapes resolved), `text` (as written) |
|
|
92
|
+
| `Number` | `16`, `-4`, `0.9` | `value` (`int` or `float`), `text` |
|
|
93
|
+
| `Percent` | `50%` | `value` (`int`), `text` |
|
|
94
|
+
| `Call` | `rgba(0 0 0 0.5)` | `name`, `args` (values separated by spaces) |
|
|
95
|
+
| `List` | `[a, b, c]` | `items` |
|
|
96
|
+
| `Slot` | `{{ expression }}` | `text` (what is between the braces), `text_origin` (where that text starts, for TEL), `origin` |
|
|
97
|
+
| `Template` | `Battery {{ battery.percent }}%` | `parts`: strings and `Slot`s, in order |
|
|
98
|
+
|
|
99
|
+
A value that is exactly one slot is a `Slot`; text with a slot inside is a `Template`, matching the rule in [TDL Language > Values](language.md).
|
|
100
|
+
|
|
101
|
+
`Slot.text_origin` is built to be handed to TEL. The library does not import TEL. The shell converts: `tel.Origin(file=o.file, line=o.line, column=o.column)`.
|
|
102
|
+
|
|
103
|
+
### Editing
|
|
104
|
+
|
|
105
|
+
A document never changes in place. An edit returns a new `Document` whose text equals the old text with one change.
|
|
106
|
+
|
|
107
|
+
| Call | Does |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `document.with_metadata(key, value)` | Sets a metadata key. A new `UID` goes directly after the last version line (`TDL` or `Shell`, D10, G24); any other new key goes just before the closing `---`; an existing key changes only its value. `value` is plain text: a bare word when it reads back the same, else a quoted string (G32) |
|
|
110
|
+
| `document.without_metadata(key)` | Removes the whole line of a metadata key; unchanged if the key is absent |
|
|
111
|
+
| `block.with_property(key, value)` | Sets a property and returns the new `Document`. `value` is the TDL text of one value on one line. An existing property changes only its value; a new one goes on a line after the last property with that property's indentation and line ending, or inline in a block written on one line (G24, G32) |
|
|
112
|
+
| `document.replace(start, end, text)` | Replaces `document.text[start:end]` (character offsets) and reparses; the escape hatch |
|
|
113
|
+
| `document.find(path)` | The block at a dotted path of `Name:` values; the first name is the file's own `Name` and stands for the root block. Raises `KeyError` if there is none (D28, G32) |
|
|
114
|
+
|
|
115
|
+
Every edit keeps comments, blank lines, indentation and the order of everything it did not touch (this is what the UID write needs; R-06 in Risk Register). An edit that would make the file invalid raises `TDLError` and returns nothing. A call that is wrong whatever the file holds (a key that is not a name, a reserved word as a property key, a value that is not one value, a span outside the text, a block built in code) raises `ValueError`. Edits reread the result with the default `Limits`.
|
|
116
|
+
|
|
117
|
+
### Plain Data View
|
|
118
|
+
|
|
119
|
+
`document.to_data()` and `block.to_data()` return plain dicts, lists and strings, with no comments or positions, for tools and tests that only need the content. The shape is the library's first-slice decision. The tree is the main API; this is a convenience.
|
|
120
|
+
|
|
121
|
+
### Building a Tree in Code
|
|
122
|
+
|
|
123
|
+
The Python builder (Python Builder) must produce the same tree as the parser, so the nodes can be constructed directly: `Block("Bar", [Property("Edge", Word("TOP"))])`. A constructed node has no position. `tdl.dumps` formats a constructed tree with the library's standard layout (two-space indent). `node.canonical()` returns a normalised plain structure with trivia and positions dropped, so a parity test compares two trees by `==`. Names follow TEL's `canonical` (Expression Language Requirements > TEL 0.1.0 Released (2026-11-02)). Whether the formatter is in v0.1 is open.
|
|
124
|
+
|
|
125
|
+
### Wording and Familiar Names
|
|
126
|
+
|
|
127
|
+
The maintainer's rule (2026-11-02): the names and the prose are for people from the UI design world, who expect a thing to behave as it does in another framework. In this library that means:
|
|
128
|
+
|
|
129
|
+
- Function names come from `json` and `tomllib` (`loads`, `load`, `dumps`, `dump`), and node names from the file's own words (`Block`, `Property`, `Comment`), not from compiler jargon (`Token`, `Production`, `Lexeme` stay private).
|
|
130
|
+
- An error message says what happened in one plain sentence and the hint says what to write. It names the thing the author wrote, not the parser state: "This colour needs quotes. Write "#ffffff"." and not "unexpected token HASH".
|
|
131
|
+
- The language reference in `tess-dl` explains each construct with a short example before any rule, and says where TDL behaves differently from CSS, QML or Flutter (Familiar Names Audit when written).
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
None. The only inputs are the arguments above and the `Limits`.
|
|
136
|
+
|
|
137
|
+
## Python Contract
|
|
138
|
+
|
|
139
|
+
| Item | Value |
|
|
140
|
+
|---|---|
|
|
141
|
+
| Import | `import tdl`; every public name is in `tdl.__all__` |
|
|
142
|
+
| Version | `tdl.__version__` (package) and `tdl.LANGUAGE_VERSION` (`"0.1"`); a file with a newer major version raises `tdl-unsupported-version` |
|
|
143
|
+
| Python | 3.14 or later; no runtime dependencies (maintainer, 2026-11-02) |
|
|
144
|
+
| Typing | Fully typed; ships `py.typed` |
|
|
145
|
+
| Threads | Pure functions on immutable data: safe to call from any thread; the shell may parse on the watcher thread and hand the `Document` to the loop with `call_soon` (Process Model and Threading) |
|
|
146
|
+
| Stability | Before 1.0, a minor version may change the API; every change is listed in the changelog and in a `TDL-REQ-###` when the shell asks for it (AGENTS > 6. Requests Between Projects) |
|
|
147
|
+
| Private | Anything not in `__all__`, and the whole lexer |
|
|
148
|
+
|
|
149
|
+
## Integration
|
|
150
|
+
|
|
151
|
+
- The shell calls `tdl.parse` for every file at load, collects the errors, then walks the tree with its schema (Validation and Errors).
|
|
152
|
+
- The shell calls `document.with_metadata("UID", uuid)` and writes the result with its own file code when it registers a file (Config Discovery and Registry).
|
|
153
|
+
- TEL receives `slot.text` and an origin built from `slot.text_origin`; the shell adds nothing to the position because a slot sits on one line (Validation and Errors).
|
|
154
|
+
- A TextMate grammar and any later editor tool use `tdl.parse` and the codes.
|
|
155
|
+
|
|
156
|
+
## Design
|
|
157
|
+
|
|
158
|
+
None. This is a library without a user interface. The language it reads is designed in [TDL Language](language.md) and, after the move, in the `tess-dl` repository.
|
|
159
|
+
|
|
160
|
+
## Accessibility
|
|
161
|
+
|
|
162
|
+
Error messages are plain sentences that a screen reader reads whole: no colour or symbol carries meaning, and the position is in text (`file:line:column`).
|
|
163
|
+
|
|
164
|
+
## Performance Budget
|
|
165
|
+
|
|
166
|
+
- Parsing is one pass over the text. Budget: a 100 KB file in under 20 ms and a file at the 1 MiB limit in under 250 ms. Measured on 2026-11-03 (Python 3.14.8, AMD Ryzen AI 9 HX 370): 13.1 ms and 140.3 ms, growing linearly; no adversarial input is super-linear. The tree the reader keeps is about 19 times the size of the text. The figures, the method and the command are in [benchmarks.md](benchmarks.md); Performance Budgets in the vault holds the ledger.
|
|
167
|
+
- `loads` of the same text twice gives equal documents; a cache is the shell's business (a warm cache keys on the file's hash, as the schema cache does).
|
|
168
|
+
- The library allocates no thread and no timer.
|
|
169
|
+
|
|
170
|
+
## Failure Modes and Recovery
|
|
171
|
+
|
|
172
|
+
| Failure | Result |
|
|
173
|
+
|---|---|
|
|
174
|
+
| Invalid UTF-8 in `load` | `TDLSyntaxError` `tdl-bad-encoding`, with the byte offset |
|
|
175
|
+
| Syntax error | `loads` raises at the first one it meets; `parse` records it and carries on (G31, [TDL Language > Errors](language.md)): a bad entry is skipped to the end of its line or the `}` that closes its block |
|
|
176
|
+
| More than 50 errors | The list stops at the limit, the last entry says `The list stops at 50 errors.`, and nothing more is read |
|
|
177
|
+
| An edit would break the file | `TDLError`; the original document is untouched |
|
|
178
|
+
| A newer language version | `tdl-unsupported-version`, with the library's version in the message |
|
|
179
|
+
|
|
180
|
+
## Open Questions
|
|
181
|
+
|
|
182
|
+
1. [x] **Values stay lexical** (maintainer, 2026-11-02, "yes", D74). The reasoning was: the library returns `Word`, `String`, `Number`, `Percent`, `Call`, `List`, `Slot` and `Template`, and the schema in the shell types them. The alternative is for the library to know lengths and colours, which makes it carry shell meaning.
|
|
183
|
+
2. [x] The suffix check is the shell's, as `shell-root-suffix` (maintainer, 2026-11-02, D91).
|
|
184
|
+
3. [ ] Whether the formatter and `canonical()` are in the first release or follow when the Python builder starts.
|
|
185
|
+
4. [ ] The shape of `to_data()`.
|
|
186
|
+
5. [x] The extra codes `tdl-unsupported-version`, `tdl-too-large`, `tdl-too-deep` and `tdl-bad-encoding` are accepted (maintainer, 2026-11-02, G18), with `tdl-unexpected-text` (G13).
|
|
187
|
+
6. [ ] A `tdl` command (`tdl check file.tdl`, `tdl format`) for authors and editors: recommended after the first release, not in it.
|
|
188
|
+
7. [ ] Build tooling and CI for `tess-dl` (the TEL project's choices are the starting point).
|
|
189
|
+
|
|
190
|
+
## Acceptance Criteria
|
|
191
|
+
|
|
192
|
+
- [x] `tdl.dumps(tdl.loads(text)) == text` for every file in the conformance corpus (comments, blank lines, indentation, a final newline or none). *Tests:* `test_corpus.py::test_ok_case_round_trips_byte_for_byte` (54 files), `test_roundtrip.py` (generated files).
|
|
193
|
+
- [x] The 22 error codes each have a corpus file that raises it with the stated `line` and `column`. *Tests:* `test_corpus_files.py::test_every_code_has_a_case`, `test_corpus.py::test_error_case_loads_raises_the_first_listed_error` and `..._load_raises_the_same_first_error`; `tdl-too-large` and `tdl-too-deep` are built in `test_limits.py`.
|
|
194
|
+
- [x] `Width 32` raises `tdl-unexpected-text`, pointing at the first character that does not fit. *Tests:* corpus case `errors/unexpected-text-no-colon`; `test_acceptance.py`.
|
|
195
|
+
- [x] `tdl.parse` on a file with six deliberate mistakes returns six errors with the right positions (JSON Schema and Editor Support). *Test:* corpus case `errors/six-mistakes` through `test_error_case_parse_lists_every_listed_error`.
|
|
196
|
+
- [x] `with_metadata("UID", x)` puts `UID` directly after the last version field and changes no other byte. *Tests:* corpus cases `edits/uid-after-last-version`, `uid-after-tdl-only`; `test_edits.py` (one changed span, generated files).
|
|
197
|
+
- [x] `Color: #fff` raises `tdl-bare-hash`; `Color: "#fff" # note` is a `String` and a `Comment`. *Tests:* `test_acceptance.py`; corpus cases `errors/bare-hash`, `strings/hash-and-comment`.
|
|
198
|
+
- [x] `Text: {{ max(a, b) }}` is one `Slot` with text ` max(a, b) `; `{{ '}}' }}` ends at the second `}}`; `{{ {'a': {'b': 1}} }}` ends at the first `}}` and leaves the rest to TEL. *Tests:* `test_acceptance.py`; corpus cases `slots/one-slot`, `slots/quoted-close`, `errors/slot-ends-at-first-close`.
|
|
199
|
+
- [x] `}}` where two blocks close raises `tdl-double-close` and the hint says `} }`. *Tests:* `test_acceptance.py`; corpus case `errors/double-close`.
|
|
200
|
+
- [x] A file at 1 MiB parses within the budget; one byte over raises `tdl-too-large`. *Tests:* `test_limits.py` (exact size, one byte more, growth ratio); the measured time is in [benchmarks.md](benchmarks.md).
|
|
201
|
+
- [x] Importing `tdl` imports no other third-party package, and `tdl.__all__` lists every public name. *Tests:* `test_acceptance.py` (a clean interpreter with `-I`, and the list of public names).
|
|
202
|
+
- [ ] A tree built in code and the same file parsed give equal `canonical()` results. *Not in 0.1:* building a tree in code, the formatter and `canonical()` on such a tree wait for open question 3 (G26); `canonical()` on a parsed document is in 0.1.
|
|
203
|
+
|
|
204
|
+
## Links
|
|
205
|
+
|
|
206
|
+
- 42 Python API · ADR-0014 Shell File Language · [TDL Language](language.md) · Python Builder · Validation and Errors · Data Flow · Risk Register
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
Measured against the budget in [api.md](api.md#performance-budget): a 100 KB file in under 20 ms and a file at the 1 MiB limit in under 250 ms. The script is `benchmarks/bench.py` (standard library only, not shipped).
|
|
4
|
+
|
|
5
|
+
## Run of 2026-11-03
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Python | 3.14.8 |
|
|
10
|
+
| Machine | AMD Ryzen AI 9 HX 370 (24 threads), Linux 7.3 (CachyOS), idle |
|
|
11
|
+
| Code | commit `5f3da64` (step 7), `benchmarks/bench.py` as added in step 8 |
|
|
12
|
+
| Command | `~/Projects/pyDev/pyEnvs/tess-dl/bin/python benchmarks/bench.py --runs 15` |
|
|
13
|
+
| Method | best of 15 timed runs after one warm-up, garbage collector off while timing; memory by `tracemalloc` after `loads` returns |
|
|
14
|
+
|
|
15
|
+
### Budget
|
|
16
|
+
|
|
17
|
+
| Input | Size | `loads` | Budget | |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| Realistic file (monitors, bars, slots, `If`/`For`, comments) | 99,749 B | 13.11 ms | 20 ms | within |
|
|
20
|
+
| The same mix at the limit | 1,048,522 B | 140.30 ms | 250 ms | within |
|
|
21
|
+
|
|
22
|
+
Other calls on the same two files: `parse` 12.93 ms and 139.93 ms (the same as `loads` when there are no errors); `with_metadata` 13.09 ms and 140.78 ms (an edit rereads the whole file, so it costs one `loads`); `dumps` under 0.01 ms (it returns the stored text). The tree the reader keeps is about 19 times the size of the text (1,859 KiB for 100 KB, 19,429 KiB for 1 MiB).
|
|
23
|
+
|
|
24
|
+
### Scaling
|
|
25
|
+
|
|
26
|
+
| Input | 1x (99,749 B) | 2x | 4x |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| Realistic file, `loads` | 13.20 ms | 26.48 ms (x2.01) | 53.97 ms (x4.09) |
|
|
29
|
+
|
|
30
|
+
Growth is linear.
|
|
31
|
+
|
|
32
|
+
### Adversarial inputs
|
|
33
|
+
|
|
34
|
+
About 1 MB each; `parse` time at 250 KB, 500 KB and 1 MB, and the ratio per doubling (linear is x2).
|
|
35
|
+
|
|
36
|
+
| Input | 250 KB | 500 KB | 1 MB | Per doubling |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| Many `{{ a }}` on one line | 35.68 ms | 72.68 ms | 145.42 ms | x2.04, x2.00 |
|
|
39
|
+
| One very long word | 3.03 ms | 5.83 ms | 11.46 ms | x1.92, x1.96 |
|
|
40
|
+
| One long string | 0.25 ms | 0.45 ms | 0.86 ms | x1.77, x1.94 |
|
|
41
|
+
| A list of 330,000 numbers | 139.28 ms | 278.67 ms | 558.31 ms | x2.00, x2.00 |
|
|
42
|
+
| A long run of `}` | 0.13 ms | 0.14 ms | 0.14 ms | the cap stops it after 50 errors |
|
|
43
|
+
| Many small blocks | 52.11 ms | 105.89 ms | 214.13 ms | x2.03, x2.02 |
|
|
44
|
+
| Many comments | 50.10 ms | 100.67 ms | 203.12 ms | x2.01, x2.02 |
|
|
45
|
+
| Many mistakes, one per line (cap) | 3.13 ms | 6.14 ms | 12.46 ms | x1.96, x2.03 (the line index is built over the whole text) |
|
|
46
|
+
| Mistakes on one line | 0.24 ms | 0.45 ms | 0.84 ms | x1.85, x1.88 (one error: the rest of a bad line is skipped) |
|
|
47
|
+
| 64 levels of blocks | 0.11 ms | 0.11 ms | 0.11 ms | a size cannot grow it |
|
|
48
|
+
|
|
49
|
+
No input is super-linear. The slowest, a list of 330,000 numbers at 558 ms, is over the 250 ms figure but that figure is for a realistic file; it grows linearly and the file is at the size limit.
|
|
50
|
+
|
|
51
|
+
## What was not done
|
|
52
|
+
|
|
53
|
+
No tuning: every budget is met, so nothing was changed for speed (`perf-baseline`: no tuning before a number misses). The vault's "Performance Budgets" ledger is the maintainer's; the figures above are for a `TDL-REQ` to update it.
|
|
54
|
+
|
|
55
|
+
`tests/test_limits.py` pins the guards with generous ceilings (a 1 MiB file reads in under 3 s, about 20 times the measured time) so a quadratic step would fail the suite without making it flaky.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Design decisions
|
|
2
|
+
|
|
3
|
+
Decisions made for the language and the library, newest last. The reason is kept with each one. Syntax, grammar and boundary changes are recorded here, in this repo, not left in the vault.
|
|
4
|
+
|
|
5
|
+
`G` rows answer the 12 grammar ambiguities of the First Slice note (vault: `41 TDL Reference/tess-dl First Slice.md`). The note leaves them to the `tess-dl` session: each takes the note's recommendation unless the Notes column says otherwise. The maintainer answered the open questions on 2026-11-02; they are G13 to G18.
|
|
6
|
+
|
|
7
|
+
## 2026-11-02 (grammar, plan step 1)
|
|
8
|
+
|
|
9
|
+
| # | Decision | Notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| G1 | **An unquoted value is one kind, `Word`**, whose text is the whole run (spaces inside kept, outer whitespace dropped). A word whose text is a number is a `Number`. | The rule "an unquoted value runs to the end of the line, the next `,`, a closing brace or a comment" implies one run. `Hello world` is one word; `1st` is a word; `16` is a number. |
|
|
12
|
+
| G2 | **In the metadata block every bare value is a `Word`**, never a number. `0.1` is the text `0.1`. | Versions are strings (ADR-0014 layout rule 3): `0.10` and `0.1` must not be the same value. |
|
|
13
|
+
| G3 | **Numbers are `16`, `-4`, `0.9`**: an optional `-`, digits, optionally `.` and digits. No exponent, hex, digit separators, leading `+`, `.5` or `5.`. A word that only starts with digits (`16px`) is a word. | Smallest set the shell's lengths and opacities need. More can be added later without breaking a file. |
|
|
14
|
+
| G4 | **`Call` starts at any word written directly against `(`**; the arguments are values separated by spaces, to the `)`, on one line. | The shell, not the parser, knows which call names exist, so it can say "unknown colour function". |
|
|
15
|
+
| G5 | **String escapes:** `\\`, `\"`, `\'`, `\n`, `\t`, `\u` plus four hex digits. Anything else is `tdl-bad-escape`. | As recommended. |
|
|
16
|
+
| G6 | **Line endings, tabs, BOM:** `\n` and `\r\n` are both accepted and each line keeps its own; a tab is whitespace; a leading BOM is accepted and kept; a lone `\r` is plain whitespace; a `---` line may have trailing spaces or tabs. | A lossless tree must keep all of them. The lone `\r` and trailing-space details go beyond the recommendation; they are the least surprising reading. |
|
|
17
|
+
| G7 | **A slot is on one line.** A line end before `}}` is `tdl-unclosed-slot`. | D54 of the vault's ADR-0014. |
|
|
18
|
+
| G8 | **A list may span lines;** items are separated by `,` or a newline, with an optional trailing `,`. | As recommended; matches how entries are separated in a block. |
|
|
19
|
+
| G9 | **`Children` is an ordinary block** and `Import` an ordinary property. Only `If`, `Else`, `For`, `In` and `Key` are reserved. | The parser stays free of the shell's meaning. |
|
|
20
|
+
| G10 | **Key characters:** a letter, then letters, digits and `_`. The same rule serves block heads, `For` variables and `Name:` values. | D4 forbids numeric keys. One rule for every name keeps `tdl-bad-name` simple. |
|
|
21
|
+
| G11 | **Maximum nesting depth is 64**, counting the root block as 1; blocks, `If`/`For` bodies and lists each add a level. | Proposed by the note. A stack-safe parser must reach it without a `RecursionError`. |
|
|
22
|
+
| G12 | **Trivia is kept in source spans, not modelled.** Comments are entries in their block in source order; blank lines and indentation live in the spans between nodes. | Keeps the tree small and makes the exact round trip a property of the spans (`lossless-roundtrip`). |
|
|
23
|
+
| G13 | **One more language code: `tdl-unexpected-text`**, for text that is not valid where it stands: a missing colon (`Width 32`), a key with no value, an `Else` with no `If`, a `For` without `In` or `Key`, an unclosed `(` in a call, a stray `{` in an unquoted value. The message and hint say which case. | Answers Q1 (maintainer, 2026-11-02). It makes 18 language errors; the vault's "17" and its `TDL Language` error table need the new row. |
|
|
24
|
+
| G14 | **In value position, a `#` followed directly by a non-space character is `tdl-bare-hash`;** a `#` followed by whitespace or the end of the line is a comment. | Answers Q2 (maintainer). `Color: #fff` is an error; `Color: # note` is an empty value (G16) and a comment. |
|
|
25
|
+
| G15 | **`{{ }}` inside a quoted string is plain text.** A quoted string never holds a slot; a template is written unquoted. | Answers Q3 (maintainer). Quotes switch slots off. |
|
|
26
|
+
| G16 | **A property with no value is an error** (`tdl-unexpected-text`). | Answers Q4 (maintainer). |
|
|
27
|
+
| G17 | **`Else` follows the `If` body's closing brace,** on the same line or after newlines and comments. | Answers Q5 (maintainer). |
|
|
28
|
+
| G18 | **The four extra codes are accepted:** `tdl-too-large` (over 1 MiB, checked before parsing), `tdl-too-deep` (over 64 levels), `tdl-unsupported-version` (the `TDL:` version is newer than the library's), `tdl-bad-encoding` (invalid UTF-8 in `load`, with the byte offset). | Answers Q6 (maintainer). Together with G13 the library has 22 codes. |
|
|
29
|
+
|
|
30
|
+
## 2026-11-02 (corpus, plan step 2)
|
|
31
|
+
|
|
32
|
+
| # | Decision | Notes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| G19 | **`}}` rule.** An adjacent `}}` outside a slot and outside a string is always `tdl-double-close`, pointing at the first `}` of the pair, whatever the number of open blocks. | Keeps the rule lexical. `{{ {'a': {'b': 1}} }}` ends the slot at the first `}}` and the trailing `}}` is then `tdl-double-close`. |
|
|
35
|
+
| G20 | **Positions of the two vague "points at" rows.** `tdl-no-root` points at the first character after the closing `---` line, including its line ending (5:1 when `---` is line 4 and ends with `\n`; 4:4 if the file ends right after the `---`). `tdl-unexpected-text`: a missing value, at the first non-whitespace character after the `:` (the `#` of a comment, or the line end); a key with no `:`, at the first character after the key and its whitespace; an unclosed `(`, at the line end; `For` without `Key`, at the `{` that opens the body; `For` without `In`, at the first `{{` after the variable; `Else` without `If`, at the `Else`; a stray `{` in an unquoted value, at that `{`. | The note leaves both open; each position is the first character that does not fit. |
|
|
36
|
+
| G21 | **`tdl-bad-encoding` position.** Line and column are computed from the valid UTF-8 prefix before the first bad byte (line = 1 + newlines in the prefix; column = 1 + characters since the last newline). | The bad byte itself cannot be counted in characters. |
|
|
37
|
+
| G22 | **`canonical()` shape** (what `expect.json` `"canonical"` holds): `{"metadata": {KEY: VALUE, ...}, "root": BLOCK}`, comments dropped. Value: `{"word": text}`, `{"string": resolved_value}`, `{"number": 16}` (JSON int) or `{"number": 0.9}` (JSON float), `{"percent": 50}`, `{"call": name, "args": [value, ...]}`, `{"list": [value, ...]}`, `{"slot": text}` (exactly as between the braces), `{"template": [{"text": "Battery "}, {"slot": " battery.percent "}, {"text": "%"}]}`. Entry: `{"block": type, "entries": [entry, ...]}`, `{"property": key, "value": value}`, `{"if": slot_value, "body": [entry...], "else": [entry...] or null}`, `{"for": variable, "in": slot_value, "key": slot_value, "body": [entry...]}`. A metadata value is a `word` or a `string`. The runner compares `json.dumps(x, sort_keys=True)` of both sides, so `16` and `16.0` differ. | One shape for the runner and the shell to read; no positions and no comments. |
|
|
38
|
+
| G23 | **Edit cases** are three files: `<case>.tdl`, `<case>.expect.json` holding `{"edit": {"call": ..., ...}}`, and `<case>.result.tdl`. Calls: `with_metadata` (`key`, `value`), `without_metadata` (`key`), `with_property` (`block`: dotted path of `Name:` values as `Document.find` takes, `key`, `value`: the TDL text of the value), `replace` (`start`, `end`: character offsets, `text`). Every call returns a `Document`; `with_property` is run as `doc.find(block).with_property(key, value)` and the returned `Document` is the result. A refused edit is `{"edit": {...}, "raises": "tdl-no-name"}` with no `.result.tdl`. | The vault says an edit returns a new Document. `replace` offsets and the `with_property` return type are the choices the maintainer may want to change. |
|
|
39
|
+
| G24 | **Metadata insert rules for edits.** `with_metadata("UID", v)` with no `UID` inserts `UID: v` on a new line directly after the last metadata line whose key is `TDL` or a version key (`Shell`). With a `UID` present only the value bytes change. Any other new key goes on a new line directly before the closing `---`. A new line has no indentation and takes the line ending of the line before it. `with_property` adds `Key: value` on a new line after the last property of the block, with that property's indentation and the file's line ending there. | Keeps an edit to the bytes it must change (`lossless-roundtrip`). |
|
|
40
|
+
| G25 | **Cap entry.** With `Limits(max_errors=50)`, a file with 60 mistakes gives `len(result.errors) == 50` and the message of the last entry contains the text `50`. | The vault says only "the last entry says so". |
|
|
41
|
+
| G26 | **Step 3 follows the brief's row boundary.** Step 3 is the tree, the parser and `canonical()` on a parsed document; it is done when `loads` plus `canonical()` match for every `ok` case (`test_ok_case_reads_to_the_canonical_tree`). `dumps`, `load` and the byte-for-byte round trip are step 5 (`test_ok_case_round_trips_byte_for_byte`). `canonical()` on a document read from text is in the first release because the corpus needs it; the formatter and `canonical()` on a tree built in code stay out until API Open Question 3 is answered. | The brief lists `canonical()` under "Not in the First Slice", meaning the Python-builder use. The corpus is read by the shell and needs it on parsed trees. |
|
|
42
|
+
| G27 | **Calls count as nesting levels,** like blocks, bodies and lists: `f(f(f(...)))` deeper than 64 is `tdl-too-deep` at the `f` that passes the limit. | A call can nest inside a call, so without a level count a long run of `f(` would overflow Python's stack. G11 listed only blocks, bodies and lists. |
|
|
43
|
+
| G28 | **Step 4 follows the brief's row boundary.** `loads` is the step 4 check: it raises the first listed error with its code, position, related origins, message and hint (`test_error_case_loads_raises_the_first_listed_error`, all text cases). `tdl-bad-encoding` needs bytes, so its error is built by a private decoder in step 4 and `load` is wired to it in step 5 (`test_error_case_load_raises_the_same_first_error`). The full list through `parse` is step 7 (`test_error_case_parse_lists_every_listed_error`). | The brief says step 4 is done when the error cases pass, but `load` is step 5 and `parse` step 7; one test per step keeps each step checkable on its own. |
|
|
44
|
+
| G29 | **Every `tdl-unexpected-text` kind is documented and has a corpus case.** The reader had 24 places that raise it; the list in `language.md` now names all the kinds and where each points (G20 extended), and `tests/corpus/errors/` has a case for each. Also: a `#` or `,` inside a call is an error (a `#` followed by a non-space is `tdl-bare-hash`); an unclosed `[` points at the `}` or end of file that reaches it; a `TDL:` value that does not start with a number is `tdl-unsupported-version`. | Step 4's audit. These are positions and kinds, not new codes, so they stay inside the session's decisions. |
|
|
45
|
+
| G30 | **`dumps` returns the source text; `load` reads at most `max_bytes` + 1 bytes; a `str` that cannot be UTF-8 is `tdl-bad-encoding`.** `dumps(doc)` is `doc.text` (no re-rendering), `dump` writes it as UTF-8 with the byte-order mark kept. `load` refuses a longer input as `tdl-too-large` without reading it whole. `loads` on text with a lone surrogate raises `tdl-bad-encoding` at that character instead of `UnicodeEncodeError`. | The API promises `loads` raises only `TDLError`; a Python `str` can hold characters that no UTF-8 file can (found in step 4). The code's wording said "`load` only", which now means bytes or text that cannot be UTF-8. |
|
|
46
|
+
| G32 | **How the edit calls behave where G23 and G24 are silent.** (1) A metadata `value` is plain text: written as a bare word when it reads back the same (non-empty, no outer space, no leading quote or `#`, no line end, no ` #`), else as a double-quoted string with `\\`, `\"`, `\n`, `\t` and `\u000d` escapes. (2) `with_property` takes the TDL text of exactly one value on one line; it is checked by reading it alone, so `1 }`, `a, b` and `1 # c` are refused with `ValueError`. (3) `with_property` adds inline (`Panel { A: 1, H: 40 }`) when the last property shares its line with something other than a comment, puts a first property after the `{` line with the first entry's indentation (or the head's plus two spaces), and inline after the `{` in a one-line block. (4) `Document.find(path)`: the first name is the metadata `Name` and stands for the root block; each next name is the `Name:` of a block directly inside the one before; the first match wins; none raises `KeyError`. (5) `without_metadata` of an absent key returns the same document. (6) A call that is wrong whatever the file holds raises `ValueError`; a result that is not a valid file raises `TDLError`. | G23 and G24 give the corpus cases; these are the rules the cases leave open, chosen so an edit can never silently change anything but the named bytes. |
|
|
47
|
+
| G31 | **How `parse` recovers.** `parse` returns `ParseResult(document, errors)` with the errors in source order. A bad entry is skipped to the end of its line or to the `}` that closes its block, skipping braces the bad line opened itself; a bad metadata line is skipped and the next read; `}}` is reported once and closes up to two open blocks; each unclosed `{` is reported when the file ends; a second root block is reported and read; the limit errors stop the file with one error. The cap keeps the first 50 errors found; the 50th says `The list stops at 50 errors.` only when there was a 51st; nothing is read after it. `document` is the tree without the bad entries, or `None`. `loads` stops at the first mistake it meets, which `parse` lists too. The corpus case `slot-ends-at-first-close` now lists two errors: after `{{ {'a': {'b': 1}} }}` the trailing `}}` closes the root block and the file's last `}` is a stray one. | G25 left open how the cap is marked and what the tree holds. Cascades from one mistake are kept small by balanced-brace skipping; a mistake that opens a block on its own line (`If { `) can still cascade, which the cap bounds. |
|
|
48
|
+
| G33 | **The conformance corpus ships in the source distribution, not in the wheel.** `tests/` (with `tests/corpus/`) is in the sdist; the wheel holds only `src/tdl`, `py.typed` and the licence. The shell reads the corpus of a released version from the sdist or the tag. | A wheel that installed `tests/corpus/` would put a top-level `tests` directory in every environment. The First Slice brief says the shell installs the package's test data or copies a released version; this takes the second. The maintainer may prefer a separate `tesserae-dl-corpus` package. |
|
|
49
|
+
|
|
50
|
+
No questions are open.
|