v0.19: Replace SXML with typed XML and HTML modules #1515

Open
jay wants to merge 95 commits from jay/gerbil:v0.19-sxml-to-typed-xml into v0.19-staging
Member

Replaces SXML in the markup stack with typed XML and HTML modules, as discussed in #1509. SXML stays available, deprecated.

Changes

  • :std/text/markup/xml: typed XML nodes (XMLElement, XMLText, … under an XML base), read-xml and write-xml. Input can be a string, bytes, a port or a Reader. Bytes are decoded as UTF-8 (SSAX decoded them as Latin-1).
  • :std/text/markup/html: an HTML parser that follows the WHATWG spec (tokenizer, all 2231 named character references, tree construction, fragment parsing) and an HTML writer. The parser passes the html5lib tokenizer tests and the WPT tree construction tests. Documents nested deeper than 512 elements are refused with HTMLParseError, as Go's x/net/html does.
  • :std/encoding/utf8: UTF-8 encoding, decoding and validation with exact error offsets, used by the markup modules.
  • Djot renders typed HTML nodes. djot->sxml is replaced by djot->html-fragment. djot->html output is byte-identical on the 302 Djot conformance fixtures.
  • S3 reads responses with the typed XML reader. Non-ASCII keys are no longer garbled.
  • TAL is compiled through the typed HTML reader and writer. The exported names are unchanged. Malformed templates are now compile errors with file, line and column. tal:attributes works.
  • SXML, SXPath and SSAX remain available, deprecated, through :std/text/markup/sxml.

Nodes are immutable values. Applying a node type, e.g. (XMLCData …), checks its arguments, including list members; the make-xml-* and make-html-* names are the same constructors, and make-html-element, make-html-attribute and make-html-foreign-attribute add the HTML rules. The writers trust slot types and check what each output language forbids.

Breaking changes

  • :std/text/markup/xml and :std/text/markup/html export the typed APIs. Code using SXML should import :std/text/markup/sxml.
  • djot->sxml is removed. Djot writes attribute names in lower case, and U+0000 in text and attribute values as U+FFFD.
  • TAL rejects templates the old parser silently repaired (mismatched or stray tags, unknown tal: attributes) and templates the HTML parser would read differently.
  • TAL templates see only the defining module's bindings (no html-escape, no var.repeat), several statements follow the Zope rules, static markup is re-serialized by the HTML writer, and tal:write is a procedure; see tal.md, "Differences from the SXML TAL".
  • The SXML TAL implementation (:std/text/markup/sxml/tal/…, sxml/html/tal) is removed; :std/text/markup/tal replaces it.
  • HTML nested deeper than 512 elements raises HTMLParseError, and HTML attribute names with ASCII upper case are rejected by the constructors and write-html.

Notes for review

  • Specialised code that stays instead of a runtime facility (e.g. string-same?, the name tables, the pairwise duplicate check) cites the markup-benchmark.ss mode, size and iterations that reproduce its measurement.
  • :std/encoding/utf8 does not shadow the core string->utf8 and utf8->string. Its utf8-decode raises on invalid input instead of writing U+FFFD as the old :std/text/utf8 did.
  • src/std/text/markup/html-conformance.ss runs the pinned upstream html5lib and WPT tests from checkouts you supply, and src/std/text/markup/markup-benchmark.ss reproduces the performance and scaling claims. Neither is built or run by build.sh; their headers give the commands, as djot-benchmark.ss and http-benchmark.ss do.
  • The HTML script-data states exist in the tokenizer and in the writer's check, which work on bytes and strings respectively. A seeded differential test keeps them in agreement.
  • write-html writes a quirks-forcing DOCTYPE as a well-formed one, so the document may read back in no-quirks mode. This is documented.
  • The writers emit into std/io's output buffer structs. std/io/bio gains bio-output-reserve!, which makes room for n bytes in an output buffer; when the sink is a stdio BufferedWriter, the writers write straight into its buffer. A port receives nothing when a writer raises.

Benchmarks

Speedup over the SXML implementation (old time ÷ new time) for each operation over a corpus of generated and real files. Gerbil compiled with gxc -O -exe on an AMD Ryzen 9 7940HS, one pinned core, median of seven timed batches. Every input is faster.

Operation Inputs Median Range
XML read (string) 30 9.2× 3.3–14.2×
XML read (bytes) 30 11.4× 4.8–18.8×
XML read (file) 30 9.4× 3.8–12.2×
XML write 30 23.2× 5.1–194×
HTML read 26 18.8× 2.9–31.3×
HTML read fragment 14 15.2× 6.3–20.7×
HTML write 26 34.3× 3.1–99.4×
HTML escape 5 26.9× 3.8–61.9×
Djot to HTML string 29 8.6× 2.8–15.9×
Djot parse and render 29 4.6× 2.4–7.1×
Djot to HTML nodes 29 2.2× 1.3–3.0×
Djot written to a port 29 12.5× 4.4–19.9×
S3 key extraction 4 12.0× 6.3–13.1×
TAL render 3 8.0× 2.8–9.6×

The largest XML write factors come from the old writer's quadratic behaviour on deep and element-dense trees. A further 23 inputs crash the old implementation (numeric character references ≥ 128, a TAL template using tal:attributes) and are excluded.

Tests

  • html-conformance.ss against html5lib-tests c777c40 and WPT 260bb4c1d9: tokenizer 7045 passed, tree construction 1963 passed, write-html round trip 1950 passed, 0 failed; the 29 skipped cases are listed by id with their reasons in the runner.
  • Djot conformance fixtures: all 302 byte-identical.
  • std/...: 158 modules, 0 failures.
  • The readers, writers and :std/encoding/utf8 were also fuzzed, and every per-element or per-token loop has a scaling mode in markup-benchmark.ss.

Warning

LLM-assisted with Claude Opus 5.5, Claude Sonnet 5.5 and gerbil-mcp.

Replaces SXML in the markup stack with typed XML and HTML modules, as discussed in #1509. SXML stays available, deprecated. ## Changes - **`:std/text/markup/xml`**: typed XML nodes (`XMLElement`, `XMLText`, … under an `XML` base), `read-xml` and `write-xml`. Input can be a string, bytes, a port or a `Reader`. Bytes are decoded as UTF-8 (SSAX decoded them as Latin-1). - **`:std/text/markup/html`**: an HTML parser that follows the WHATWG spec (tokenizer, all 2231 named character references, tree construction, fragment parsing) and an HTML writer. The parser passes the html5lib tokenizer tests and the WPT tree construction tests. Documents nested deeper than 512 elements are refused with `HTMLParseError`, as Go's `x/net/html` does. - **`:std/encoding/utf8`**: UTF-8 encoding, decoding and validation with exact error offsets, used by the markup modules. - **Djot** renders typed HTML nodes. `djot->sxml` is replaced by `djot->html-fragment`. `djot->html` output is byte-identical on the 302 Djot conformance fixtures. - **S3** reads responses with the typed XML reader. Non-ASCII keys are no longer garbled. - **TAL** is compiled through the typed HTML reader and writer. The exported names are unchanged. Malformed templates are now compile errors with file, line and column. `tal:attributes` works. - **SXML, SXPath and SSAX** remain available, deprecated, through `:std/text/markup/sxml`. Nodes are immutable values. Applying a node type, e.g. `(XMLCData …)`, checks its arguments, including list members; the `make-xml-*` and `make-html-*` names are the same constructors, and `make-html-element`, `make-html-attribute` and `make-html-foreign-attribute` add the HTML rules. The writers trust slot types and check what each output language forbids. ## Breaking changes - `:std/text/markup/xml` and `:std/text/markup/html` export the typed APIs. Code using SXML should import `:std/text/markup/sxml`. - `djot->sxml` is removed. Djot writes attribute names in lower case, and U+0000 in text and attribute values as U+FFFD. - TAL rejects templates the old parser silently repaired (mismatched or stray tags, unknown `tal:` attributes) and templates the HTML parser would read differently. - TAL templates see only the defining module's bindings (no `html-escape`, no `var.repeat`), several statements follow the Zope rules, static markup is re-serialized by the HTML writer, and `tal:write` is a procedure; see tal.md, "Differences from the SXML TAL". - The SXML TAL implementation (`:std/text/markup/sxml/tal/…`, `sxml/html/tal`) is removed; `:std/text/markup/tal` replaces it. - HTML nested deeper than 512 elements raises `HTMLParseError`, and HTML attribute names with ASCII upper case are rejected by the constructors and `write-html`. ## Notes for review - Specialised code that stays instead of a runtime facility (e.g. `string-same?`, the name tables, the pairwise duplicate check) cites the `markup-benchmark.ss` mode, size and iterations that reproduce its measurement. - `:std/encoding/utf8` does not shadow the core `string->utf8` and `utf8->string`. Its `utf8-decode` raises on invalid input instead of writing U+FFFD as the old `:std/text/utf8` did. - `src/std/text/markup/html-conformance.ss` runs the pinned upstream html5lib and WPT tests from checkouts you supply, and `src/std/text/markup/markup-benchmark.ss` reproduces the performance and scaling claims. Neither is built or run by `build.sh`; their headers give the commands, as `djot-benchmark.ss` and `http-benchmark.ss` do. - The HTML script-data states exist in the tokenizer and in the writer's check, which work on bytes and strings respectively. A seeded differential test keeps them in agreement. - `write-html` writes a quirks-forcing DOCTYPE as a well-formed one, so the document may read back in no-quirks mode. This is documented. - The writers emit into std/io's output buffer structs. `std/io/bio` gains `bio-output-reserve!`, which makes room for n bytes in an output buffer; when the sink is a stdio `BufferedWriter`, the writers write straight into its buffer. A port receives nothing when a writer raises. ## Benchmarks Speedup over the SXML implementation (old time ÷ new time) for each operation over a corpus of generated and real files. Gerbil compiled with `gxc -O -exe` on an AMD Ryzen 9 7940HS, one pinned core, median of seven timed batches. Every input is faster. | Operation | Inputs | Median | Range | |---|---:|---:|---:| | XML read (string) | 30 | 9.2× | 3.3–14.2× | | XML read (bytes) | 30 | 11.4× | 4.8–18.8× | | XML read (file) | 30 | 9.4× | 3.8–12.2× | | XML write | 30 | 23.2× | 5.1–194× | | HTML read | 26 | 18.8× | 2.9–31.3× | | HTML read fragment | 14 | 15.2× | 6.3–20.7× | | HTML write | 26 | 34.3× | 3.1–99.4× | | HTML escape | 5 | 26.9× | 3.8–61.9× | | Djot to HTML string | 29 | 8.6× | 2.8–15.9× | | Djot parse and render | 29 | 4.6× | 2.4–7.1× | | Djot to HTML nodes | 29 | 2.2× | 1.3–3.0× | | Djot written to a port | 29 | 12.5× | 4.4–19.9× | | S3 key extraction | 4 | 12.0× | 6.3–13.1× | | TAL render | 3 | 8.0× | 2.8–9.6× | The largest XML write factors come from the old writer's quadratic behaviour on deep and element-dense trees. A further 23 inputs crash the old implementation (numeric character references ≥ 128, a TAL template using `tal:attributes`) and are excluded. ## Tests - `html-conformance.ss` against html5lib-tests c777c40 and WPT 260bb4c1d9: tokenizer 7045 passed, tree construction 1963 passed, write-html round trip 1950 passed, 0 failed; the 29 skipped cases are listed by id with their reasons in the runner. - Djot conformance fixtures: all 302 byte-identical. - `std/...`: 158 modules, 0 failures. - The readers, writers and `:std/encoding/utf8` were also fuzzed, and every per-element or per-token loop has a scaling mode in `markup-benchmark.ss`. > [!WARNING] > LLM-assisted with Claude Opus 5.5, Claude Sonnet 5.5 and `gerbil-mcp`.
jay added 72 commits 2026-10-20 16:37:23 +00:00
Check ASCII name bytes against the class table instead of calling
xml-name-code? per byte; keep pending prefixed attributes in a reused
scanner vector instead of consing (attribute . entry) pairs; make
utf8-length a macro; compare the xml prefix with string-same?.
Nullable scratch-text returns (Maybe :string). Measured in decision 015.
check-name! and check-attribute! become macros that bind their argument
as :t so the optimizer keeps the type tests; end tags copy the strings
the start tag checked. Nullable returns are (Maybe :string). Measured
in decision 015.
Writer: every type-tested read of a tree slot goes through untrusted, so
the PI data and document root checks are no longer optimized away; start
tag attributes are checked once and copied into a reused emitter vector
before emission; the element uri is passed as a local; string lengths are
checked after every reserve!; names longer than one slice are written in
slices; children must be a list; prolog and epilog members are checked.

API: xml-doctype and an optional doctype in xml-document (read and
written); read-xml keep-space?: and keep-comments?:;
xml-element-attribute-value; make-xml-name defaults the prefix of the
reserved namespaces. Probe cap tested before insertion; comments trimmed;
small predicates become definline where that costs nothing.
The emitters use Writer alone; :std/io pulled C code into any closure
that imports them, which expansion-time users cannot load.
emit-element! is now with-start-tag, emit-element-children! and
emit-element-end-tag!, macros the TAL compiler instantiates for templates
(the writer's code is unchanged). The scanner reports the offset of the
next token, and the XML emitter can escape ' as '.
define-TAL reads its template with its own structure (tal/reader), compiles
it at expansion time (tal/compiler) into byte literals and calls to
tal/runtime, which renders into one buffer and discards failed statements
by cutting it back. Templates no longer see the HTML facade or TAL
internals, and the legacy SXML TAL modules are removed.
A 4 KB first buffer is a still object in Gambit, and allocating one per
render made GC dominate small templates.
Conflict in html/emitter.ss: the H3 review checks (svg and math in the
HTML namespace, nested noscript, start tags the tree builder ignores)
move into with-start-tag; the foreign element rule and the frameset and
template states into emit-element-children!, which now takes the
element's namespace and context.

The TAL compiler keeps the emitter's frameset and template state while
dynamic children compile (a col child, also inside tal: elements or
tal:omit-tag elements, switches a template to column group; a tag that
may be omitted keeps the outer state), and its tags keep the element
class in 16 bits, as class-template is 256.
:std/text/markup/xml exports the typed XML types, read-xml and the
writer; :std/text/markup/html the HTML types, read-html,
read-html-fragment and the writer. :std/text/markup/sxml now also
exports what the old xml and html facades did (SSAX read-xml, write-xml,
print-sxml->xml, the HTML parser and printer) and warns that it is
deprecated. The SXML tests of the old facades move to sxml/ and import
it; new tests cover the typed facades.
xml.md and html.md describe the typed APIs and their differences from
SXML; tal.md replaces the TAL README under sxml/ and covers the typed
TAL's template rules and statements. The SXML documents say they are
deprecated.
Without its tags, the content of a raw text or plaintext element was
read as markup, and the children of an integration point (foreignObject,
mi, ...) in the parent's namespace: dynamic text could inject elements.
A scripting parser reads noscript as raw text, so dynamic script or style
text could end it and inject markup; the writer checks this for static
content.
jay requested review from Owners 2026-10-20 16:37:32 +00:00
Owner

impressive speedups!

impressive speedups!
vyzo left a comment

this is massive, but very much needed work. first pass, need to digest it.

this is massive, but very much needed work. first pass, need to digest it.
Owner

can you post the djot benchmark numbers compared to C and js?

can you post the djot benchmark numbers compared to C and js?
Owner

astra is reviewing and is quite critical of opus's code already :)

astra is reviewing and is quite critical of opus's code already :)
vyzo requested changes 2026-10-23 12:03:31 +00:00
Dismissed
vyzo left a comment

Request changes on 49b02af0. I found reproducible correctness defects and substantial avoidable machinery. The strongest maintainability concern is duplicated infrastructure and semantic rules that have already drifted apart.

I reviewed the branch against its merge base with origin/v0.19-staging.

  1. [P2] XML attribute checking is quadratic on untrusted input.
    xml/scanner.ss:792 compares each plain attribute against all preceding attributes. The namespace-resolution path has another pairwise comparison at line 739.

    Parsing one element with 1,000, 2,000, and 4,000 distinct attributes took approximately 0.37, 1.27, and 5.06 seconds in interpreted execution. These are scaling measurements, not compiled-performance benchmarks, but the quadratic algorithm is explicit.

    Use a hash set keyed by expanded attribute name. The HTML emitter already uses a small-list/hash-table threshold for duplicate detection; XML should reuse that approach.

  2. [P2] The HTML parser silently changes tree-building semantics beyond 512 open elements.
    html/builder.ss:512 limits scope searches and reports "not in scope" when the target lies outside its window.

    Reproduction: <div>, followed by 513 <span> tags, followed by </div><p>after</p>. The resulting fragment has only the outer div at the top level; the paragraph remains buried inside it. Processing </div> should close the intervening spans and allow the paragraph to become a sibling.

    A resource limit should produce an explicit failure or a deliberately specified recovery policy. Returning a false scope result silently corrupts parsing.

  3. [P2] TAL dynamic content loses a leading newline in textarea.
    tal/compiler.ss:308 chooses a value writer without handling the newline-sensitive element class.

    With <textarea tal:content="x"></textarea> and x = "\nx", TAL emits <textarea>\nx</textarea>. Parsing that output produces "x": HTML consumes the first newline.

    The HTML emitter and TAL's static-children path already handle this case. Dynamic content needs the same rule, preferably through a shared procedure rather than another independent special case.

  4. [P2] TAL incorrectly turns foreign-content CDATA into a comment.
    tal/reader.ss:231 derives CDATA permission from the child insertion context. That differs from the current element's namespace at HTML integration points.

    For:

    <svg><foreignObject><![CDATA[hi]]></foreignObject></svg>
    

    TAL emits:

    <svg><foreignObject><!--[CDATA[hi]]--></foreignObject></svg>
    

    The HTML reader correctly returns text, producing <svg><foreignObject>hi</foreignObject></svg> when serialized. This is a concrete consequence of maintaining separate versions of the foreign-content rules.

  5. [P2] TAL's repeat-method optimization breaks valid Scheme identifiers.
    tal/compiler.ss:549 searches raw attribute strings to decide which bindings to generate, using a handwritten approximation of Scheme identifier syntax.

    This valid template fails:

    <b tal:repeat="(i (list 1 2))"
       tal:content="(|repeat/i.number|)"></b>
    

    Both iterations render an unbound-variable error for repeat/i.number. Pipe quoting denotes the same identifier, but the string search misses it.

    Generate the small, fixed set of bindings and let the compiler optimize them, or inspect actual Scheme syntax. A partial Scheme lexer is excessive machinery for this optimization.

  6. [P2] Script-tokenizer shortcuts bypass carriage-return normalization.
    html/scanner.ss:973 jumps past the separator following matched script names without processing that separator.

    Parsing "<script><!--<script\r></script>--></script>" leaves the literal \r in the script text instead of normalizing it to \n. The corresponding </script\r> transition also reproduces the problem.

    Process the separator through the common normalization logic, or correctly propagate the need to normalize the output span.

  7. [P2] XML declarations are accepted without enforcing their meaning.
    xml/scanner.ss:1237 checks only that the version begins with "1.", accepting version='1.foo'.

    Separately, line 1272 treats ASCII declarations as unrestricted UTF-8. UTF-8 bytes for <?xml version='1.0' encoding='US-ASCII'?><a>é</a> are accepted and produce é.

    Validate the version grammar and supported versions, and enforce ASCII for byte input declaring ASCII.

Structural cleanup and reuse of existing facilities

  • Remove the elaborate support for prohibited mutation.
    xml/types.ss:5 says callers should not mutate nodes or their lists after construction. Nevertheless, xml/emitter.ss:8 explicitly caters for mutation by writer callbacks or another thread.

    This produces an untrusted macro that erases type information, repeated slot checks, attribute snapshots, string-length rechecks, and defensive copies. html/emitter.ss:463 copies individual text strings before the surrounding code may concatenate them again.

    Settle the ownership contract and trust it internally. Keep checked public constructors, external-input validation, and serialization-validity checks. The machinery for concurrent mutation is a separate feature with considerable complexity and allocation cost; the stated API contract gives it no justification.

  • Use existing I/O facilities instead of building a private buffering subsystem.
    xml/source.ss:14 implements read-all buffering already supplied by :std/io's read-all-from-reader and :std/misc/ports's read-all-as-string / read-all-as-u8vector.

    xml/emitter.ss:37 combines generic byte buffering with XML namespace state. HTML and TAL consequently depend on an XML emitter as their generic output buffer.

    Separate namespace bookkeeping from output. Start with the buffered/memory writers and adapters already available through :std/io, including with-buffered-writer. A scanner sentinel needs only a thin adaptation. Any custom fast path should have reproducible benchmark evidence showing why the existing abstraction is inadequate.

  • Consolidate HTML semantic rules.
    Namespace/context classification and integration-point handling are repeated across the builder, emitter, and TAL reader. Script-state logic also exists in both scanning and serialization.

    Shared metadata and transition helpers would reduce the maintenance burden. The CDATA and leading-newline defects above demonstrate the practical cost of the current duplication. The different consumers can retain their own traversal algorithms while sharing these rules.

  • Replace trivial reinventions and generate mechanical repetition.

    New code Existing facility or appropriate abstraction
    html/node.ss's string-table Core list->hash-table-string
    html/emitter.ss's contains? string-contains, converting its result to a boolean if necessary
    xml/node.ss's string-same? string=?, unless measurements justify specialization
    HTML ascii-ci=? and emitter folded-equal? One shared ASCII comparison procedure
    Repeated scanner scratch-buffer reserve/append routines Shared buffer helper or a macro generating typed specializations
    Djot's 36 tag-* declarations One declaration macro generating the same shared constants

    The Djot declarations begin at djot/html.ss:19. A macro preserves their allocation behavior without the repetitive definitions. Much of Djot's renderer-dispatch boilerplate predates this PR, so I would not attribute that existing repetition to this change.

  • Reduce duplicated UTF-8 machinery.
    Input conversion, node emission, scanning, and output decoding each contain handwritten UTF-8 work. In particular, xml/emitter.ss:90 implements another decoder for output already known to be valid UTF-8.

    Use existing conversions for those cases. XML-specific character validation still belongs in the XML parser. If specialized encoding loops are measurably necessary, share their implementation or generation rather than maintaining several copies.

The generated entity/atom tables are reasonable, and define-string-emitter already demonstrates useful macro factoring. The cleanup should concentrate on unnecessary defensive machinery, duplicated library facilities, and duplicated semantic rules.

Validation: ten XML/HTML test modules and the S3 response tests passed using the installed runtime with the PR's sources. TAL's interpreted cases passed; its compiled hygiene case could not complete because the source-only setup lacked class optimizer metadata. Djot tests could not load because the installed dependencies lacked char-set:punctuation. I did not perform a full build or modify repository files.

Request changes on `49b02af0`. I found reproducible correctness defects and substantial avoidable machinery. The strongest maintainability concern is duplicated infrastructure and semantic rules that have already drifted apart. I reviewed the branch against its merge base with `origin/v0.19-staging`. 1. **[P2] XML attribute checking is quadratic on untrusted input.** [xml/scanner.ss:792](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/scanner.ss#L792) compares each plain attribute against all preceding attributes. The namespace-resolution path has another pairwise comparison at [line 739](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/scanner.ss#L739). Parsing one element with 1,000, 2,000, and 4,000 distinct attributes took approximately **0.37, 1.27, and 5.06 seconds** in interpreted execution. These are scaling measurements, not compiled-performance benchmarks, but the quadratic algorithm is explicit. Use a hash set keyed by expanded attribute name. The HTML emitter already uses a small-list/hash-table threshold for duplicate detection; XML should reuse that approach. 2. **[P2] The HTML parser silently changes tree-building semantics beyond 512 open elements.** [html/builder.ss:512](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/builder.ss#L512) limits scope searches and reports "not in scope" when the target lies outside its window. Reproduction: `<div>`, followed by 513 `<span>` tags, followed by `</div><p>after</p>`. The resulting fragment has only the outer `div` at the top level; the paragraph remains buried inside it. Processing `</div>` should close the intervening spans and allow the paragraph to become a sibling. A resource limit should produce an explicit failure or a deliberately specified recovery policy. Returning a false scope result silently corrupts parsing. 3. **[P2] TAL dynamic content loses a leading newline in `textarea`.** [tal/compiler.ss:308](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/compiler.ss#L308) chooses a value writer without handling the newline-sensitive element class. With `<textarea tal:content="x"></textarea>` and `x = "\nx"`, TAL emits `<textarea>\nx</textarea>`. Parsing that output produces `"x"`: HTML consumes the first newline. The HTML emitter and TAL's static-children path already handle this case. Dynamic content needs the same rule, preferably through a shared procedure rather than another independent special case. 4. **[P2] TAL incorrectly turns foreign-content CDATA into a comment.** [tal/reader.ss:231](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/reader.ss#L231) derives CDATA permission from the child insertion context. That differs from the current element's namespace at HTML integration points. For: ```html <svg><foreignObject><![CDATA[hi]]></foreignObject></svg> ``` TAL emits: ```html <svg><foreignObject><!--[CDATA[hi]]--></foreignObject></svg> ``` The HTML reader correctly returns text, producing `<svg><foreignObject>hi</foreignObject></svg>` when serialized. This is a concrete consequence of maintaining separate versions of the foreign-content rules. 5. **[P2] TAL's repeat-method optimization breaks valid Scheme identifiers.** [tal/compiler.ss:549](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/compiler.ss#L549) searches raw attribute strings to decide which bindings to generate, using a handwritten approximation of Scheme identifier syntax. This valid template fails: ```html <b tal:repeat="(i (list 1 2))" tal:content="(|repeat/i.number|)"></b> ``` Both iterations render an unbound-variable error for `repeat/i.number`. Pipe quoting denotes the same identifier, but the string search misses it. Generate the small, fixed set of bindings and let the compiler optimize them, or inspect actual Scheme syntax. A partial Scheme lexer is excessive machinery for this optimization. 6. **[P2] Script-tokenizer shortcuts bypass carriage-return normalization.** [html/scanner.ss:973](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/scanner.ss#L973) jumps past the separator following matched script names without processing that separator. Parsing `"<script><!--<script\r></script>--></script>"` leaves the literal `\r` in the script text instead of normalizing it to `\n`. The corresponding `</script\r>` transition also reproduces the problem. Process the separator through the common normalization logic, or correctly propagate the need to normalize the output span. 7. **[P2] XML declarations are accepted without enforcing their meaning.** [xml/scanner.ss:1237](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/scanner.ss#L1237) checks only that the version begins with `"1."`, accepting `version='1.foo'`. Separately, [line 1272](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/scanner.ss#L1272) treats ASCII declarations as unrestricted UTF-8. UTF-8 bytes for `<?xml version='1.0' encoding='US-ASCII'?><a>é</a>` are accepted and produce `é`. Validate the version grammar and supported versions, and enforce ASCII for byte input declaring ASCII. **Structural cleanup and reuse of existing facilities** - **Remove the elaborate support for prohibited mutation.** [xml/types.ss:5](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/types.ss#L5) says callers should not mutate nodes or their lists after construction. Nevertheless, [xml/emitter.ss:8](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/emitter.ss#L8) explicitly caters for mutation by writer callbacks or another thread. This produces an `untrusted` macro that erases type information, repeated slot checks, attribute snapshots, string-length rechecks, and defensive copies. [html/emitter.ss:463](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/emitter.ss#L463) copies individual text strings before the surrounding code may concatenate them again. Settle the ownership contract and trust it internally. Keep checked public constructors, external-input validation, and serialization-validity checks. The machinery for concurrent mutation is a separate feature with considerable complexity and allocation cost; the stated API contract gives it no justification. - **Use existing I/O facilities instead of building a private buffering subsystem.** [xml/source.ss:14](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/source.ss#L14) implements read-all buffering already supplied by `:std/io`'s `read-all-from-reader` and `:std/misc/ports`'s `read-all-as-string` / `read-all-as-u8vector`. [xml/emitter.ss:37](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/emitter.ss#L37) combines generic byte buffering with XML namespace state. HTML and TAL consequently depend on an XML emitter as their generic output buffer. Separate namespace bookkeeping from output. Start with the buffered/memory writers and adapters already available through `:std/io`, including [with-buffered-writer](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/io/util.ss#L119). A scanner sentinel needs only a thin adaptation. Any custom fast path should have reproducible benchmark evidence showing why the existing abstraction is inadequate. - **Consolidate HTML semantic rules.** Namespace/context classification and integration-point handling are repeated across the builder, emitter, and TAL reader. Script-state logic also exists in both scanning and serialization. Shared metadata and transition helpers would reduce the maintenance burden. The CDATA and leading-newline defects above demonstrate the practical cost of the current duplication. The different consumers can retain their own traversal algorithms while sharing these rules. - **Replace trivial reinventions and generate mechanical repetition.** | New code | Existing facility or appropriate abstraction | |---|---| | `html/node.ss`'s `string-table` | Core `list->hash-table-string` | | `html/emitter.ss`'s `contains?` | `string-contains`, converting its result to a boolean if necessary | | `xml/node.ss`'s `string-same?` | `string=?`, unless measurements justify specialization | | HTML `ascii-ci=?` and emitter `folded-equal?` | One shared ASCII comparison procedure | | Repeated scanner scratch-buffer reserve/append routines | Shared buffer helper or a macro generating typed specializations | | Djot's 36 `tag-*` declarations | One declaration macro generating the same shared constants | The Djot declarations begin at [djot/html.ss:19](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/djot/html.ss#L19). A macro preserves their allocation behavior without the repetitive definitions. Much of Djot's renderer-dispatch boilerplate predates this PR, so I would not attribute that existing repetition to this change. - **Reduce duplicated UTF-8 machinery.** Input conversion, node emission, scanning, and output decoding each contain handwritten UTF-8 work. In particular, [xml/emitter.ss:90](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/emitter.ss#L90) implements another decoder for output already known to be valid UTF-8. Use existing conversions for those cases. XML-specific character validation still belongs in the XML parser. If specialized encoding loops are measurably necessary, share their implementation or generation rather than maintaining several copies. The generated entity/atom tables are reasonable, and `define-string-emitter` already demonstrates useful macro factoring. The cleanup should concentrate on unnecessary defensive machinery, duplicated library facilities, and duplicated semantic rules. **Validation:** ten XML/HTML test modules and the S3 response tests passed using the installed runtime with the PR's sources. TAL's interpreted cases passed; its compiled hygiene case could not complete because the source-only setup lacked class optimizer metadata. Djot tests could not load because the installed dependencies lacked `char-set:punctuation`. I did not perform a full build or modify repository files.
Owner

review by astra, quite critical for the slop factor.

review by astra, quite critical for the slop factor.
Author
Member

@vyzo addressing your feedback in separate messages so it's easy to reply to indivdually

  1. HTML nested more than 512 elements deep
Parser What it does Follows browser rules?
Old Gerbil html->sxml No limit, stays fast. But it doesn't apply browser rules even at depth 2: <div><span><span>a</div><p>after</p> comes out as (div (span (span a)) (span (p after))). The review's example is broken in the old library at any depth. No
Go x/net/html Refuses the document: "open stack of elements exceeds 512 nodes". Yes
Clojure (hickory uses jsoup) jsoup closes the innermost element once 512 are open. You can change the limit with setMaxDepth. Yes
Safari (WebKit) Same as jsoup: closes the innermost element past 512. Yes
Chrome, Firefox Past 512, attach new elements one level up, but still search every open element. That gets slow on very deep input. Yes
Python html5lib, Rust html5ever No limit, full search, so slow on very deep input. Yes
Racket html-parsing No browser rules at all, like the old Gerbil library. No

What do you think we should do? I would lean towards explicitly refuse like Go does, seems the simplest.

@vyzo addressing your feedback in separate messages so it's easy to reply to indivdually 1. HTML nested more than 512 elements deep | Parser | What it does | Follows browser rules? | |---|---|---| | Old Gerbil `html->sxml` | No limit, stays fast. But it doesn't apply browser rules even at depth 2: `<div><span><span>a</div><p>after</p>` comes out as `(div (span (span a)) (span (p after)))`. The review's example is broken in the old library at any depth. | No | | Go `x/net/html` | Refuses the document: "open stack of elements exceeds 512 nodes". | Yes | | Clojure (hickory uses jsoup) | jsoup closes the innermost element once 512 are open. You can change the limit with `setMaxDepth`. | Yes | | Safari (WebKit) | Same as jsoup: closes the innermost element past 512. | Yes | | Chrome, Firefox | Past 512, attach new elements one level up, but still search every open element. That gets slow on very deep input. | Yes | | Python html5lib, Rust html5ever | No limit, full search, so slow on very deep input. | Yes | | Racket `html-parsing` | No browser rules at all, like the old Gerbil library. | No | What do you think we should do? I would lean towards explicitly refuse like Go does, seems the simplest.
Author
Member

Use a hash set keyed by expanded attribute name. The HTML emitter already uses a small-list/hash-table threshold for duplicate detection; XML should reuse that approach.

Yeah it's quadratic but a hash set doesn't fix it. Gerbil's string hash is FNV-1a and the table seed doesn't change which names collide, so someone can pick attribute names that all land in the same bucket. I had Opus try it and 16k colliding names take 567 ms in a hash set, and the HTML emitter's existing check takes over a second. Sorting and comparing neighbours can't be gamed that way, so I'll do that above 16 attributes and switch the HTML emitter to it too if that sounds good.

> Use a hash set keyed by expanded attribute name. The HTML emitter already uses a small-list/hash-table threshold for duplicate detection; XML should reuse that approach. Yeah it's quadratic but a hash set doesn't fix it. Gerbil's string hash is FNV-1a and the table seed doesn't change which names collide, so someone can pick attribute names that all land in the same bucket. I had Opus try it and 16k colliding names take 567 ms in a hash set, and the HTML emitter's existing check takes over a second. Sorting and comparing neighbours can't be gamed that way, so I'll do that above 16 attributes and switch the HTML emitter to it too if that sounds good.
Author
Member

Validate the version grammar and supported versions

The grammar, yes but if "supported versions" means rejecting 1.1, that's wrong: XML 1.0 §2.8 says a 1.0 processor has to accept any 1.x document and treat it as 1.0. So 1.foo gets rejected and 1.1 doesn't.

> Validate the version grammar and supported versions The grammar, yes but if "supported versions" means rejecting 1.1, that's wrong: [XML 1.0 §2.8](https://www.w3.org/TR/xml/#sec-prolog-dtd) says a 1.0 processor has to accept any 1.x document and treat it as 1.0. So `1.foo` gets rejected and `1.1` doesn't.
Author
Member

Start with the buffered/memory writers and adapters already available through :std/io, including with-buffered-writer.

Opus measured this and it's much slower. Our writer escapes and UTF-8-encodes in one pass straight into a byte buffer. BufferedWriter has no public way to write into its buffer directly (correct?), so the same work through its methods is 2.6× slower with write-string-utf8 and 8.6× slower with write-char-utf8.

So I'll keep a small byte buffer, but move it out of the XML emitter so HTML and TAL stop depending on it if that sounds good.

If you'd take a public way to reserve space in a BufferedWriter's buffer, we can do that in a separate PR?

> Start with the buffered/memory writers and adapters already available through :std/io, including with-buffered-writer. Opus measured this and it's much slower. Our writer escapes and UTF-8-encodes in one pass straight into a byte buffer. `BufferedWriter` has no public way to write into its buffer directly (correct?), so the same work through its methods is 2.6× slower with `write-string-utf8` and 8.6× slower with `write-char-utf8`. So I'll keep a small byte buffer, but move it out of the XML emitter so HTML and TAL stop depending on it if that sounds good. If you'd take a public way to reserve space in a BufferedWriter's buffer, we can do that in a separate PR?
Author
Member

Script-state logic also exists in both scanning and serialization.

These two haven't drifted. I had Opus fuzz them against each other with 60k random scripts and they agree on all of them. The CR bug in #6 is a shortcut in the scanner, not a disagreement between the two. The scanner works on bytes in the hot loop and the writer works on strings, so merging them would slow parsing down. I'd rather keep both and add the fuzz test to the suite so any drift gets caught.

> Script-state logic also exists in both scanning and serialization. These two haven't drifted. I had Opus fuzz them against each other with 60k random scripts and they agree on all of them. The CR bug in #6 is a shortcut in the scanner, not a disagreement between the two. The scanner works on bytes in the hot loop and the writer works on strings, so merging them would slow parsing down. I'd rather keep both and add the fuzz test to the suite so any drift gets caught.
Author
Member

xml/node.ss's string-same? → string=?, unless measurements justify specialization

They do, string=? is n-ary, and per element it measured about 1300 instructions against 500 for string-same?. I'll add a comment saying so.

Djot's 36 tag-* declarations → One declaration macro

Agree they should go, but we probably don't need a macro? The HTML parser already keeps one shared name object per known tag, so Djot can use those and all 36 definitions disappear.

> xml/node.ss's string-same? → string=?, unless measurements justify specialization They do, `string=?` is n-ary, and per element it measured about 1300 instructions against 500 for `string-same?`. I'll add a comment saying so. > Djot's 36 tag-* declarations → One declaration macro Agree they should go, but we probably don't need a macro? The HTML parser already keeps one shared name object per known tag, so Djot can use those and all 36 definitions disappear.
Author
Member

Use existing conversions for those cases.

Tried that and they're slower. utf8->string makes xml->string 10–19% slower end to end, and string->utf8 slows string input by 16–44%, because both make two passes over the data. So we can go with your alternative of one shared decoder and validator. Encoding was already a single macro.

> Use existing conversions for those cases. Tried that and they're slower. `utf8->string` makes `xml->string` 10–19% slower end to end, and `string->utf8` slows string input by 16–44%, because both make two passes over the data. So we can go with your alternative of one shared decoder and validator. Encoding was already a single macro.
Author
Member

I think the Djot test failure is your installed runtime: char-set:punctuation came in with #1509.

The TAL hygiene failure is my test's fault. It assumed compiled TAL metadata was on the loadpath, and it'll skip when it isn't.

I think the Djot test failure is your installed runtime: `char-set:punctuation` came in with https://git.cons.io/mighty-gerbils/gerbil/pulls/1509. The TAL hygiene failure is my test's fault. It assumed compiled TAL metadata was on the loadpath, and it'll skip when it isn't.
Owner

i'll do a second pass for matters more of idiom.

i'll do a second pass for matters more of idiom.
vyzo requested changes 2026-10-23 21:55:41 +00:00
Dismissed
vyzo left a comment

Second pass: type hierarchy, signatures, and idiomatic field access

Request changes on 49b02af0585ee3884039e7b46477ce70cf401ce2. The XML root is feasible, and this cleanup should happen before merging. This review supplements the first review.

The implementation has typed structs, but their naming and use are inconsistent. The parser frequently uses typed dotted access; TAL, S3, and parts of the HTML API revert to nested accessor calls. The emitters deliberately discard type information and reconstruct it through manual checks.

Please make the following changes across the implementation and its consumers.

  1. Introduce an XML base struct and rename the concrete XML types.

    The declarations in xml/node.ss are nine unrelated, lowercase struct types. Use an empty, extensible XML base and keep concrete types final:

    (defstruct XML ())
    
    (defstruct (XMLName XML)
      ((local  :- :string)
       (uri    :- :string)
       (prefix :- :string))
      final: #t)
    
    (defstruct (XMLAttribute XML)
      ((name  :- XMLName)
       (value :- :string))
      final: #t)
    
    (defstruct (XMLElement XML)
      ((name       :- XMLName)
       (attributes :- :list)
       (children   :- :list))
      final: #t)
    

    Apply this mapping consistently:

    Current type Requested type
    xml-name XMLName
    xml-attribute XMLAttribute
    xml-element XMLElement
    xml-text XMLText
    xml-cdata XMLCData
    xml-comment XMLComment
    xml-pi XMLPI
    xml-doctype XMLDoctype
    xml-document XMLDocument

    XML is a nominal model root. XML? must not replace the predicate for valid element children: names, attributes, documents, and doctypes are not legal children merely because they share that root. Preserve the existing context-specific restrictions.

    Rename the HTML additions to HTMLRaw, HTMLDoctype, and HTMLDocument, retaining their distinct representations and their reuse of XMLElement, XMLAttribute, etc. In particular, HTML and XML doctypes have different validity rules.

    Ordinary procedures such as read-xml, xml-name=?, and checked make-xml-* factories can retain conventional lowercase names. Update generated predicates and accessor names consistently. Preserve the checked-construction boundary; a naming migration must not replace validated construction with unchecked allocation.

  2. Convert consumers to typed dotted access, including nested fields.

    The clearest cleanup targets are:

    Location Required change
    S3 response processing Use doc.root, name.local, name.uri, el.children, and el.name.local.
    TAL compiler Convert node inspection, attribute filtering, name comparison, and static/dynamic attribute handling.
    TAL reader Annotate attribute callbacks and use attr.name.local and attr.value.
    HTML fragment reader Establish the context element's type once, then use its typed fields.
    Djot backlink handling Narrow text/element values and use dotted fields when inspecting or rebuilding paragraphs.

    For example, S3's name predicate should become:

    (def (s3-name? (name  : XMLName)
                   (local : :string))
      => :boolean
      (and (string=? name.local local)
           (or (string=? name.uri s3-namespace-uri)
               (string=? name.uri s3-doc-namespace-uri))))
    

    For an attribute list already established by the parser or constructor:

    (filter-map
     (lambda ((attr :- XMLAttribute))
       (let (local attr.name.local)
         (and (tal-name? local)
              [(substring local 4 (string-length local))
               attr.value])))
     el.attributes)
    

    For a heterogeneous value, retain the actual dispatch and annotate the selected branch:

    (cond
     ((XMLElement? node)
      (using (node :- XMLElement)
        node.name.local))
     ...)
    

    Use parameter signatures when the whole procedure operates on one concrete type. Use using for local narrowing or values retrieved from heterogeneous collections. Avoid repeated casts around individual field reads.

  3. Preserve type information instead of erasing and rechecking it.

    xml/node.ss:237 defines:

    (defrule (untrusted expr)
      (:- expr :t))
    

    This defeats the typed model. For example, XML attribute emission reads an attribute through using, erases the types of its fields, checks them again, then repeats this for the name's fields.

    Resolve the ownership issue identified in the first review and remove this machinery under the documented no-mutation contract. Typed attr.name, attr.value, and attr.name.local should remain typed.

    Keep validation that types do not establish: namespace constraints, valid characters, duplicate attributes, legal child kinds, and serialization restrictions. Also retain element validation at public list-taking boundaries: :list establishes a proper list, not the types of its members.

  4. Replace unnecessarily broad signatures with the actual types.

    Several known types are currently spelled :t:

    Location Requested annotation
    HTML builder new-element Its name argument is XMLName.
    HTML builder freeze-element Return XMLElement.
    HTML builder token-attribute Return (Maybe :string); propagate that to encoding parameters.
    HTML builder attribute-table Return HashTable.
    TAL template and compiler Their positions fields are HashTable.
    XML emit-doctype! Its doctype argument is XMLDoctype.

    Likewise, internal child-list procedures should receive :list once the boundary establishes that contract. Use :? Type for nullable parameters and fields, and precise return signatures.

    Some values genuinely remain heterogeneous: reader sources, HTML writer inputs that also accept fragments, scanner result registers, and user Scheme expressions. Keep those broad where necessary and narrow at dispatch points.

  5. Use forward types for the HTML builder's recursive links.

    html/builder.ss:166 justifies six :t fields with “a slot type cannot name its own struct.” Gerbil provides forward type aliases.

    Rename hnode to HTMLBuilderNode and use:

    (deftype @HTMLBuilderNode HTMLBuilderNode)
    

    Its parent, prev, next, first, last, and contents fields can then use :? @HTMLBuilderNode; name can use :? XMLName. After a nonfalse check, access the typed value directly.

    This removes a real source of lost type information and repeated using blocks. The builder node remains an internal mutable construction representation, separate from the public XML records.

  6. Apply the convention to newly introduced implementation types and macros.

    Use descriptive CamelCase names such as XMLScanner, XMLEmitter, HTMLTokenizer, HTMLBuilder, HTMLEmitter, TALTemplate, TALCompiler, and TALRender. Keep these implementation-state types outside the XML model hierarchy.

    Include field writes in the cleanup. For example, TAL's emitter reset should use typed bindings and (set! em.pos 0). TAL runtime macros should establish a typed render binding and use fields such as r.out.pos.

    Preserve macro hygiene and single evaluation when adding bindings. Keep semantic operations—such as checked tokenizer operations—as procedures; distinguish those from generated field accessors.

  7. Complete the public API migration and verify it across module boundaries.

    Update both public facades, type modules, signatures, imports, predicates, tests, and the XML/HTML/TAL/Djot documentation together. Public examples should demonstrate typed dotted access. Export the type information consumers need without exposing implementation structs or unchecked construction helpers through the facade.

    Acceptance checks should cover:

    • Common-base membership and unchanged concrete-type discrimination.
    • Rejection of names, attributes, doctypes, and documents in invalid child positions.
    • Nested dotted access through public imports, including nullable results.
    • Checked constructors and unchanged XML/HTML serialization behavior.
    • S3 consumers, Djot rendering, and compiled TAL hygiene.
    • Remaining accessor calls reviewed deliberately, rather than mechanically renamed.

    Follow src/std/AGENTS.md for aligned, one-argument-per-line signatures and typed callbacks. Avoid blanket unchecked annotations where the input type has not been established.

Validation for this pass: static source review plus small interpreted checks confirming struct inheritance, nested dotted access, and nullable recursive fields through deftype. No repository files were changed, and no build or regression suite was run.

**Second pass: type hierarchy, signatures, and idiomatic field access** Request changes on `49b02af0585ee3884039e7b46477ce70cf401ce2`. The `XML` root is feasible, and this cleanup should happen before merging. This review supplements the first review. The implementation has typed structs, but their naming and use are inconsistent. The parser frequently uses typed dotted access; TAL, S3, and parts of the HTML API revert to nested accessor calls. The emitters deliberately discard type information and reconstruct it through manual checks. Please make the following changes across the implementation and its consumers. 1. **Introduce an `XML` base struct and rename the concrete XML types.** The declarations in [xml/node.ss](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/node.ss#L14) are nine unrelated, lowercase struct types. Use an empty, extensible `XML` base and keep concrete types final: ```scheme (defstruct XML ()) (defstruct (XMLName XML) ((local :- :string) (uri :- :string) (prefix :- :string)) final: #t) (defstruct (XMLAttribute XML) ((name :- XMLName) (value :- :string)) final: #t) (defstruct (XMLElement XML) ((name :- XMLName) (attributes :- :list) (children :- :list)) final: #t) ``` Apply this mapping consistently: | Current type | Requested type | |---|---| | `xml-name` | `XMLName` | | `xml-attribute` | `XMLAttribute` | | `xml-element` | `XMLElement` | | `xml-text` | `XMLText` | | `xml-cdata` | `XMLCData` | | `xml-comment` | `XMLComment` | | `xml-pi` | `XMLPI` | | `xml-doctype` | `XMLDoctype` | | `xml-document` | `XMLDocument` | `XML` is a nominal model root. **`XML?` must not replace the predicate for valid element children:** names, attributes, documents, and doctypes are not legal children merely because they share that root. Preserve the existing context-specific restrictions. Rename the HTML additions to `HTMLRaw`, `HTMLDoctype`, and `HTMLDocument`, retaining their distinct representations and their reuse of `XMLElement`, `XMLAttribute`, etc. In particular, HTML and XML doctypes have different validity rules. Ordinary procedures such as `read-xml`, `xml-name=?`, and checked `make-xml-*` factories can retain conventional lowercase names. Update generated predicates and accessor names consistently. Preserve the checked-construction boundary; a naming migration must not replace validated construction with unchecked allocation. 2. **Convert consumers to typed dotted access, including nested fields.** The clearest cleanup targets are: | Location | Required change | |---|---| | [S3 response processing](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/net/s3/response.ss#L20) | Use `doc.root`, `name.local`, `name.uri`, `el.children`, and `el.name.local`. | | [TAL compiler](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/compiler.ss#L104) | Convert node inspection, attribute filtering, name comparison, and static/dynamic attribute handling. | | [TAL reader](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/reader.ss#L197) | Annotate attribute callbacks and use `attr.name.local` and `attr.value`. | | [HTML fragment reader](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/reader.ss#L65) | Establish the context element's type once, then use its typed fields. | | [Djot backlink handling](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/djot/html.ss#L738) | Narrow text/element values and use dotted fields when inspecting or rebuilding paragraphs. | For example, S3's name predicate should become: ```scheme (def (s3-name? (name : XMLName) (local : :string)) => :boolean (and (string=? name.local local) (or (string=? name.uri s3-namespace-uri) (string=? name.uri s3-doc-namespace-uri)))) ``` For an attribute list already established by the parser or constructor: ```scheme (filter-map (lambda ((attr :- XMLAttribute)) (let (local attr.name.local) (and (tal-name? local) [(substring local 4 (string-length local)) attr.value]))) el.attributes) ``` For a heterogeneous value, retain the actual dispatch and annotate the selected branch: ```scheme (cond ((XMLElement? node) (using (node :- XMLElement) node.name.local)) ...) ``` Use parameter signatures when the whole procedure operates on one concrete type. Use `using` for local narrowing or values retrieved from heterogeneous collections. Avoid repeated casts around individual field reads. 3. **Preserve type information instead of erasing and rechecking it.** [xml/node.ss:237](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/node.ss#L237) defines: ```scheme (defrule (untrusted expr) (:- expr :t)) ``` This defeats the typed model. For example, [XML attribute emission](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/emitter.ss#L348) reads an attribute through `using`, erases the types of its fields, checks them again, then repeats this for the name's fields. Resolve the ownership issue identified in the first review and remove this machinery under the documented no-mutation contract. Typed `attr.name`, `attr.value`, and `attr.name.local` should remain typed. Keep validation that types do not establish: namespace constraints, valid characters, duplicate attributes, legal child kinds, and serialization restrictions. Also retain element validation at public list-taking boundaries: `:list` establishes a proper list, not the types of its members. 4. **Replace unnecessarily broad signatures with the actual types.** Several known types are currently spelled `:t`: | Location | Requested annotation | |---|---| | [HTML builder `new-element`](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/builder.ss#L189) | Its `name` argument is `XMLName`. | | [HTML builder `freeze-element`](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/builder.ss#L2344) | Return `XMLElement`. | | [HTML builder `token-attribute`](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/builder.ss#L394) | Return `(Maybe :string)`; propagate that to encoding parameters. | | [HTML builder `attribute-table`](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/builder.ss#L706) | Return `HashTable`. | | [TAL template](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/reader.ss#L35) and [compiler](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/compiler.ss#L31) | Their `positions` fields are `HashTable`. | | [XML `emit-doctype!`](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/xml/emitter.ss#L618) | Its doctype argument is `XMLDoctype`. | Likewise, internal child-list procedures should receive `:list` once the boundary establishes that contract. Use `:? Type` for nullable parameters and fields, and precise return signatures. Some values genuinely remain heterogeneous: reader sources, HTML writer inputs that also accept fragments, scanner result registers, and user Scheme expressions. Keep those broad where necessary and narrow at dispatch points. 5. **Use forward types for the HTML builder's recursive links.** [html/builder.ss:166](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/html/builder.ss#L166) justifies six `:t` fields with “a slot type cannot name its own struct.” Gerbil provides forward type aliases. Rename `hnode` to `HTMLBuilderNode` and use: ```scheme (deftype @HTMLBuilderNode HTMLBuilderNode) ``` Its `parent`, `prev`, `next`, `first`, `last`, and `contents` fields can then use `:? @HTMLBuilderNode`; `name` can use `:? XMLName`. After a nonfalse check, access the typed value directly. This removes a real source of lost type information and repeated `using` blocks. The builder node remains an internal mutable construction representation, separate from the public XML records. 6. **Apply the convention to newly introduced implementation types and macros.** Use descriptive CamelCase names such as `XMLScanner`, `XMLEmitter`, `HTMLTokenizer`, `HTMLBuilder`, `HTMLEmitter`, `TALTemplate`, `TALCompiler`, and `TALRender`. Keep these implementation-state types outside the `XML` model hierarchy. Include field writes in the cleanup. For example, [TAL's emitter reset](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/text/markup/tal/compiler.ss#L52) should use typed bindings and `(set! em.pos 0)`. TAL runtime macros should establish a typed render binding and use fields such as `r.out.pos`. Preserve macro hygiene and single evaluation when adding bindings. Keep semantic operations—such as checked tokenizer operations—as procedures; distinguish those from generated field accessors. 7. **Complete the public API migration and verify it across module boundaries.** Update both public facades, type modules, signatures, imports, predicates, tests, and the XML/HTML/TAL/Djot documentation together. Public examples should demonstrate typed dotted access. Export the type information consumers need without exposing implementation structs or unchecked construction helpers through the facade. Acceptance checks should cover: - Common-base membership and unchanged concrete-type discrimination. - Rejection of names, attributes, doctypes, and documents in invalid child positions. - Nested dotted access through public imports, including nullable results. - Checked constructors and unchanged XML/HTML serialization behavior. - S3 consumers, Djot rendering, and compiled TAL hygiene. - Remaining accessor calls reviewed deliberately, rather than mechanically renamed. Follow [src/std/AGENTS.md](https://git.cons.io/mighty-gerbils/gerbil/src/commit/49b02af0585ee3884039e7b46477ce70cf401ce2/src/std/AGENTS.md) for aligned, one-argument-per-line signatures and typed callbacks. Avoid blanket unchecked annotations where the input type has not been established. **Validation for this pass:** static source review plus small interpreted checks confirming struct inheritance, nested dotted access, and nullable recursive fields through `deftype`. No repository files were changed, and no build or regression suite was run.
Owner

if we are getting a new implementation of string->utf8 and utf8->string again, then abstract them into a separate module. we had that before, but went back to gambit forms. either in :std/string/utf8 or (better i think) in :std/encoding/utf8.

if we are getting a new implementation of string->utf8 and utf8->string again, then abstract them into a separate module. we had that before, but went back to gambit forms. either in `:std/string/utf8` or (better i think) in `:std/encoding/utf8`.
Owner

@jay wrote in #1515 (comment):

@vyzo addressing your feedback in separate messages so it's easy to reply to indivdually

1. HTML nested more than 512 elements deep

Parser What it does Follows browser rules?
Old Gerbil html->sxml No limit, stays fast. But it doesn't apply browser rules even at depth 2: <div><span><span>a</div><p>after</p> comes out as (div (span (span a)) (span (p after))). The review's example is broken in the old library at any depth. No
Go x/net/html Refuses the document: "open stack of elements exceeds 512 nodes". Yes
Clojure (hickory uses jsoup) jsoup closes the innermost element once 512 are open. You can change the limit with setMaxDepth. Yes
Safari (WebKit) Same as jsoup: closes the innermost element past 512. Yes
Chrome, Firefox Past 512, attach new elements one level up, but still search every open element. That gets slow on very deep input. Yes
Python html5lib, Rust html5ever No limit, full search, so slow on very deep input. Yes
Racket html-parsing No browser rules at all, like the old Gerbil library. No

What do you think we should do? I would lean towards explicitly refuse like Go does, seems the simplest.

yeah, let's refuse and raise an error.

@jay wrote in https://git.cons.io/mighty-gerbils/gerbil/pulls/1515#issuecomment-3056: > @vyzo addressing your feedback in separate messages so it's easy to reply to indivdually > > 1. HTML nested more than 512 elements deep > > > Parser What it does Follows browser rules? > Old Gerbil `html->sxml` No limit, stays fast. But it doesn't apply browser rules even at depth 2: `<div><span><span>a</div><p>after</p>` comes out as `(div (span (span a)) (span (p after)))`. The review's example is broken in the old library at any depth. No > Go `x/net/html` Refuses the document: "open stack of elements exceeds 512 nodes". Yes > Clojure (hickory uses jsoup) jsoup closes the innermost element once 512 are open. You can change the limit with `setMaxDepth`. Yes > Safari (WebKit) Same as jsoup: closes the innermost element past 512. Yes > Chrome, Firefox Past 512, attach new elements one level up, but still search every open element. That gets slow on very deep input. Yes > Python html5lib, Rust html5ever No limit, full search, so slow on very deep input. Yes > Racket `html-parsing` No browser rules at all, like the old Gerbil library. No > > What do you think we should do? I would lean towards explicitly refuse like Go does, seems the simplest. yeah, let's refuse and raise an error.
Owner

@jay wrote in #1515 (comment):

Start with the buffered/memory writers and adapters already available through :std/io, including with-buffered-writer.

Opus measured this and it's much slower. Our writer escapes and UTF-8-encodes in one pass straight into a byte buffer. BufferedWriter has no public way to write into its buffer directly (correct?), so the same work through its methods is 2.6× slower with write-string-utf8 and 8.6× slower with write-char-utf8.

So I'll keep a small byte buffer, but move it out of the XML emitter so HTML and TAL stop depending on it if that sounds good.

If you'd take a public way to reserve space in a BufferedWriter's buffer, we can do that in a separate PR?

ok, let's leave that for follow up work.

@jay wrote in https://git.cons.io/mighty-gerbils/gerbil/pulls/1515#issuecomment-3059: > > Start with the buffered/memory writers and adapters already available through :std/io, including with-buffered-writer. > > Opus measured this and it's much slower. Our writer escapes and UTF-8-encodes in one pass straight into a byte buffer. `BufferedWriter` has no public way to write into its buffer directly (correct?), so the same work through its methods is 2.6× slower with `write-string-utf8` and 8.6× slower with `write-char-utf8`. > > So I'll keep a small byte buffer, but move it out of the XML emitter so HTML and TAL stop depending on it if that sounds good. > > If you'd take a public way to reserve space in a BufferedWriter's buffer, we can do that in a separate PR? ok, let's leave that for follow up work.
Owner

@jay wrote in #1515 (comment):

Use existing conversions for those cases.

Tried that and they're slower. utf8->string makes xml->string 10–19% slower end to end, and string->utf8 slows string input by 16–44%, because both make two passes over the data. So we can go with your alternative of one shared decoder and validator. Encoding was already a single macro.

as i said above, let's make std/encoding/utf8 for the fast variants. we had them before gambit had native procedures.

@jay wrote in https://git.cons.io/mighty-gerbils/gerbil/pulls/1515#issuecomment-3063: > > Use existing conversions for those cases. > > Tried that and they're slower. `utf8->string` makes `xml->string` 10–19% slower end to end, and `string->utf8` slows string input by 16–44%, because both make two passes over the data. So we can go with your alternative of one shared decoder and validator. Encoding was already a single macro. as i said above, let's make std/encoding/utf8 for the fast variants. we had them before gambit had native procedures.
Owner

@jay wrote in #1515 (comment):

I think the Djot test failure is your installed runtime: char-set:punctuation came in with #1509.

The TAL hygiene failure is my test's fault. It assumed compiled TAL metadata was on the loadpath, and it'll skip when it isn't.

yeah, makes sense.

@jay wrote in https://git.cons.io/mighty-gerbils/gerbil/pulls/1515#issuecomment-3064: > I think the Djot test failure is your installed runtime: `char-set:punctuation` came in with #1509. > > The TAL hygiene failure is my test's fault. It assumed compiled TAL metadata was on the loadpath, and it'll skip when it isn't. yeah, makes sense.
HTML: normalize CR after script tags in escaped script data; refuse documents nested deeper than 512 elements instead of searching a 512-entry window; read lone surrogates in string input as U+FFFD; reject upper-case HTML attribute names the parser would fold.

XML: check the declaration's version, encoding name and US-ASCII bytes; resolve prefixes through per-prefix binding stacks; make write-xml reject comments and namespace declarations XML cannot represent.

Detect duplicate attributes pairwise up to 16 and by sorting above, instead of pairwise always or with a string hash table, which colliding names can flood.
Allow CDATA where the current element is foreign; write the LF after pre, listing and textarea start tags before dynamic content; bind every tal:repeat method; compile conditional tal:omit-tag content once; reject templates the parser would read differently; check as-is: content of raw text elements; skip the compiled hygiene test without compiled TAL. write-html applies the svg/math namespace rule at the top level.
Nodes are documented as immutable values; mutating one is undefined behaviour. The writers and constructors no longer re-check slot types, copy strings or snapshot attributes, and the node modules no longer export slot setters.
The writer, tree builder and TAL use one module for namespaces, contexts, element classes, raw text, foreign names and written names. TAL no longer rewrites emitter output. Djot names elements with the parser's interned names. Replaces private helpers with list->hash-table-string and string-contains.
XMLName, XMLElement and the other node types derive from an empty XML struct; implementation types are CamelCase. The HTML builder links nodes through a forward type, signatures name their types, and S3, TAL and Djot use dotted access.
One input module reads every source kind through read-all-as-string and read-all-from-reader. The XML scanner validates UTF-8 as it scans. Byte output moves out of XMLEmitter into a ByteOutput that the HTML writer, TAL and Djot share.
Author
Member

@vyzo feedback addressed!

@vyzo feedback addressed!
Owner

ok, i am doing another astra pass and if she's happy i'll do a manual pass.

ok, i am doing another astra pass and if she's happy i'll do a manual pass.
vyzo requested changes 2026-10-28 16:06:43 +00:00
Dismissed
vyzo left a comment

Third review pass: coding conventions, abstraction, and remaining complexity

I would still request changes on 97080fa9, although this revision substantially addresses the previous reviews. The XML hierarchy, CamelCase names, typed dotted access, recursive types, and removal of type-erasing defensive checks are largely done.

The remaining concerns are more concentrated: one demonstrated performance problem, an unnecessarily complicated rule-sharing mechanism, and some unresolved API and maintenance issues.

  1. [P2] TAL now does quadratic work on ordinary nested elements.

    In tal/reader.ss:209, every HTML start tag calls open-p-in-button-scope? before checking whether that tag could close a paragraph. The scope check walks the open-element stack.

    Consequently, nested <span> elements repeatedly scan all their ancestors—even when the template contains no paragraph at all. I reproduced the following timings for read-tal-template:

    Nested spans Elapsed seconds
    100 0.027
    200 0.144
    400 0.474
    800 1.794

    These are interpreted timings, not compiled benchmarks, but the quadratic traversal is explicit in the implementation.

    Requested change: perform a cheap start-tag classification before searching ancestors. Where repeated scope queries remain necessary, maintain the relevant scope information in the open-element state. Add a nested non-paragraph regression case and a reproducible scaling check.

  2. TAL invokes the HTML parser to implement a classification predicate. This is the clearest remaining abstraction problem.

    html-start-tag-closes-p? constructs "<p><" + name + ">", creates scanner input and a tokenizer, builds a fragment, and examines its resulting sibling list.

    That is an excessive amount of machinery for deciding whether a start tag closes a paragraph. It also makes the decision indirectly depend on a synthetic fragment's parsing context.

    Requested change: extract the actual rule into html/rules.ss and have both the builder and TAL use it. Represent context-dependent conditions explicitly. Avoid either a second copied tag list or memoization around the miniature parse; neither fixes the underlying abstraction.

    This belongs with the previous finding: simply reversing the current predicates would put a miniature HTML parse on every start tag.

  3. The ordinary public type constructors still bypass the advertised construction boundary.

    xml/types.ss:9–14 now explicitly documents that applying an exported type identifier invokes unchecked allocation. Thus make-xml-cdata validates its input, while the natural CamelCase spelling (XMLCData "]]>") bypasses that validation.

    This is an API design objection, not an undocumented contract violation: the new comment acknowledges the behavior. Nevertheless, documenting undefined behavior for ordinary constructor syntax leaves the checked-construction boundary weaker than the previous review requested.

    Requested change: make normal public construction perform validation, using the existing constructor/initializer mechanisms, and retain a clearly internal allocation path for parsers that already establish the invariants. Preserve type exports for signatures and dotted access. Do not compensate by restoring repeated validation throughout the writers.

  4. There are still private replacements for existing runtime functionality, with insufficient reproducible justification.

    Two specific examples deserve another pass:

    • html/rules.ss:103–134 implements its own 64-bucket string hash table, including hashing, bucket construction, and lookup. Existing string-keyed hash tables should be the baseline. The atom-indexed vectors are a separate, reasonable specialization.
    • xml/node.ss:249–262 implements string equality as an expanded character loop. Its justification compares against checked, n-ary string=?; the runtime already uses the binary ##string=? primitive in its string-table implementation.

    Requested change: compare these implementations against the appropriate existing facilities under compiled execution. Prefer the existing implementation unless the specialized version demonstrates a meaningful benefit. If retained, include the benchmark and explain the workload that warrants the maintenance cost.

    The same standard should apply to the private &string-empty? macro. A performance comment alone does not let subsequent maintainers reproduce or reassess the decision.

  5. The strongest conformance and optimization evidence remains outside the repository.

    The PR reports substantial external HTML conformance testing, but the runner is not included. The checked-in focused tests are useful; they do not reproduce those corpus-wide claims. Several optimization comments likewise give precise measurements without the corresponding driver.

    Requested change: include a runner accepting locally supplied upstream fixtures, document pinned revisions and invocation commands, and make expected skips explicit and checked. Include the small benchmark drivers supporting the retained custom machinery. The existing Djot conformance setup provides a useful precedent.

A smaller, nonblocking macro opportunity remains in html/tokenizer.ss:133–167: seven field wrappers repeat the same typed-argument, kind-check, and dotted-access pattern. A small declaration macro could generate those procedures while preserving their signatures and error attribution. Keep accessors that perform actual conversion explicit.

Validation: all 11 selected UTF-8, XML, HTML, and S3 test modules passed using the updated sources with the installed interpreter. Targeted TAL checks also passed for dynamic textarea newlines, quoted repeat-method references, and foreign-content CDATA. I did not run a full build or the external conformance corpus. No repository files were changed.

**Third review pass: coding conventions, abstraction, and remaining complexity** I would still **request changes on `97080fa9`**, although this revision substantially addresses the previous reviews. The `XML` hierarchy, CamelCase names, typed dotted access, recursive types, and removal of type-erasing defensive checks are largely done. The remaining concerns are more concentrated: one demonstrated performance problem, an unnecessarily complicated rule-sharing mechanism, and some unresolved API and maintenance issues. 1. **[P2] TAL now does quadratic work on ordinary nested elements.** In [tal/reader.ss:209](https://git.cons.io/mighty-gerbils/gerbil/src/commit/97080fa9d83a3fd5bd04facb9c5bd55a463c664c/src/std/text/markup/tal/reader.ss#L209), every HTML start tag calls `open-p-in-button-scope?` before checking whether that tag could close a paragraph. The scope check walks the open-element stack. Consequently, nested `<span>` elements repeatedly scan all their ancestors—even when the template contains no paragraph at all. I reproduced the following timings for `read-tal-template`: | Nested spans | Elapsed seconds | |---:|---:| | 100 | 0.027 | | 200 | 0.144 | | 400 | 0.474 | | 800 | 1.794 | These are interpreted timings, not compiled benchmarks, but the quadratic traversal is explicit in the implementation. **Requested change:** perform a cheap start-tag classification before searching ancestors. Where repeated scope queries remain necessary, maintain the relevant scope information in the open-element state. Add a nested non-paragraph regression case and a reproducible scaling check. 2. **TAL invokes the HTML parser to implement a classification predicate. This is the clearest remaining abstraction problem.** [html-start-tag-closes-p?](https://git.cons.io/mighty-gerbils/gerbil/src/commit/97080fa9d83a3fd5bd04facb9c5bd55a463c664c/src/std/text/markup/tal/reader.ss#L58) constructs `"<p><" + name + ">"`, creates scanner input and a tokenizer, builds a fragment, and examines its resulting sibling list. That is an excessive amount of machinery for deciding whether a start tag closes a paragraph. It also makes the decision indirectly depend on a synthetic fragment's parsing context. **Requested change:** extract the actual rule into `html/rules.ss` and have both the builder and TAL use it. Represent context-dependent conditions explicitly. Avoid either a second copied tag list or memoization around the miniature parse; neither fixes the underlying abstraction. This belongs with the previous finding: simply reversing the current predicates would put a miniature HTML parse on every start tag. 3. **The ordinary public type constructors still bypass the advertised construction boundary.** [xml/types.ss:9–14](https://git.cons.io/mighty-gerbils/gerbil/src/commit/97080fa9d83a3fd5bd04facb9c5bd55a463c664c/src/std/text/markup/xml/types.ss#L9) now explicitly documents that applying an exported type identifier invokes unchecked allocation. Thus `make-xml-cdata` validates its input, while the natural CamelCase spelling `(XMLCData "]]>")` bypasses that validation. This is **an API design objection, not an undocumented contract violation**: the new comment acknowledges the behavior. Nevertheless, documenting undefined behavior for ordinary constructor syntax leaves the checked-construction boundary weaker than the previous review requested. **Requested change:** make normal public construction perform validation, using the existing constructor/initializer mechanisms, and retain a clearly internal allocation path for parsers that already establish the invariants. Preserve type exports for signatures and dotted access. Do not compensate by restoring repeated validation throughout the writers. 4. **There are still private replacements for existing runtime functionality, with insufficient reproducible justification.** Two specific examples deserve another pass: - [html/rules.ss:103–134](https://git.cons.io/mighty-gerbils/gerbil/src/commit/97080fa9d83a3fd5bd04facb9c5bd55a463c664c/src/std/text/markup/html/rules.ss#L103) implements its own 64-bucket string hash table, including hashing, bucket construction, and lookup. Existing string-keyed hash tables should be the baseline. The atom-indexed vectors are a separate, reasonable specialization. - [xml/node.ss:249–262](https://git.cons.io/mighty-gerbils/gerbil/src/commit/97080fa9d83a3fd5bd04facb9c5bd55a463c664c/src/std/text/markup/xml/node.ss#L249) implements string equality as an expanded character loop. Its justification compares against checked, n-ary `string=?`; the runtime already uses the binary `##string=?` primitive in its string-table implementation. **Requested change:** compare these implementations against the appropriate existing facilities under compiled execution. Prefer the existing implementation unless the specialized version demonstrates a meaningful benefit. If retained, include the benchmark and explain the workload that warrants the maintenance cost. The same standard should apply to the private `&string-empty?` macro. A performance comment alone does not let subsequent maintainers reproduce or reassess the decision. 5. **The strongest conformance and optimization evidence remains outside the repository.** The PR reports substantial external HTML conformance testing, but the runner is not included. The checked-in focused tests are useful; they do not reproduce those corpus-wide claims. Several optimization comments likewise give precise measurements without the corresponding driver. **Requested change:** include a runner accepting locally supplied upstream fixtures, document pinned revisions and invocation commands, and make expected skips explicit and checked. Include the small benchmark drivers supporting the retained custom machinery. The existing Djot conformance setup provides a useful precedent. A smaller, nonblocking macro opportunity remains in [html/tokenizer.ss:133–167](https://git.cons.io/mighty-gerbils/gerbil/src/commit/97080fa9d83a3fd5bd04facb9c5bd55a463c664c/src/std/text/markup/html/tokenizer.ss#L133): seven field wrappers repeat the same typed-argument, kind-check, and dotted-access pattern. A small declaration macro could generate those procedures while preserving their signatures and error attribution. Keep accessors that perform actual conversion explicit. **Validation:** all 11 selected UTF-8, XML, HTML, and S3 test modules passed using the updated sources with the installed interpreter. Targeted TAL checks also passed for dynamic textarea newlines, quoted repeat-method references, and foreign-content CDATA. I did not run a full build or the external conformance corpus. No repository files were changed.
Owner

astra still has complaints

astra still has complaints
TAL keeps p and list item scope on its open elements instead of searching ancestors per start tag. One rule in html/rules.ss decides which start tags close a p; the tree builder and TAL use it, and the miniature parse is gone. Applying a node type checks its arguments. Specialised code that stays cites a reproducible measurement. Adds html-conformance.ss and markup-benchmark.ss, standalone programs like djot-benchmark.ss. The tokenizer field accessors come from one macro.
The TAL compiler finds its dynamic elements in one bottom-up pass, and the tree builder records on each open element whether a p is in button scope, so neither walks ancestors per element.
write-xml looks up prefixes through per-prefix stacks with a per-element undo list, as the reader does. Also states what the writers trust, removes dead exports and gives each kept specialisation a reproducible measurement.
Merges repeated html and body attributes once, locates TAL template offsets by binary search, bounds the XML writer's search past rebound prefixes and matches tal:attributes through tables. Element categories and the font breakout rule move to html/rules.ss, so TAL no longer imports the tree builder. The make- factories are the constructors.
One pairwise-duplicate macro and one p-in-button-scope rule; TAL computes template columns once and closes optional end tags by atom. UTF-8 sizing uses the runtime primitive, and the read chunk is chosen by measurement.
A 4 KB first buffer is not movable, so every small render paid for it in garbage collection; the comment cites the measurement.
Author
Member

@vyzo latest feedback addressed!

@vyzo latest feedback addressed!
vyzo approved these changes 2026-10-30 22:38:12 +00:00
Dismissed
vyzo left a comment

Fourth review pass: approval with minor follow-ups

Reviewed 3b85ab6d. The previous blocking findings are addressed; this pass found no new merge-blocking defect.

The implementation now resolves the earlier concerns directly:

  • TAL uses shared paragraph-closing rules and cached scope information. The miniature parse and repeated ancestor scans are gone.
  • Public XML/HTML constructors validate arguments. Unchecked allocation is explicitly internal and consolidated in one macro.
  • TAL computes dynamic-subtree information once, and attribute merging avoids repeated searches.
  • The duplicate-detection loops and tokenizer field wrappers are generalized.
  • The retained specialized implementations now cite concrete benchmark workloads, including comparison against ##string=?. I haven't independently verified those compiled performance figures, but the earlier lack of reproducible workloads is resolved.

Two minor follow-ups remain:

  1. [P3] The conformance runner's exception-reporting path throws another exception.

    In html-conformance.ss:303–308, a tokenizer exception becomes a list containing its message string. That value then goes through normalize, which expects each entry to be a token list and calls car on it.

    If tokenization raises, the runner therefore aborts during normalization instead of recording the original failure with its case ID and continuing.

    Fix: record exceptions separately from token results, or wrap execution and comparison together and append a failure from the handler. Preserve the original exception and case ID.

  2. Small convention cleanup.

    markup-benchmark.ss:27 still puts both parameters on one line, contrary to the signature convention. git diff --check also reports an extra blank line at EOF in html/reader-test.ss.

    These are minor; I don't see justification for another broad abstraction or naming rewrite.

Validation was substantially stronger this time:

Check Result
UTF-8, XML, HTML, S3 and TAL 12 focused modules passed
Pinned html5lib tokenizer corpus 7,045 passed; 8 documented skips
Pinned WPT tree construction 1,963 passed; 4 documented skips
Writer round-trip/rejection checks 1,950 passed; 17 documented skips
1,800 generated HTML inputs, previous versus updated revision Identical serialized results

The original 800-level TAL example fell from approximately 1.79 seconds to 0.072 seconds in interpreted execution. The source inspection confirms that the quadratic scope traversal was removed.

Testing used the updated sources with the installed interpreter. Compiled TAL hygiene was skipped because optimizer metadata was unavailable. Djot tests could not load because the older installed runtime lacks char-set:punctuation; this is an environment limitation, not a demonstrated PR regression.

No source files were changed.

**Fourth review pass: approval with minor follow-ups** Reviewed **`3b85ab6d`**. The previous blocking findings are addressed; this pass found no new merge-blocking defect. The implementation now resolves the earlier concerns directly: - TAL uses shared paragraph-closing rules and cached scope information. The miniature parse and repeated ancestor scans are gone. - Public XML/HTML constructors validate arguments. Unchecked allocation is explicitly internal and consolidated in one macro. - TAL computes dynamic-subtree information once, and attribute merging avoids repeated searches. - The duplicate-detection loops and tokenizer field wrappers are generalized. - The retained specialized implementations now cite concrete benchmark workloads, including comparison against `##string=?`. I haven't independently verified those compiled performance figures, but the earlier lack of reproducible workloads is resolved. Two minor follow-ups remain: 1. **[P3] The conformance runner's exception-reporting path throws another exception.** In [html-conformance.ss:303–308](https://git.cons.io/mighty-gerbils/gerbil/src/commit/3b85ab6de6cbf483fb12bf87d7b0a11099051ef3/src/std/text/markup/html-conformance.ss#L303), a tokenizer exception becomes a list containing its message string. That value then goes through `normalize`, which expects each entry to be a token list and calls `car` on it. If tokenization raises, the runner therefore aborts during normalization instead of recording the original failure with its case ID and continuing. **Fix:** record exceptions separately from token results, or wrap execution and comparison together and append a failure from the handler. Preserve the original exception and case ID. 2. **Small convention cleanup.** [markup-benchmark.ss:27](https://git.cons.io/mighty-gerbils/gerbil/src/commit/3b85ab6de6cbf483fb12bf87d7b0a11099051ef3/src/std/text/markup/markup-benchmark.ss#L27) still puts both parameters on one line, contrary to the signature convention. `git diff --check` also reports an extra blank line at EOF in `html/reader-test.ss`. These are minor; I don't see justification for another broad abstraction or naming rewrite. Validation was substantially stronger this time: | Check | Result | |---|---| | UTF-8, XML, HTML, S3 and TAL | 12 focused modules passed | | Pinned html5lib tokenizer corpus | 7,045 passed; 8 documented skips | | Pinned WPT tree construction | 1,963 passed; 4 documented skips | | Writer round-trip/rejection checks | 1,950 passed; 17 documented skips | | 1,800 generated HTML inputs, previous versus updated revision | Identical serialized results | The original 800-level TAL example fell from approximately **1.79 seconds to 0.072 seconds** in interpreted execution. The source inspection confirms that the quadratic scope traversal was removed. Testing used the updated sources with the installed interpreter. Compiled TAL hygiene was skipped because optimizer metadata was unavailable. Djot tests could not load because the older installed runtime lacks `char-set:punctuation`; this is an environment limitation, not a demonstrated PR regression. No source files were changed.
vyzo approved these changes 2026-10-30 22:38:26 +00:00
Dismissed
vyzo left a comment

Fourth review pass: approval with minor follow-ups

Reviewed 3b85ab6d. The previous blocking findings are addressed; this pass found no new merge-blocking defect.

The implementation now resolves the earlier concerns directly:

  • TAL uses shared paragraph-closing rules and cached scope information. The miniature parse and repeated ancestor scans are gone.
  • Public XML/HTML constructors validate arguments. Unchecked allocation is explicitly internal and consolidated in one macro.
  • TAL computes dynamic-subtree information once, and attribute merging avoids repeated searches.
  • The duplicate-detection loops and tokenizer field wrappers are generalized.
  • The retained specialized implementations now cite concrete benchmark workloads, including comparison against ##string=?. I haven't independently verified those compiled performance figures, but the earlier lack of reproducible workloads is resolved.

Two minor follow-ups remain:

  1. [P3] The conformance runner's exception-reporting path throws another exception.

    In html-conformance.ss:303–308, a tokenizer exception becomes a list containing its message string. That value then goes through normalize, which expects each entry to be a token list and calls car on it.

    If tokenization raises, the runner therefore aborts during normalization instead of recording the original failure with its case ID and continuing.

    Fix: record exceptions separately from token results, or wrap execution and comparison together and append a failure from the handler. Preserve the original exception and case ID.

  2. Small convention cleanup.

    markup-benchmark.ss:27 still puts both parameters on one line, contrary to the signature convention. git diff --check also reports an extra blank line at EOF in html/reader-test.ss.

    These are minor; I don't see justification for another broad abstraction or naming rewrite.

Validation was substantially stronger this time:

Check Result
UTF-8, XML, HTML, S3 and TAL 12 focused modules passed
Pinned html5lib tokenizer corpus 7,045 passed; 8 documented skips
Pinned WPT tree construction 1,963 passed; 4 documented skips
Writer round-trip/rejection checks 1,950 passed; 17 documented skips
1,800 generated HTML inputs, previous versus updated revision Identical serialized results

The original 800-level TAL example fell from approximately 1.79 seconds to 0.072 seconds in interpreted execution. The source inspection confirms that the quadratic scope traversal was removed.

Testing used the updated sources with the installed interpreter. Compiled TAL hygiene was skipped because optimizer metadata was unavailable. Djot tests could not load because the older installed runtime lacks char-set:punctuation; this is an environment limitation, not a demonstrated PR regression.

No source files were changed.

**Fourth review pass: approval with minor follow-ups** Reviewed **`3b85ab6d`**. The previous blocking findings are addressed; this pass found no new merge-blocking defect. The implementation now resolves the earlier concerns directly: - TAL uses shared paragraph-closing rules and cached scope information. The miniature parse and repeated ancestor scans are gone. - Public XML/HTML constructors validate arguments. Unchecked allocation is explicitly internal and consolidated in one macro. - TAL computes dynamic-subtree information once, and attribute merging avoids repeated searches. - The duplicate-detection loops and tokenizer field wrappers are generalized. - The retained specialized implementations now cite concrete benchmark workloads, including comparison against `##string=?`. I haven't independently verified those compiled performance figures, but the earlier lack of reproducible workloads is resolved. Two minor follow-ups remain: 1. **[P3] The conformance runner's exception-reporting path throws another exception.** In [html-conformance.ss:303–308](https://git.cons.io/mighty-gerbils/gerbil/src/commit/3b85ab6de6cbf483fb12bf87d7b0a11099051ef3/src/std/text/markup/html-conformance.ss#L303), a tokenizer exception becomes a list containing its message string. That value then goes through `normalize`, which expects each entry to be a token list and calls `car` on it. If tokenization raises, the runner therefore aborts during normalization instead of recording the original failure with its case ID and continuing. **Fix:** record exceptions separately from token results, or wrap execution and comparison together and append a failure from the handler. Preserve the original exception and case ID. 2. **Small convention cleanup.** [markup-benchmark.ss:27](https://git.cons.io/mighty-gerbils/gerbil/src/commit/3b85ab6de6cbf483fb12bf87d7b0a11099051ef3/src/std/text/markup/markup-benchmark.ss#L27) still puts both parameters on one line, contrary to the signature convention. `git diff --check` also reports an extra blank line at EOF in `html/reader-test.ss`. These are minor; I don't see justification for another broad abstraction or naming rewrite. Validation was substantially stronger this time: | Check | Result | |---|---| | UTF-8, XML, HTML, S3 and TAL | 12 focused modules passed | | Pinned html5lib tokenizer corpus | 7,045 passed; 8 documented skips | | Pinned WPT tree construction | 1,963 passed; 4 documented skips | | Writer round-trip/rejection checks | 1,950 passed; 17 documented skips | | 1,800 generated HTML inputs, previous versus updated revision | Identical serialized results | The original 800-level TAL example fell from approximately **1.79 seconds to 0.072 seconds** in interpreted execution. The source inspection confirms that the quadratic scope traversal was removed. Testing used the updated sources with the installed interpreter. Compiled TAL hygiene was skipped because optimizer metadata was unavailable. Djot tests could not load because the older installed runtime lacks `char-set:punctuation`; this is an environment limitation, not a demonstrated PR regression. No source files were changed.
Owner

astra is satisfied; let's address the minor follow ups and i'll do a manual pass tomorrow.

astra is satisfied; let's address the minor follow ups and i'll do a manual pass tomorrow.
Owner

@jay can you post the latest djot benchmarks? I want to see our progress.

@jay can you post the latest djot benchmarks? I want to see our progress.
Author
Member

@jay can you post the latest djot benchmarks? I want to see our progress.

File Gerbil with typed XML Gerbil current v0.19-staging JavaScript C
Pandoc manual 15 ms 85 ms 12 ms 2 ms
Tartan 106 ms 421 ms 123 ms 17 ms
  • Since #1509: we're 4–6× faster.
  • Against C: we're about 6–7× slower (up from 36–55× slower).
  • Against JavaScript: we're about even

@vyzo I have a local branch that speeds up the Djot parsing by about 2x, at some point I will get a PR out for that.

> @jay can you post the latest djot benchmarks? I want to see our progress. | File | Gerbil with typed XML | Gerbil current v0.19-staging | JavaScript | C | |---|---:|---:|---:|---:| | Pandoc manual | 15 ms | 85 ms | 12 ms | 2 ms | | Tartan | 106 ms | 421 ms | 123 ms | 17 ms | - **Since #1509:** we're 4–6× faster. - **Against C:** we're about 6–7× slower (up from 36–55× slower). - **Against JavaScript:** we're about even @vyzo I have a local branch that speeds up the Djot parsing by about 2x, at some point I will get a PR out for that.
Author
Member

@vyzo also if it's faster for you to make any changes in the name of style / idiomaticity yourself, you are more than welcome to. otherwise I will address whatever additional feedback you have

@vyzo also if it's faster for you to make any changes in the name of style / idiomaticity yourself, you are more than welcome to. otherwise I will address whatever additional feedback you have
Owner

yeah, it's fine. i'll just do a pass myself to see if anything stands out.

yeah, it's fine. i'll just do a pass myself to see if anything stands out.
vyzo left a comment

now i have some concerns.

now i have some concerns.
@ -0,0 +119,4 @@
(start :- :fixnum)
(end :- :fixnum)
(tail :- :fixnum))
=> :u8vector
Owner

shouldn't we just shrink the u8vector to the right size instead of filling the end?

shouldn't we just shrink the u8vector to the right size instead of filling the end?
@ -0,0 +57,4 @@
(defconst ignored-column-group 2)
;;; An emitter at the top level, writing to out.
(def (make-html-emitter (out :- ByteOutput)
Owner

what is a ByteOutput? can't we just use a BufferedWriter?

what is a ByteOutput? can't we just use a BufferedWriter?
@ -0,0 +21,4 @@
;;; BufferedWriter), a byte port, or a character port (as characters).
;;; Raises ContractViolation for any tree the HTML parser would not read
;;; back as written; html.md gives the output and every rejected case.
(def (write-html (node : :t)
Owner

the types are too wide here.

the types are too wide here.
@ -0,0 +24,4 @@
(def (write-html (node : :t)
(sink : :t := (current-output-port))
scripting?: (scripting? : :boolean := #t)) => :void
(check-html-node node write-html)
Owner

this could be typed at the procedure signature instead of a check here.

this could be typed at the procedure signature instead of a check here.
@ -0,0 +36,4 @@
emit-ascii!)
(declare (not safe))
(defstruct ByteOutput
Owner

this effectively duplicates the memory output buffer.

this effectively duplicates the memory output buffer.
Owner

i have concerns, i'll ask astra about another pass more targeted.

i have concerns, i'll ask astra about another pass more targeted.
vyzo requested changes 2026-10-31 10:55:05 +00:00
Dismissed
vyzo left a comment

Follow-up review: consolidate ByteOutput into stdio and tighten type contracts

Request changes on 91e655af. This supersedes my earlier approval. The separate buffering implementation needs to be consolidated into stdio before merge. The type-contract cleanup below is also part of the requested changes.

I checked the update from 3b85ab6d to 91e655af: it addresses the earlier tokenizer-exception reporting and formatting findings. Those findings are resolved by source inspection. The output implementation and the production signatures discussed below are unchanged.

1. Consolidate ByteOutput into stdio; do not maintain a second buffering subsystem.

markup/output.ss:39 defines a private buffer, cursor and optional Writer sink, then implements allocation, growth, reservation, flushing and extraction. These are stdio responsibilities. The missing public reservation operation is a reason to extend BufferedWriter interface and its stdio buffer APIs, not a reason to maintain parallel buffering inside markup or defer consolidation to another PR.

Required implementation direction:

  • Add a writable-space reservation operation to BufferedWriter and a way to commit the actual number of bytes produced. Reservation is an upper bound; reserving space must not itself advance the logical output position.
  • Specify the returned buffer/range and its lifetime. No intervening writer operation may invalidate a reservation before it is committed. Never flush uncommitted bytes. Define bounds checks and failure behavior.
  • Support the existing memory, sink, binary-port and textual-port backends. The binary-port backend currently delegates directly to Gambit and does not expose an owned byte buffer; account for that explicitly in the implementation.
  • Replace ByteOutput throughout XML, HTML, TAL and scanner scratch output. Put reusable memory-buffer inspection/reset/truncation operations in stdio as needed. Preserve TAL's save-position/rollback behavior when discarding output from a failed statement.
  • Keep markup-specific escaping and emission macros in markup. Buffer ownership, allocation, growth, cursor advancement, flushing and extraction belong in stdio. A thin adapter is fine; retaining the independent buffering engine behind a renamed adapter is not consolidation.
  • Preserve caller ownership of writers/ports, textual-port UTF-8 behavior and the existing externally visible output/error behavior.

Validate reservation larger than current capacity, partial commit, growth preserving prior bytes, UTF-8 split boundaries and sink failures, then rerun the relevant markup/stdio regressions, conformance checks and benchmarks. This is a blocking architectural requirement, not a cosmetic rename.

2. Express the public writer domain in contracts instead of accepting :t and checking it in the body.

xml/writer.ss:12 and html/writer.ss:15 declare node : :t, then immediately call check-xml-node / check-html-node. Centralize the accepted domain in reusable predicate contracts and use those in all three public writer entry points for each format.

For XML, the intended shape is:

(def (xml-writable? (x : :t)) => :boolean
  (or (XMLDocument? x) (xml-node? x)))

;; Parameter annotation:
(node :~ xml-writable? :- XML)

Do not simply replace this with XML?: the base class also includes names, attributes and other objects that are not standalone writable nodes. For HTML, define the corresponding accepted domain for a document, a node, or a proper list of permitted fragment nodes. Keep semantic serialization checks in the emitter; a signature cannot establish namespace consistency or round-trip validity.

3. Put list element types in constructor contracts; remove the resulting duplicate member checks.

Concrete sites:

Use the existing list-of? contract machinery where it expresses the domain, for example:

(attributes :~ (list-of? XMLAttribute?) :- :list)
(children   :~ (list-of? xml-node?) :- :list)

Use the appropriate predicate for each position; document prolog/epilog do not accept arbitrary XML nodes. These contracts establish proper lists and member types. After strengthening the contracts, remove manual checks that merely repeat those facts, and use typed callbacks/dotted access for the validated members. Retain checks for duplicate attributes, namespace agreement, doctype ordering and other relationships between values.

The current :list annotations do NOT establish member types, so deleting the body checks without strengthening the contracts would be wrong.

4. Narrow internal parameters and return types where call sites establish a smaller domain.

Location Requested change
tal/compiler.ss:70 code! receives constructed nonempty Scheme forms at its call sites. Change form :- :t to form :- :pair; arbitrary user expressions can still occur inside those forms.
tal/reader.ss:95 optional-end-closers-of returns a list or the sentinel 0, forcing => :t. Use #f for absence and => (Maybe :list); update callers accordingly.
djot/html.ss:687 newline-text? and paragraph? inspect internally rendered XML/HTML nodes. Give their parameter the established XML type rather than :t; retain the concrete subtype tests that distinguish text from elements.
djot/html.ss:160 render-reference accepts any djot-container, although the actual variants are links and images. Let typed link/image entry points extract the shared reference/destination information with dotted access and pass it to a shared renderer, instead of rediscovering the variant inside an overbroad helper.

Do not introduce a new class hierarchy or a general dispatch framework merely to remove one broad annotation.

5. Restore branch-specific types after dispatch and use dotted access.

In tal-repeat-next!, the heterogeneous sequence storage is legitimate, but the kind discriminator establishes whether the active value is a vector, string or Iterator. Introduce the corresponding using binding with :- inside each established branch. Use a typed Iterator binding and its dotted next! call rather than carrying the value as :t through the implementation.

In html/builder.ss:2262, the internal data is either a string or the accumulated list of pieces. Once the string branch is excluded, bind the other branch as :list instead of continuing to use an untyped value. Keep the representation invariant explicit; do not add redundant runtime checks for facts already established by the producer/dispatch.

6. Avoid repeated checks in callbacks after the element type is established.

  • In html/rules.ss:124–146, callbacks over internally constructed rule entries can annotate entry :- :pair rather than leave the entry untyped.
  • In render-attribute-list, attribute validation happens before the final rendering/filter-map pass. Preserve validation at the boundary: these AST lists are externally constructible and :list alone does not validate their members. In a subsequent pass over the established attribute values, use :- annotations instead of checking the same member type again.

Use the repository's one-argument-per-line signatures, aligned annotation sigils and typed dotted access consistently throughout these changes. Prefer an existing contract or a small shared procedure/macro over repeated validation loops and one-use wrappers.

Keep genuinely necessary broad types and semantic checks.

This is not a request to mechanically replace every :t or delete every predicate. Arbitrary TAL expression values and exceptions, generic duplicate detection, heterogeneous scanner registers and polymorphic I/O adapters can need broad types. In particular, write-djot-html has destination-dependent return behavior (string, bytes or the writer callback's result); do not blindly change it to return void or a fixnum. Preserve token-kind, UTF-8, namespace, nesting and serialization validation. Narrow types only when all call sites or a checked boundary establish the narrower contract.

Validation scope: This follow-up is a source/type-contract audit. The proposed predicate-plus-base-type and list-of? contract forms were checked in the available interpreter. No implementation changes were made, and the test/conformance suites were not rerun for this follow-up. Earlier runtime results apply to the earlier reviewed revision; the newest four-file commit was inspected as a diff.

**Follow-up review: consolidate ByteOutput into stdio and tighten type contracts** **Request changes on `91e655af`. This supersedes my earlier approval.** The separate buffering implementation needs to be consolidated into stdio before merge. The type-contract cleanup below is also part of the requested changes. I checked the update from `3b85ab6d` to `91e655af`: it addresses the earlier tokenizer-exception reporting and formatting findings. Those findings are resolved by source inspection. The output implementation and the production signatures discussed below are unchanged. **1. Consolidate ByteOutput into stdio; do not maintain a second buffering subsystem.** [markup/output.ss:39](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/output.ss#L39) defines a private buffer, cursor and optional Writer sink, then implements allocation, growth, reservation, flushing and extraction. These are stdio responsibilities. The missing public reservation operation is a reason to extend [BufferedWriter interface](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/io/interface/bio.ss#L48) and its [stdio buffer APIs](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/io/bio/api.ss#L60), not a reason to maintain parallel buffering inside markup or defer consolidation to another PR. Required implementation direction: - Add a writable-space reservation operation to BufferedWriter and a way to commit the actual number of bytes produced. Reservation is an upper bound; reserving space must not itself advance the logical output position. - Specify the returned buffer/range and its lifetime. No intervening writer operation may invalidate a reservation before it is committed. Never flush uncommitted bytes. Define bounds checks and failure behavior. - Support the existing memory, sink, binary-port and textual-port backends. The binary-port backend currently delegates directly to Gambit and does not expose an owned byte buffer; account for that explicitly in the implementation. - Replace ByteOutput throughout XML, HTML, TAL and scanner scratch output. Put reusable memory-buffer inspection/reset/truncation operations in stdio as needed. Preserve TAL's save-position/rollback behavior when discarding output from a failed statement. - Keep markup-specific escaping and emission macros in markup. Buffer ownership, allocation, growth, cursor advancement, flushing and extraction belong in stdio. A thin adapter is fine; retaining the independent buffering engine behind a renamed adapter is not consolidation. - Preserve caller ownership of writers/ports, textual-port UTF-8 behavior and the existing externally visible output/error behavior. Validate reservation larger than current capacity, partial commit, growth preserving prior bytes, UTF-8 split boundaries and sink failures, then rerun the relevant markup/stdio regressions, conformance checks and benchmarks. This is a blocking architectural requirement, not a cosmetic rename. **2. Express the public writer domain in contracts instead of accepting `:t` and checking it in the body.** [xml/writer.ss:12](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/xml/writer.ss#L12) and [html/writer.ss:15](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/html/writer.ss#L15) declare `node : :t`, then immediately call `check-xml-node` / `check-html-node`. Centralize the accepted domain in reusable predicate contracts and use those in all three public writer entry points for each format. For XML, the intended shape is: ```scheme (def (xml-writable? (x : :t)) => :boolean (or (XMLDocument? x) (xml-node? x))) ;; Parameter annotation: (node :~ xml-writable? :- XML) ``` Do not simply replace this with `XML?`: the base class also includes names, attributes and other objects that are not standalone writable nodes. For HTML, define the corresponding accepted domain for a document, a node, or a proper list of permitted fragment nodes. Keep semantic serialization checks in the emitter; a signature cannot establish namespace consistency or round-trip validity. **3. Put list element types in constructor contracts; remove the resulting duplicate member checks.** Concrete sites: - [XMLElement initializer](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/xml/node.ss#L198): attributes and children. - [XMLDocument initializer](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/xml/node.ss#L330): prolog and epilog. - [make-html-element](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/html/types.ss#L58): attributes and children. - [HTMLDocument initializer](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/html/node.ss#L96): permitted document child kinds. Use the existing `list-of?` contract machinery where it expresses the domain, for example: ```scheme (attributes :~ (list-of? XMLAttribute?) :- :list) (children :~ (list-of? xml-node?) :- :list) ``` Use the appropriate predicate for each position; document prolog/epilog do not accept arbitrary XML nodes. These contracts establish proper lists and member types. After strengthening the contracts, remove manual checks that merely repeat those facts, and use typed callbacks/dotted access for the validated members. Retain checks for duplicate attributes, namespace agreement, doctype ordering and other relationships between values. The current `:list` annotations do NOT establish member types, so deleting the body checks without strengthening the contracts would be wrong. **4. Narrow internal parameters and return types where call sites establish a smaller domain.** | Location | Requested change | | --- | --- | | [tal/compiler.ss:70](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/tal/compiler.ss#L70) | `code!` receives constructed nonempty Scheme forms at its call sites. Change `form :- :t` to `form :- :pair`; arbitrary user expressions can still occur inside those forms. | | [tal/reader.ss:95](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/tal/reader.ss#L95) | `optional-end-closers-of` returns a list or the sentinel `0`, forcing `=> :t`. Use `#f` for absence and `=> (Maybe :list)`; update callers accordingly. | | [djot/html.ss:687](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/djot/html.ss#L687) | `newline-text?` and `paragraph?` inspect internally rendered XML/HTML nodes. Give their parameter the established `XML` type rather than `:t`; retain the concrete subtype tests that distinguish text from elements. | | [djot/html.ss:160](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/djot/html.ss#L160) | `render-reference` accepts any `djot-container`, although the actual variants are links and images. Let typed link/image entry points extract the shared reference/destination information with dotted access and pass it to a shared renderer, instead of rediscovering the variant inside an overbroad helper. | Do not introduce a new class hierarchy or a general dispatch framework merely to remove one broad annotation. **5. Restore branch-specific types after dispatch and use dotted access.** In [tal-repeat-next!](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/tal/runtime.ss#L341), the heterogeneous sequence storage is legitimate, but the kind discriminator establishes whether the active value is a vector, string or Iterator. Introduce the corresponding `using` binding with `:-` inside each established branch. Use a typed Iterator binding and its dotted `next!` call rather than carrying the value as `:t` through the implementation. In [html/builder.ss:2262](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/html/builder.ss#L2262), the internal data is either a string or the accumulated list of pieces. Once the string branch is excluded, bind the other branch as `:list` instead of continuing to use an untyped value. Keep the representation invariant explicit; do not add redundant runtime checks for facts already established by the producer/dispatch. **6. Avoid repeated checks in callbacks after the element type is established.** - In [html/rules.ss:124–146](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/html/rules.ss#L124), callbacks over internally constructed rule entries can annotate `entry :- :pair` rather than leave the entry untyped. - In [render-attribute-list](https://git.cons.io/mighty-gerbils/gerbil/src/commit/91e655afbe2b0f8d523fe74861afab49f10e4d9d/src/std/text/markup/djot/html.ss#L107), attribute validation happens before the final rendering/filter-map pass. Preserve validation at the boundary: these AST lists are externally constructible and `:list` alone does not validate their members. In a subsequent pass over the established attribute values, use `:-` annotations instead of checking the same member type again. Use the repository's one-argument-per-line signatures, aligned annotation sigils and typed dotted access consistently throughout these changes. Prefer an existing contract or a small shared procedure/macro over repeated validation loops and one-use wrappers. **Keep genuinely necessary broad types and semantic checks.** This is not a request to mechanically replace every `:t` or delete every predicate. Arbitrary TAL expression values and exceptions, generic duplicate detection, heterogeneous scanner registers and polymorphic I/O adapters can need broad types. In particular, `write-djot-html` has destination-dependent return behavior (string, bytes or the writer callback's result); do not blindly change it to return void or a fixnum. Preserve token-kind, UTF-8, namespace, nesting and serialization validation. Narrow types only when all call sites or a checked boundary establish the narrower contract. **Validation scope:** This follow-up is a source/type-contract audit. The proposed predicate-plus-base-type and `list-of?` contract forms were checked in the available interpreter. No implementation changes were made, and the test/conformance suites were not rerun for this follow-up. Earlier runtime results apply to the earlier reviewed revision; the newest four-file commit was inspected as a diff.
Owner

more changes requested, plus the shrink question about the utf8 encoding.

more changes requested, plus the shrink question about the utf8 encoding.
Owner

one way to deal with your buffer sizing is to extend the BufferWriter interface with a reserve method. then you can use memory buffers from stdio and use an definterface-method to call it directly when you need to reserve space. another thing you can do is use the buffer struct directly, it does already everything the ByteOutput does. probably the best course of action if you want to avoid interface overhead.

one way to deal with your buffer sizing is to extend the BufferWriter interface with a reserve method. then you can use memory buffers from stdio and use an definterface-method to call it directly when you need to reserve space. another thing you can do is use the buffer struct directly, it does already everything the ByteOutput does. probably the best course of action if you want to avoid interface overhead.
The writers, TAL, Djot and the scanners use std/io's basic-output-buffer structs directly. std/io gains bio-output-reserve!, which makes room for n bytes in an output buffer. The bio modules import only the interfaces they use, so TAL can load them at expansion time.
write-xml and write-html take their node through xml-writable? and html-writable? contracts, the node constructors check list members with list-of?, and internal procedures take the types their callers establish. &utf8-encode keeps exact sizing; its comment gives the measurement against allocating 4 bytes per character and shrinking.
bio-output-reserve! tests room without fixnum overflow, raises a bad argument for an impossible size and returns the reserved vector. The memory output helpers live once, in output.ss.
Author
Member

@vyzo latest feedback addressed!

From the clanker:

  • ByteOutput is removed. The XML and HTML writers, TAL, Djot and the scanners now write into the std/io buffer structs directly, as you suggested. std/io/bio gains one procedure, bio-output-reserve!, with tests, for making room in a buffer. No reserve method was added to the BufferedWriter interface, since you said the struct approach was preferable.
  • The writers' node arguments are now typed by contracts in their signatures (xml-writable?, html-writable?). The constructors check list members with list-of?, and the duplicated checks are removed. The other types you pointed out are narrowed.
  • UTF-8 shrink: the fill at the end only writes the zero sentinel the scanners rely on. Allocating 4× and shrinking was measured: mostly-ASCII documents got up to 14% slower, and many small fragments 2.2× slower, because the oversized buffers stay alive until the next GC. So exact sizing stays, with the numbers in a comment.

Separately: open-detachable-writer in std/io raises when called, because detachable.ss passes positional arguments to a defclass constructor. That's untouched in this PR.

@vyzo latest feedback addressed! From the clanker: > - ByteOutput is removed. The XML and HTML writers, TAL, Djot and the scanners now write into the std/io buffer structs directly, as you suggested. std/io/bio gains one procedure, `bio-output-reserve!`, with tests, for making room in a buffer. No reserve method was added to the BufferedWriter interface, since you said the struct approach was preferable. > - The writers' node arguments are now typed by contracts in their signatures (`xml-writable?`, `html-writable?`). The constructors check list members with `list-of?`, and the duplicated checks are removed. The other types you pointed out are narrowed. > - UTF-8 shrink: the fill at the end only writes the zero sentinel the scanners rely on. Allocating 4× and shrinking was measured: mostly-ASCII documents got up to 14% slower, and many small fragments 2.2× slower, because the oversized buffers stay alive until the next GC. So exact sizing stays, with the numbers in a comment. > > Separately: `open-detachable-writer` in std/io raises when called, because `detachable.ss` passes positional arguments to a `defclass` constructor. That's untouched in this PR.
Owner

can you fix the open-detachable-writer bug while at it? i'll do another manual pass.

can you fix the open-detachable-writer bug while at it? i'll do another manual pass.
vyzo requested changes 2026-11-03 22:08:33 +00:00
Dismissed
vyzo left a comment

Follow-up review of 6c138fcb: request changes.

The major ByteOutput and signature findings are addressed. Using stdio buffer structs directly matches the maintainer's subsequent guidance in comment 3100; the absence of a new BufferedWriter interface method is therefore not an outstanding finding.

1. P2 — Release temporary buffers when emission fails.

In markup/sink.ss:33, buffer-detach! only runs after emit! succeeds. An emission error or a sink failure during reservation bypasses cleanup. The textual-port branch at lines 55–58 has the same problem if writer.write raises.

I reproduced the emission-error path: the temporary buffer remains open, retains its cached allocation, and does not return it to the cache. Repeated failures discard reusable cache allocations and cause unnecessary allocation and GC pressure; this is not a claim of a file-descriptor leak.

Scope ownership of the temporary buffers explicitly. Flush and detach on success; discard pending bytes and release on failure. Preserve the original exception, including raised #f, without retrying failed writes or closing the caller's sink. Simply placing the current flushing detach operation in finally would introduce unwanted writes during failure cleanup. Keep this ownership handling shared between the affected paths rather than copying cleanup state machines.

Add focused failure-path coverage for an emitter exception and an underlying write failure: the temporary allocation is released, the original exception is preserved, no failed write is retried, and the caller's sink remains open. Caller-owned BufferedWriter buffers are not temporary buffers and must remain caller-owned.

2. P2 — Validate fragment context before consuming the input.

read-html-fragment at html/reader.ss:48 still accepts context : :t and constructs the tokenizer before checking the context.

With an input port and context: 42, I confirmed that the source is consumed completely before ContractViolation is raised. For a streaming source this can also block waiting for EOF before reporting an invalid argument.

Express the string-or-XMLElement domain in a predicate contract. Validate the context's tag name and namespace before reading the source. Keep those semantic checks; move them ahead of I/O. Add a regression check that an invalid context leaves an input port unread, and cover invalid context names/namespaces as well as an invalid argument type.

3. P3 — Carry the established XML base type into internal node dispatch.

Both XML emit-node! and HTML emit-node! retain node :- :t. Their node inputs have the common XML base established by constructors, readers, or the public writer contracts.

Use node :- XML in these dispatch procedures. Retain concrete subtype dispatch and rejection of node kinds unsupported by the output format: the base type does not establish that a node is writable in that format. The outer HTML entry point legitimately retains its node-or-list domain. This is a remaining precision/style cleanup, not a request to replace every polymorphic :t indiscriminately.

The separately requested detachable-writer fix is still outstanding.

open-detachable-writer is unchanged, and calling it raises non keyword slot initializer. This is pre-existing, rather than a regression in these commits, but the maintainer explicitly requested its repair in comment 3104. The matching reader constructor has the same positional-initializer pattern; address that shared mistake consistently and verify wrapping/detaching preserves ownership of the underlying object.

Earlier findings resolved: the independent ByteOutput implementation is removed, constructor member-check loops have been replaced by contracts, the previously identified TAL/internal types have been narrowed, and the link/image rendering refactor shares the common work appropriately. I would close those earlier findings.

Validation: nine focused test modules passed. The pinned HTML conformance runs passed 7,045 tokenizer cases, 1,963 tree-construction cases, and 1,950 writer round-trip cases, with 8, 4, and 17 documented skips respectively and no failures. Eight targeted Djot link/image rendering cases matched the previous reviewed implementation. The cleanup and context-consumption findings above were reproduced separately. git diff --check passed and no source files were changed.

These checks used the reviewed sources through a temporary source overlay on the installed Gerbil runtime, not a full build of this revision. Compiled TAL coverage was skipped because the installed environment lacks its metadata, and the full Djot suite is blocked by the installed runtime's missing char-set:punctuation. The targeted Djot renderer checks do not replace that suite.

**Follow-up review of `6c138fcb`: request changes.** The major ByteOutput and signature findings are addressed. Using stdio buffer structs directly matches the maintainer's subsequent guidance in [comment 3100](https://git.cons.io/mighty-gerbils/gerbil/pulls/1515#issuecomment-3100); the absence of a new BufferedWriter interface method is therefore not an outstanding finding. **1. P2 — Release temporary buffers when emission fails.** In [markup/sink.ss:33](https://git.cons.io/mighty-gerbils/gerbil/src/commit/6c138fcbcbc6fd4e7c43f0463cc605a965c940ef/src/std/text/markup/sink.ss#L33), `buffer-detach!` only runs after `emit!` succeeds. An emission error or a sink failure during reservation bypasses cleanup. The textual-port branch at lines 55–58 has the same problem if `writer.write` raises. I reproduced the emission-error path: the temporary buffer remains open, retains its cached allocation, and does not return it to the cache. Repeated failures discard reusable cache allocations and cause unnecessary allocation and GC pressure; this is not a claim of a file-descriptor leak. Scope ownership of the temporary buffers explicitly. Flush and detach on success; discard pending bytes and release on failure. Preserve the original exception, including raised `#f`, without retrying failed writes or closing the caller's sink. Simply placing the current flushing detach operation in `finally` would introduce unwanted writes during failure cleanup. Keep this ownership handling shared between the affected paths rather than copying cleanup state machines. Add focused failure-path coverage for an emitter exception and an underlying write failure: the temporary allocation is released, the original exception is preserved, no failed write is retried, and the caller's sink remains open. Caller-owned BufferedWriter buffers are not temporary buffers and must remain caller-owned. **2. P2 — Validate fragment context before consuming the input.** [read-html-fragment at html/reader.ss:48](https://git.cons.io/mighty-gerbils/gerbil/src/commit/6c138fcbcbc6fd4e7c43f0463cc605a965c940ef/src/std/text/markup/html/reader.ss#L48) still accepts `context : :t` and constructs the tokenizer before checking the context. With an input port and `context: 42`, I confirmed that the source is consumed completely before `ContractViolation` is raised. For a streaming source this can also block waiting for EOF before reporting an invalid argument. Express the string-or-`XMLElement` domain in a predicate contract. Validate the context's tag name and namespace before reading the source. Keep those semantic checks; move them ahead of I/O. Add a regression check that an invalid context leaves an input port unread, and cover invalid context names/namespaces as well as an invalid argument type. **3. P3 — Carry the established XML base type into internal node dispatch.** Both [XML emit-node!](https://git.cons.io/mighty-gerbils/gerbil/src/commit/6c138fcbcbc6fd4e7c43f0463cc605a965c940ef/src/std/text/markup/xml/emitter.ss#L425) and [HTML emit-node!](https://git.cons.io/mighty-gerbils/gerbil/src/commit/6c138fcbcbc6fd4e7c43f0463cc605a965c940ef/src/std/text/markup/html/emitter.ss#L608) retain `node :- :t`. Their node inputs have the common `XML` base established by constructors, readers, or the public writer contracts. Use `node :- XML` in these dispatch procedures. Retain concrete subtype dispatch and rejection of node kinds unsupported by the output format: the base type does not establish that a node is writable in that format. The outer HTML entry point legitimately retains its node-or-list domain. This is a remaining precision/style cleanup, not a request to replace every polymorphic `:t` indiscriminately. **The separately requested detachable-writer fix is still outstanding.** [open-detachable-writer](https://git.cons.io/mighty-gerbils/gerbil/src/commit/6c138fcbcbc6fd4e7c43f0463cc605a965c940ef/src/std/io/detachable.ss#L25) is unchanged, and calling it raises `non keyword slot initializer`. This is pre-existing, rather than a regression in these commits, but the maintainer explicitly requested its repair in [comment 3104](https://git.cons.io/mighty-gerbils/gerbil/pulls/1515#issuecomment-3104). The matching reader constructor has the same positional-initializer pattern; address that shared mistake consistently and verify wrapping/detaching preserves ownership of the underlying object. **Earlier findings resolved:** the independent ByteOutput implementation is removed, constructor member-check loops have been replaced by contracts, the previously identified TAL/internal types have been narrowed, and the link/image rendering refactor shares the common work appropriately. I would close those earlier findings. **Validation:** nine focused test modules passed. The pinned HTML conformance runs passed 7,045 tokenizer cases, 1,963 tree-construction cases, and 1,950 writer round-trip cases, with 8, 4, and 17 documented skips respectively and no failures. Eight targeted Djot link/image rendering cases matched the previous reviewed implementation. The cleanup and context-consumption findings above were reproduced separately. `git diff --check` passed and no source files were changed. These checks used the reviewed sources through a temporary source overlay on the installed Gerbil runtime, not a full build of this revision. Compiled TAL coverage was skipped because the installed environment lacks its metadata, and the full Djot suite is blocked by the installed runtime's missing `char-set:punctuation`. The targeted Djot renderer checks do not replace that suite.
The temporary buffer a Writer or character port is written through is returned to the buffer cache on every path; on failure the unwritten bytes are discarded, nothing is retried, the exception propagates unchanged and the caller's sink stays open. read-html-fragment checks its context before reading the source. The emitters' node dispatch takes XML nodes.
Both passed positional arguments to a defclass constructor. Adds tests that detaching leaves the underlying reader or writer open and usable.
Author
Member

@vyzo addressed your feedback in the latest commits

@vyzo addressed your feedback in the latest commits
vyzo approved these changes 2026-11-09 18:11:52 +00:00
vyzo left a comment

Follow-up review of af2fdbb0: approved.

No new blocking findings. The outstanding findings from my previous review are addressed:

  • Temporary buffers now share one cleanup helper. Failures discard pending bytes, release cached buffers, preserve raised #f, and avoid retrying failed writes or closing caller-owned sinks.
  • Fragment context validation happens before input consumption, including type, tag-name and namespace checks.
  • XML and HTML node dispatch use node :- XML, retaining the necessary subtype checks and rejection of node kinds unsupported by the output format.
  • Both detachable constructors use keyword initialization and preserve ownership of the underlying reader/writer.

The fixes are appropriately small. The shared cleanup helper and single fragment-building call reduce duplication without adding unnecessary abstractions. I found no additional coding-convention or duplication issue warranting changes in this revision. The earlier ByteOutput consolidation and type-contract findings remain resolved; direct use of stdio buffer structs follows the maintainer's subsequent guidance.

Validation against this revision:

  • Ten focused test modules passed, covering detachable I/O, output-buffer reservation, XML types/reader/writer, HTML API/types/reader/writer, and TAL.
  • Pinned HTML conformance: 7,045 tokenizer cases, 1,963 tree-construction cases, and 1,950 writer round-trip cases passed, with zero failures and the existing 8, 4, and 17 documented skips respectively.
  • Additional textual-port encoding-failure checks exercised both buffered flush and large direct-write paths. The temporary allocation returned to the cache and the caller's port remained writable.
  • git diff --check passed. No source files were changed during review.

These were source-overlay tests on the installed Gerbil runtime, not a full rebuild of this revision. The compiled TAL case was skipped because the environment lacks its metadata. The full Djot suite remains unverified in this environment because the installed runtime lacks char-set:punctuation; it was not rerun in this pass. This approval does not claim full compiled-suite validation.

**Follow-up review of `af2fdbb0`: approved.** No new blocking findings. The outstanding findings from my previous review are addressed: - Temporary buffers now share one cleanup helper. Failures discard pending bytes, release cached buffers, preserve raised `#f`, and avoid retrying failed writes or closing caller-owned sinks. - Fragment context validation happens before input consumption, including type, tag-name and namespace checks. - XML and HTML node dispatch use `node :- XML`, retaining the necessary subtype checks and rejection of node kinds unsupported by the output format. - Both detachable constructors use keyword initialization and preserve ownership of the underlying reader/writer. The fixes are appropriately small. The shared cleanup helper and single fragment-building call reduce duplication without adding unnecessary abstractions. I found no additional coding-convention or duplication issue warranting changes in this revision. The earlier ByteOutput consolidation and type-contract findings remain resolved; direct use of stdio buffer structs follows the maintainer's subsequent guidance. Validation against this revision: - Ten focused test modules passed, covering detachable I/O, output-buffer reservation, XML types/reader/writer, HTML API/types/reader/writer, and TAL. - Pinned HTML conformance: 7,045 tokenizer cases, 1,963 tree-construction cases, and 1,950 writer round-trip cases passed, with zero failures and the existing 8, 4, and 17 documented skips respectively. - Additional textual-port encoding-failure checks exercised both buffered flush and large direct-write paths. The temporary allocation returned to the cache and the caller's port remained writable. - `git diff --check` passed. No source files were changed during review. These were source-overlay tests on the installed Gerbil runtime, not a full rebuild of this revision. The compiled TAL case was skipped because the environment lacks its metadata. The full Djot suite remains unverified in this environment because the installed runtime lacks `char-set:punctuation`; it was not rerun in this pass. This approval does not claim full compiled-suite validation.
Owner

astra is happy; i'll do another manual pass, and unless something iffy catches my eye, i'll merge.

astra is happy; i'll do another manual pass, and unless something iffy catches my eye, i'll merge.
vyzo left a comment

well ok, i think it is ready to merge. a couple of minor comments, please use relative imports for package facades.

well ok, i think it is ready to merge. a couple of minor comments, please use relative imports for package facades.
@ -8,0 +3,4 @@
;;; Typed HTML: the HTML node types and checked constructors, the reader and
;;; the writer. HTML trees are built from the node types of
;;; :std/text/markup/xml as well. See html.md.
(import :std/text/markup/html/types
Owner

use relative imports with ./html/... its the package itself.

use relative imports with ./html/... its the package itself.
@ -15,0 +2,4 @@
;;; © Gerbil contributors
;;; Typed XML: node types and checked constructors, the reader and the
;;; writer. See xml.md.
(import :std/text/markup/xml/types
Owner

relative imports please

relative imports please
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u v0.19-sxml-to-typed-xml:jay-v0.19-sxml-to-typed-xml
git switch jay-v0.19-sxml-to-typed-xml

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch v0.19-staging
git merge --no-ff jay-v0.19-sxml-to-typed-xml
git switch jay-v0.19-sxml-to-typed-xml
git rebase v0.19-staging
git switch v0.19-staging
git merge --ff-only jay-v0.19-sxml-to-typed-xml
git switch jay-v0.19-sxml-to-typed-xml
git rebase v0.19-staging
git switch v0.19-staging
git merge --no-ff jay-v0.19-sxml-to-typed-xml
git switch v0.19-staging
git merge --squash jay-v0.19-sxml-to-typed-xml
git switch v0.19-staging
git merge --ff-only jay-v0.19-sxml-to-typed-xml
git switch v0.19-staging
git merge jay-v0.19-sxml-to-typed-xml
git push origin v0.19-staging
Sign in to join this conversation.
No description provided.