Releases: lutaml/taurus
Release list
taurus v0.12.0
New API — per-node binding_wrapper (#262)
Added void* binding_wrapper to TaurusNode base struct. Language
bindings (Ruby FFI, Python, etc.) can cache their native wrapper
object on first node access, eliminating per-node FFI call overhead
on subsequent traversals.
New functions:
taurus_node_get_binding_wrapper(node)→void*taurus_node_set_binding_wrapper(node, void* wrapper)
ABI change: TaurusNode grows from 12→20 bytes. Element struct
grows from 80→88 bytes. Minor version bump.
Measured impact (from #262 benchmark data):
| Query | Before (Ruby) | With cache | Nokogiri |
|---|---|---|---|
//book (100 nodes) |
88 µs | ~13 µs | 13 µs |
| Union (200 nodes) | 188 µs | ~27 µs | 27 µs |
The binding eliminates 100+ FFI calls per nodeset traversal. On
first traversal, the binding wraps each node and caches the wrapper.
On subsequent traversals, the cached wrapper is found with zero FFI
calls.
The field is opaque to libtaurus — never dereferenced or freed.
Initialized to NULL on node creation.
Combined with the batch accessor (taurus_xpath_result_get_nodes,
shipped in v0.11.4), this addresses the complete #262 proposal.
taurus v0.11.5
Quality — warning-free build
Eliminated all 9 compiler warnings across the codebase:
- Nested
/*in block comments (element_index.h, vm.c) - Unused functions (node_public.c
append_path_segment,
taurus.ctaurus_input_has_internal_dtd_subset) - Const qualifier discard (serialize.c attr caching)
- Scalar initializer style (c14n.c, dtd/validator.c)
The build is now completely -Wall -Wextra clean.
Documentation — remaining work TODOs
- TODO 151: in-place parsing (eliminate buffer copy)
- TODO 152: per-node
user_datafor FFI wrapper caching (#262) - TODO 153: high-document-count stress test for CI
Issues closed
taurus v0.11.4
Fixes
-
#253:
taurus_doctype_get_internal_subsetnow returns the raw
DTD internal subset text. Previouslydirect_parseextracted the
subset for entity parsing but didn't store it on the DOCTYPE node. -
#217:
taurus_element_append_childcorrectly unlinks a child
before re-appending, even when the parent is the same element
(re-ordering). The oldold_parent != elemcheck skipped
unlinking for same-parent moves, causing duplicate children and
inflatedchild_count.
New API
- #262:
taurus_xpath_result_get_nodes(result, out, max)—
batch-copy all nodes from a nodeset result in one call. Eliminates
per-node FFI overhead for bindings iterating large nodesets
(100+ nodes).
taurus v0.11.3
Fix — benchmark-ips segfault with 15,000+ alive documents (#261)
direct_parse used a shared thread-local overflow hash table for
compact pointer encoding of next_sibling and attribute edges.
Under benchmark-ips (which keeps every return value alive), the
table accumulated entries from 15,000+ simultaneously-alive
documents. Combined with malloc address reuse, this caused
cross-document pointer corruption and a segfault in
taurus_node_freeze.
Three-part fix (all in direct_parse.c):
-
Overflow-table-free wiring: all compact pointer edges
(parent, child, sibling, attribute) now use direct offset
arithmetic.direct_parsenever touches the thread-local
overflow state — it's fully self-contained. -
Contiguous elem+attr allocation:
elem_blockand
attr_blockare now ONE combinedpool_alloccall. Offsets
between elements and attributes are bounded by the allocation
size (<4MB), always fitting in int32. -
Right-sized pool pages:
page_sizeis set to
elem_bytes + attr_bytes + text_headroom(capped at 4MB).
This keeps the bulk allocation and text/comment/CDATA nodes
on the same pool page, within int32 offset range.
Verified: 15,000 simultaneously-alive 38KB documents parsed,
child_count-verified, and freed — zero crashes, zero corruption
(both plain and ASAN).
taurus v0.11.2
Fix — DOCTYPE PUBLIC/SYSTEM identifiers (#253)
direct_parse's DOCTYPE handler extracted the name and internal
subset but silently dropped PUBLIC/SYSTEM external identifiers.
After the name scan, the parser skipped straight to [ or >,
bypassing the external ID declarations.
Fix: re-scan the region between name and [ / > for PUBLIC or
SYSTEM keywords followed by quoted identifiers. Set public_id /
system_id on the DOCTYPE node. Verified with all DOCTYPE variants:
bare name, SYSTEM, PUBLIC, PUBLIC+subset, name+subset.
Fix — iterative tree freeze (#256, deeper investigation)
The v0.11.1 fix (clearing g_current_document) addressed the
thread-local stale pointer but the crash persisted for some inputs.
taurus_node_freeze was recursive — under tight parse loops
on deeply nested documents, the unbounded recursion could exhaust
the thread stack.
Fix: converted to an iterative depth-first walk with a fixed
256-deep explicit stack, eliminating the stack-overflow crash
vector entirely.
taurus v0.11.1
Fix — segfault under tight parse loops (#256)
taurus_parse_string could segfault under tight parse/free cycles
(Ruby benchmark-ips with delayed GC). Root cause: the thread-local
g_current_document retained a dangling pointer to the returned
document after the caller freed it, corrupting overflow-table
cleanup for subsequent parses.
Fix: clear g_current_document (call
taurus_compact_set_current_document(NULL)) on the direct_parse
success path, not just on failure. The thread-local is now NULL
between parse cycles, preventing stale-pointer contamination of
the compact-pointer overflow table.
483/483 tests pass on macOS + Linux; ASAN clean.
taurus v0.11.0
ONE parser architecture — flat subsystem DELETED (4479 lines removed)
direct_parse is now the sole XML parser. The entire flat/
subsystem (flat_parser, flat_promote, flat_doc, flat_fast,
flat_serialize, flat_xpath) is deleted. One parser, like pugixml.
Changes
-
UTF-8 name support: Added
CT_UTF8flag to the shared
chartype table for bytes >= 0x80.IS_NAME_CHARand
IS_NAME_STARTnow includeCT_UTF8, so UTF-8 multibyte names
(<café>) scan without truncation. -
Close tag prefix:local fix: Close tag verification now
strips the prefix from</ns:elem>before comparing with the
open element's local name. This was a latent bug that surfaced
when the flat_parser fallback was removed. -
Deleted flat subsystem (~4479 lines):
flat_doc.c/h,flat_parser.c/h,flat_promote.c/h,
flat_fast.c/h,flat_serialize.c/h,flat_xpath.c/htest/flat/directory +test_flat_promote_line.cppbenchmarks/flat/bench_flat_parse.cflat_doc/flat_promotedfields fromstruct taurus_document- Flat fast-path checks in
xpath_public.candserialize.c
-
taurus_parsecallsdirect_parsedirectly — no fallback chain. -
taurus_document_ensure_promotedis now a no-op chokepoint.
Architecture
src/taurus/parse/— empty (legacy parser deleted v0.10.0)src/taurus/flat/— onlydirect_parse.canddirect_parse.h
One parser, one codebase, ~7500 lines of parser code removed across
v0.10.0 + v0.11.0.
taurus v0.10.0
Breaking — legacy parser DELETED (3092 lines removed)
The legacy parser (parser_new.c, 1956 lines) is gone. direct_parse
(with DTD entity support from v0.9.0) now covers the full XML feature
set. The three-parser architecture collapses to two.
Deleted
src/taurus/parse/parser_new.c— 1956 linessrc/taurus/parse/parser_new.h— 175 linessrc/taurus/parse/compact_parser.c— 654 lines (was dead code)- Legacy parser fallback in
taurus_parse— 319 lines - Legacy parser path in
taurus_parse_inplace— delegates totaurus_parse
The src/taurus/parse/ directory is now empty.
Changes
taurus_parse: whendirect_parseandflat_parseboth fail,
returns NULL. No legacy fallback.taurus_parse_inplace: delegates totaurus_parse(direct_parse
copies the caller's buffer for in-place NUL termination).direct_parseandflat_parser: now respectg_taurus_max_depth
(custom depth limit) via__thread extern. Falls back to
DP_MAX_DEPTH(256) /FLAT_MAX_DEPTHwhen the limit is 0.
Architecture after this release
Two parsers instead of three:
flat/direct_parse.c— single-pass, zero-copy, bulk-alloc,
DTD-aware (primary).flat/flat_parser.c— FlatDoc intermediate + lazy promote
(fallback for edge casesdirect_parserejects).
This is an internal ABI change (no public API surface change).
576/576 tests pass; ASAN clean.
taurus v0.9.0
direct_parse handles DTD entities — path to deleting legacy parser
The legacy parser (parser_new.c, 1955 lines) existed primarily to
handle DTD internal subsets with custom entity declarations. This
release makes direct_parse DTD-aware, enabling deletion of the
legacy parser in a future release.
Changes
-
DOCTYPE extraction: when
direct_parseencounters
<!DOCTYPE name [subset]>, it extracts the internal subset and
parses it viataurus_dtd_parse_internal_subset(reusing the
existing DTD parser). A DOCTYPE node is created so
taurus_document_internal_subsetexposes the name. -
Entity expansion: when a DTD is present and text/attr content
contains&, entities are eagerly expanded via
taurus_decode_entities_view_with_dtd. Predefined entities
(&etc.) still use the lazy expansion path when no DTD. -
Parse-path gate: the DTD internal-subset gate in
taurus_parse
is removed.direct_parsenow handles DTD inputs directly —
no more forced legacy-parser fallback for<!DOCTYPE>inputs. -
Serializer:
serialize_text_internalnow routes through
taurus_text_get_contentso borrowed text nodes with entities
are materialized + expanded before output.
Verified
<!DOCTYPE root [<!ENTITY foo "Hello">]><root>&foo;</root> parses
via direct_parse with text content "Hello" (was "&foo;"
before this change).
Next steps (future releases)
Once confidence builds that direct_parse handles all real-world
DTD inputs:
- Remove
flat_parsefallback fromtaurus_parse. - Delete the legacy parser (
parser_new.c, ~1955 lines). - Delete
flat_parser.c+flat_promote.c(~1245 lines). - Total: ~3200 lines of parser code removed.
taurus v0.8.0
Performance — parse algorithm over struct size
This release closes the algorithmic parse gap with pugixml via two
targeted hot-path improvements. Element struct size (80 bytes vs
pugixml's 44) remains unchanged — measured analysis shows struct size
is a secondary cache effect, not the dominant cost.
Route predefined entities through the fast parser
The parse-path gate previously fell back to the slow legacy parser
for ANY input containing &, even when only predefined XML entities
(&, <, >, ", ') or numeric character
references (A, B) were present. Since most real-world XML
uses & for escaping, this gate forced the slow path on the
majority of inputs.
Fix: removed the entity gate. The fast path (direct_parse +
flat_promote) now handles predefined entities via lazy expansion:
direct_parseandflat_promotedetect&in attr values, set
has_entities=1, leaveattr->value=NULLso the accessor expands
viataurus_decode_entities_viewon first read.taurus_text_get_contentchecks for&in borrowed content and
expands before materializing.- The serializer expands entity-containing attrs before re-escaping.
taurus_element_get_text_content(XPathstring()) routes through
taurus_text_get_content.
The DOCTYPE internal-subset gate is retained — custom DTD entities
still require the legacy parser.
memchr for attr/comment/CDATA/PI scans in direct_parse
Replaced sequential per-byte scans with libc memchr (SIMD-vectorized,
16-32 bytes/iteration):
- Attribute value closing quote
- Comment body terminator
-->(memchr for-, verify candidate) - CDATA body terminator
]]>(memchr for], verify candidate) - PI data terminator
?
Big win for long attribute values (URLs), large comments/CDATA
sections. On a 200KB attr-heavy input: 0.6ms/parse (~333 MB/s),
competitive with pugixml.
Text scanning already used memchr (for <). Name scanning stays
LUT-based — SIMD name scan was tried in TODO 144 and found slower
for typical 5-20 char names (vector setup cost not amortized).
Fixes
- Remove duplicate unreachable
returninfp_is_name_char
(flat_parser.c). - Remove dead
taurus_input_has_entities/taurus_input_has_namespaces
functions after entity-gate removal. - Fix two nested-comment warnings in
taurus.c.