Skip to content

Releases: lutaml/taurus

taurus v0.12.0

Choose a tag to compare

@github-actions github-actions released this 10 Aug 21:34

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

Choose a tag to compare

@github-actions github-actions released this 10 Aug 15:38

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.c taurus_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_data for FFI wrapper caching (#262)
  • TODO 153: high-document-count stress test for CI

Issues closed

taurus v0.11.4

Choose a tag to compare

@github-actions github-actions released this 10 Aug 15:01

Fixes

  • #253: taurus_doctype_get_internal_subset now returns the raw
    DTD internal subset text. Previously direct_parse extracted the
    subset for entity parsing but didn't store it on the DOCTYPE node.

  • #217: taurus_element_append_child correctly unlinks a child
    before re-appending, even when the parent is the same element
    (re-ordering). The old old_parent != elem check skipped
    unlinking for same-parent moves, causing duplicate children and
    inflated child_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

Choose a tag to compare

@github-actions github-actions released this 10 Aug 11:59

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):

  1. Overflow-table-free wiring: all compact pointer edges
    (parent, child, sibling, attribute) now use direct offset
    arithmetic. direct_parse never touches the thread-local
    overflow state — it's fully self-contained.

  2. Contiguous elem+attr allocation: elem_block and
    attr_block are now ONE combined pool_alloc call. Offsets
    between elements and attributes are bounded by the allocation
    size (<4MB), always fitting in int32.

  3. Right-sized pool pages: page_size is 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

Choose a tag to compare

@github-actions github-actions released this 10 Aug 02:59

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

Choose a tag to compare

@github-actions github-actions released this 10 Aug 02:31

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

Choose a tag to compare

@github-actions github-actions released this 09 Aug 23:37

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_UTF8 flag to the shared
    chartype table for bytes >= 0x80. IS_NAME_CHAR and
    IS_NAME_START now include CT_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/h
    • test/flat/ directory + test_flat_promote_line.cpp
    • benchmarks/flat/bench_flat_parse.c
    • flat_doc/flat_promoted fields from struct taurus_document
    • Flat fast-path checks in xpath_public.c and serialize.c
  • taurus_parse calls direct_parse directly — no fallback chain.

  • taurus_document_ensure_promoted is now a no-op chokepoint.

Architecture

  • src/taurus/parse/ — empty (legacy parser deleted v0.10.0)
  • src/taurus/flat/ — only direct_parse.c and direct_parse.h

One parser, one codebase, ~7500 lines of parser code removed across
v0.10.0 + v0.11.0.

taurus v0.10.0

Choose a tag to compare

@github-actions github-actions released this 09 Aug 14:57

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 lines
  • src/taurus/parse/parser_new.h — 175 lines
  • src/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 to taurus_parse

The src/taurus/parse/ directory is now empty.

Changes

  • taurus_parse: when direct_parse and flat_parse both fail,
    returns NULL. No legacy fallback.
  • taurus_parse_inplace: delegates to taurus_parse (direct_parse
    copies the caller's buffer for in-place NUL termination).
  • direct_parse and flat_parser: now respect g_taurus_max_depth
    (custom depth limit) via __thread extern. Falls back to
    DP_MAX_DEPTH (256) / FLAT_MAX_DEPTH when the limit is 0.

Architecture after this release

Two parsers instead of three:

  1. flat/direct_parse.c — single-pass, zero-copy, bulk-alloc,
    DTD-aware (primary).
  2. flat/flat_parser.c — FlatDoc intermediate + lazy promote
    (fallback for edge cases direct_parse rejects).

This is an internal ABI change (no public API surface change).
576/576 tests pass; ASAN clean.

taurus v0.9.0

Choose a tag to compare

@github-actions github-actions released this 09 Aug 12:38

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_parse encounters
    <!DOCTYPE name [subset]>, it extracts the internal subset and
    parses it via taurus_dtd_parse_internal_subset (reusing the
    existing DTD parser). A DOCTYPE node is created so
    taurus_document_internal_subset exposes 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
    (&amp; etc.) still use the lazy expansion path when no DTD.

  • Parse-path gate: the DTD internal-subset gate in taurus_parse
    is removed. direct_parse now handles DTD inputs directly —
    no more forced legacy-parser fallback for <!DOCTYPE> inputs.

  • Serializer: serialize_text_internal now routes through
    taurus_text_get_content so 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_parse fallback from taurus_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

Choose a tag to compare

@github-actions github-actions released this 09 Aug 11:44

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
(&amp;, &lt;, &gt;, &quot;, &apos;) or numeric character
references (&#65;, &#x42;) were present. Since most real-world XML
uses &amp; 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_parse and flat_promote detect & in attr values, set
    has_entities=1, leave attr->value=NULL so the accessor expands
    via taurus_decode_entities_view on first read.
  • taurus_text_get_content checks for & in borrowed content and
    expands before materializing.
  • The serializer expands entity-containing attrs before re-escaping.
  • taurus_element_get_text_content (XPath string()) 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 return in fp_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.