|
|
Donner SVG 0.8.0-pre
SVG editor and embeddable C++20 engine.
|
Status: Implemented. ComponentValueStream is the selector cursor, the CSS tokenizer is a manual pull source, and ordinary CSS names avoid temporary decoding buffers. Coroutine token generation is rejected by measurement. Author: GPT-5 Drafted by: Claude Opus 4.6 Requested by: Jeff McGlynn
Donner parses CSS with two distinct pull abstractions:
The parser retains owned ComponentValue, declaration, and rule vectors because they are public parse results. The tokenizer's ordinary-name path copies the source slice directly into RcString. Only names containing escapes or NUL bytes use the decoding buffer.
Tokenizer owns a cursor into its caller-provided input for the duration of parsing. Every token payload that survives a call to next() is an owning RcString or another owning value. Ordinary names enter RcString directly, allowing short names to use its inline storage. Escaped and NUL-containing names preserve the prior incremental std::vector<char> decoding behavior.
ComponentValueStream wraps std::span<const ComponentValue> and provides peek, advance, isEOF, and whitespace helpers. It does not own or copy the component values. The caller owns the underlying vector for the stream's entire lifetime.
Six alternating parent/optimized runs on the representative benchmark corpus produce these mean CPU times:
| Workload | Parent | Optimized | Delta |
|---|---|---|---|
| Raw medium stylesheet tokenization | 10,085 ns | 4,704 ns | -53.4% |
| Medium style attribute | 2,898 ns | 1,960 ns | -32.4% |
| Medium stylesheet | 29,844 ns | 23,879 ns | -20.0% |
| Complex selector | 2,097 ns | 1,547 ns | -26.2% |
The allocation benchmark counts C++ operator new calls and requested bytes. It does not measure process RSS or non-C++ allocators.
| Workload | Parent calls / bytes | Optimized calls / bytes | Call delta |
|---|---|---|---|
| Raw medium stylesheet tokenization | 404 / 5,282 | 24 / 315 | -94.1% |
| Medium style attribute | 78 / 8,029 | 19 / 7,248 | -75.6% |
| Medium stylesheet | 705 / 111,554 | 325 / 106,587 | -53.9% |
Retained fallback cases cover a long ordinary prefix followed by an escape or embedded NUL. Both take about 165 ns and perform 8 allocation calls / 175 requested bytes on the measured system.
The rejected co_yield prototype adds one 248-byte allocation per tokenizer, grows the tokenizer object from 144 to 152 bytes, slows raw medium tokenization by 28%, and slows the measured public parser entry points by 11% to 17%.
Run the retained benchmarks with:
The tokenizer remains linear in input length. Ordinary names perform one scan and one owning RcString construction. Decoded names perform the fast scan followed by the existing incremental decode path; no recursion or new amplification mechanism is introduced. Parser component-count and recursion budgets remain unchanged.
No returned CSS object depends on the caller's input lifetime. Token offsets remain byte offsets into the parse input for diagnostics, while retained token payloads own their bytes.
The original detailed experiment plan and discussion remain available in git history at dcd36aff2:docs/design_docs/0019-css_token_stream.md.