Bulk-scan single-line string bodies - #491
Merged
Merged
Conversation
tfoutrein
force-pushed
the
perf/string-scan
branch
2 times, most recently
from
June 5, 2026 17:39
421172e to
e08a656
Compare
This was referenced Jun 6, 2026
Contributor
|
@tfoutrein please resolve the conflicts |
Parsing a single-line string appended its body one character at a time (`value += current; inc()`). For long string values this dominates. Scan the run of ordinary characters up to the next delimiter, backslash or control character in a single pass (`Source.advance_until`) and append the whole slice at once; the stop character is then handled by the existing branch on the next iteration. Multiline strings keep the per-character loop (CRLF handling). The stop-set is exactly the control characters the per-character loop rejects, so InvalidControlChar / escape / delimiter handling is unchanged. No behaviour change (972 tests incl. the toml-test conformance submodule; plus a 4135-input adversarial differential — output and error-type byte-identical to the per-char loop). Up to ~5x faster parsing on string-heavy single-line documents.
tfoutrein
force-pushed
the
perf/string-scan
branch
from
June 10, 2026 06:25
e08a656 to
1ec5f04
Compare
Contributor
Author
|
Rebased onto |
frostming
approved these changes
Jun 10, 2026
tfoutrein
added a commit
to AstekGroup/tomlkit
that referenced
this pull request
Jun 15, 2026
python-poetry#491 bulk-scanned single-line string bodies but left multiline ("""/''') bodies on the per-character loop because of \r\n handling. Extend the same fast path to multiline: append the run of ordinary characters in one slice up to the next delimiter, backslash, carriage return or control character. Raw line feeds and tabs are valid inside a multiline body, so they are NOT stop characters -- a whole multi-line body is consumed in a single pass. A carriage return still stops the scan, so the existing branch keeps validating the \r\n pair (and rejecting a lone \r) and \r\n is preserved byte-for-byte. Invalid control characters and DEL also stop the scan, so they are still rejected at the same position. No behaviour change: full suite incl. toml-test conformance passes; verified by a differential over ~122k generated multiline strings (LF / CRLF / bare CR, tabs, lone quotes, escapes, control bytes, unicode/astral, EOF truncation) -- byte-identical round-trip and identical exception class/line/col. ~23x faster parsing an LF multiline-heavy document (~11x with CRLF).
frostming
added a commit
that referenced
this pull request
Jun 18, 2026
#491 bulk-scanned single-line string bodies but left multiline ("""/''') bodies on the per-character loop because of \r\n handling. Extend the same fast path to multiline: append the run of ordinary characters in one slice up to the next delimiter, backslash, carriage return or control character. Raw line feeds and tabs are valid inside a multiline body, so they are NOT stop characters -- a whole multi-line body is consumed in a single pass. A carriage return still stops the scan, so the existing branch keeps validating the \r\n pair (and rejecting a lone \r) and \r\n is preserved byte-for-byte. Invalid control characters and DEL also stop the scan, so they are still rejected at the same position. No behaviour change: full suite incl. toml-test conformance passes; verified by a differential over ~122k generated multiline strings (LF / CRLF / bare CR, tabs, lone quotes, escapes, control bytes, unicode/astral, EOF truncation) -- byte-identical round-trip and identical exception class/line/col. ~23x faster parsing an LF multiline-heavy document (~11x with CRLF). Co-authored-by: Frost Ming <me@frostming.com>
netbsd-srcmastr
pushed a commit
to NetBSD/pkgsrc
that referenced
this pull request
Jul 26, 2026
## [0.15.1] - 2026-07-17 ### Changed - Speed up membership tests (`key in ...`) on `Container`, `Table` and `InlineTable` with native `__contains__` implementations, avoiding the inherited `MutableMapping` round-trip through `__getitem__` (which resolves the value and builds an exception on every absent key). ([#483](python-poetry/tomlkit#483)) - Speed up parsing by making `Source` index-based: it now tracks an integer position over the input string instead of materializing a list of `(index, char)` tuples up front, so construction is O(1) and state save/restore no longer copies an iterator. ([#489](python-poetry/tomlkit#489)) - Speed up parsing by scanning character runs in bulk: `Source.advance_while`/`advance_until` consume a whole run of whitespace, bare-key or number characters in a single pass over the input string instead of one `inc()` call per character. ([#490](python-poetry/tomlkit#490)) - Speed up parsing of single-line strings by bulk-appending the run of ordinary characters up to the next delimiter, backslash or control character in one pass, instead of one character at a time. ([#491](python-poetry/tomlkit#491)) - Speed up parsing by removing the internal `TOMLChar` wrapper: the parser now reads plain `str` characters from `Source` and detects end-of-input positionally, avoiding a per-character object construction and method dispatch. ([#492](python-poetry/tomlkit#492)) - Speed up parsing by comparing `StringType` members by identity (`is`) instead of building a set on every `is_basic`/`is_literal`/`is_singleline`/`is_multiline` call, avoiding millions of enum hashes while parsing. ([#502](python-poetry/tomlkit#502)) - Speed up merging super tables by merging in place instead of deep-copying the growing target on every merge, turning the parse of documents with many subtables under a shared super table (e.g. consecutive `[a.b.c]` / `[a.b.d]` headers) from O(n²) into O(n). ([#503](python-poetry/tomlkit#503)) - Speed up membership tests (`key in ...`) on out-of-order tables with a native `OutOfOrderTableProxy.__contains__`, completing [#483](python-poetry/tomlkit#483) for the last mapping type that still inherited the slow `MutableMapping` mixin (which resolves the value and builds an exception on every absent key). ([#515](python-poetry/tomlkit#515)) - Speed up parsing documents with many dotted keys or table headers sharing a prefix by validating out-of-order tables incrementally: each new fragment is merged into a cached validation container once, instead of re-merging (and deep-copying) every earlier fragment on each append, turning a super-cubic worst case into linear time (80 shared-prefix dotted keys: ~8 s → ~10 ms). ([#479](python-poetry/tomlkit#479)) - Speed up parsing of arrays that close right after a value (e.g. the `files = [...]` blocks that dominate lock files): the parser no longer attempts to read a value while sitting on the closing `]`, which previously built an `UnexpectedCharError` just to discard it — and constructing that exception eagerly computes a line/column by scanning the whole document, making it O(document size) per such array. ([#517](python-poetry/tomlkit#517)) - Speed up parsing of multiline strings by bulk-appending the run of ordinary characters — across raw line feeds and tabs — up to the next delimiter, backslash, carriage return or control character, instead of one character at a time. This extends to `"""`/`'''` bodies the single-line fast path added in [#491](python-poetry/tomlkit#491); a `\r` still stops the scan so `\r\n` stays validated and byte-for-byte preserved. ([#518](python-poetry/tomlkit#518)) - Speed up `unwrap()` (converting a parsed document to a plain `dict`) by resolving each key directly from the container's key map instead of iterating the inherited `MutableMapping` view, which rebuilt a `SingleKey` from the bare string for every key just to re-look-up the value. Out-of-order tables still resolve through their proxy, so their validation is unchanged. ([#521](python-poetry/tomlkit#521)) - Speed up rendering (`as_string()` / `dumps()`) of inline tables with many keys by precomputing the last-key and last-deleted-element indices in a single pass, instead of rescanning the remaining body on every separator comma — turning an O(n²) render into O(n). ([#525](python-poetry/tomlkit#525)) - Raise on malformed array element instead of dropping it, ([#527](python-poetry/tomlkit#527)) ### Fixed - Fix `string()` dropping a leading newline of a multiline string on round-trip: a value beginning with a newline is now rendered with an extra leading newline (the one the parser trims after the opening delimiter) so it survives re-parsing. - Fix invalid serialization with a duplicated comma when removing a non-edge element from a parsed inline table. ([#486](python-poetry/tomlkit#486)) - Fix invalid serialization with a duplicated comma when appending or inserting into a comma-first formatted array. ([#499](python-poetry/tomlkit#499)) - Fix `ParseError` when a sub-table extends the last element of an array of tables after an unrelated table. ([#261](python-poetry/tomlkit#261)) - Fix unparseable serialization when adding a key to a dotted-key table inside an inline table. ([#500](python-poetry/tomlkit#500)) - Fix a table replaced by a plain value being serialized inside the preceding table's body when other tables follow; the value now moves before the first table like other root-level values. ([#504](python-poetry/tomlkit#504)) - Fix assigning a table over a dotted key (e.g. `doc["a"] = {...}` where `a` came from `a.b = ...`): the dotted prefix was duplicated onto the new `[a]` header, and the header then swallowed any sibling that follows it on round-trip. The replacement now renders as a plain table and, when needed, moves before the inline entries (values and dotted keys) it would otherwise capture. ([#513](python-poetry/tomlkit#513), [#524](python-poetry/tomlkit#524)) - Restore `dumps()` rendering mapping-like wrappers around a parsed document (e.g. `dotty_dict`'s `Dotty`) through their delegated `as_string`, preserving the original table order and layout instead of re-encoding through a plain dict — a 0.15.0 regression. ([#482](python-poetry/tomlkit#482)) - Fix uncontrolled recursion when parsing deeply nested documents: crafted input could crash the process with a `RecursionError`. Values nested more than 100 levels deep and keys with more than 100 dotted fragments now raise `ParseError`. ([#459](python-poetry/tomlkit#459)) - Fix `comment()` producing invalid TOML for a multiline string by prefixing every line with `#`, not just the first. ([#449](python-poetry/tomlkit#449)) - Fix the separator comma being swallowed by a trailing comment when appending a key to a multiline inline table, leaving the new key without a separator so the result no longer round-trips. ([#512](python-poetry/tomlkit#512)) - Fix a `KeyAlreadyPresent` error when parsing or accessing an out-of-order table whose array-of-tables elements are split across the table's parts. ([#505](python-poetry/tomlkit#505)) - Out-of-order value-vs-table and dotted-key-vs-table redefinitions are now rejected at parse time instead of being silently accepted or raising only on access. The parser also detects when a non-dotted key is a prefix of an existing dotted key, matching the stdlib `tomllib` behaviour. ([#523](python-poetry/tomlkit#523)) - Reject tables inserted into inline tables instead of serializing invalid TOML. ([#531](python-poetry/tomlkit#531)) - Fix assigning an array of tables over a dotted key (e.g. `doc["a"] = aot(...)` where `a` came from `a.b = ...`): the new `[[a]]` header kept the dotted key's inline position and swallowed the following dotted sibling on round-trip. The array of tables now renders past the inline entries it would otherwise capture, mirroring the table fix for [#513](python-poetry/tomlkit#513). ([#542](python-poetry/tomlkit#542)) - Fix a new top-level scalar being captured by a table rendered from a dotted key: appending a scalar after a dotted-key entry (e.g. `a.b = 1`) whose table had gained a `[a.c]`-style child placed the scalar inside that table's scope, silently re-nesting it on round-trip. Scalars now move before such an entry, like they do before regular tables. ([#543](python-poetry/tomlkit#543)) - Fix invalid serialization with a duplicated `[table]` header when adding a key to an out-of-order table whose concrete header is declared after its sub-tables; the new key now lands in the existing concrete part instead of giving the header-less super part a second header. ([#545](python-poetry/tomlkit#545)) - Fix a table's display name (its exact header spelling, including whitespace and quoting) being normalised when the table is assigned onto itself, e.g. `doc[k] = doc[k]` rewriting `[keys .'a'.'c']` to `[keys.a.'c']`. ([#291](python-poetry/tomlkit#291)) - Fix missing newlines when appending a key after a dotted inline table, including when the original document has no trailing newline. ([#533](python-poetry/tomlkit#533)) - Preserve trailing whitespace when replacing a super table, including assigning it onto itself. ([#534](python-poetry/tomlkit#534)) - Fix `str()` and `repr()` of out-of-order table proxies to show their merged values. ([#536](python-poetry/tomlkit#536)) - Reject decimal integer literals that exceed Python's integer-string conversion limit instead of coercing them to infinity. ([#538](python-poetry/tomlkit#538))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Parsing a single-line string appended its body one character at a time (
value += current; inc()). For long string values (the bulk of most config / lockfile content) this dominates.This scans the run of ordinary characters up to the next delimiter, backslash or control character in a single pass (
Source.advance_until) and appends the whole slice at once; the stop character is then handled by the existing branch on the next iteration. Multiline strings keep the per-character loop (CRLF handling).The stop-set is exactly the control characters the per-character loop rejects, so
InvalidControlChar/ escape / delimiter handling is unchanged, and a mid-string EOF raisesUnexpectedEofErrorjust as the per-charinc(exception=...)did.Benchmarks
Parsing speedup across document shapes (median, interleaved A/B vs
master, includes #489+#490):No regression on any shape (multiline-heavy and nested docs are unchanged).
Tests
Full suite passes (972 tests, incl. the toml-test conformance submodule). On top of that, a 4135-input adversarial differential (random escapes valid+invalid, every control byte 0x00–0x1F+DEL in basic & literal, unicode/astral/combining, the other-quote char inside strings, truncated/malformed inputs for error parity) is byte-identical in output and exception type to the per-character loop. No public API or behaviour change.