LEPTRIS

XPath 1.0 — complete, and quick

All 13 axes, 27 functions, 15 operators, full predicate syntax. 438/438 on the W3C XPath 1.0 conformance suite — served by a bytecode VM that beats libxml2 on every measured query.

The engine

Expressions compile to a bytecode VM: compile-once, evaluate-many dispatch with per-axis specialization, predicate fast paths, absolute-path fusion, a per-document element index with attribute buckets, fused axis+predicate opcodes, and memcpy fast paths for index-backed queries. Repeated //x queries are served from a subtree-interval index at effectively no cost.

Axes — all thirteen

axisshorthandnotes
ancestorexcludes self
ancestor-or-self
attribute@attrworks on all nodes
childdefaultthe default axis
descendantexcludes self
descendant-or-self//fused with name tests (//foo)
followingexcludes descendants
following-sibling
namespacenamespace nodes
parent..
precedingexcludes ancestors
preceding-sibling
self.

All axes maintain document order as the spec requires. Node tests: name tests, *, ns:*,text(), comment(),processing-instruction() (with and without target),node().

Predicates and abbreviated syntax

  • Position predicates — [N] (1-based), [last()], [position()=N]
  • Boolean predicates — [@attr], [child]
  • Value predicates — [@id='1'], with fast paths
  • Multiple sequential predicates — [1][@id]
  • Complex predicates — [position() mod 2 = 0]

Mixed nodesets

Results can mix element nodes and synthetic attribute nodes —//a/@x is consumable through the public result API (leptris_xpath_result_node_kind / get_node / node_name / node_value) without touching internals. Bindings expose this through the result-scoped kind quartet, never through raw node tags.

Namespace-bound evaluation

Since v1.2.0, expressions can carry external namespace bindings: leptris_xpath_eval_ns with aLeptrisXPathNsSet resolves prefixed name tests bynamespace — an element matches when it carries the bound namespace via any prefix or the default namespace, and//t:* selects only elements in t’s namespace (previously prefix wildcards were namespace-blind). XPointerxmlns(prefix=uri) components bind subsequentxpointer() bodies too.

Measured — vs libxml2

From benchmarks/xpath/bench_diagnostic on a ~5 KB catalog fixture, CPU time:

queryleptrislibxml2advantage
self::* (per-call floor)0.57 µs0.89 µs1.6× faster
child::*0.71 µs0.94 µs1.3× faster
attribute::id0.63 µs2.52 µs4.0× faster
descendant::*0.72 µs0.96 µs1.3× faster
//book0.55 µs~1 µs1.8× faster
//*0.56 µs~1 µs1.8× faster
count(//book[@id='b1'])1.13 µs~3 µs2.7× faster
descendant::*[@id]0.77 µs1.02 µs1.3× faster
/catalog0.53 µs~1 µs1.9× faster

leptris leads on all ten XPath benchmarks. Release 1.1.2 made union deduplication ~140× faster on large merged sets (//name | //item on 20k elements: 229 ms → 1.6 ms), and 1.1.1 made //text() ~7× faster via single-pass pre-order walks that emit results directly in document order. The element index currently accelerates absolute paths and root-context descendant queries; extending it to relative-descendant queries is on the roadmap.

EXSLT — the extension pack

Since lib v1.3.0, leptris_exslt_enable(doc) activates15 native handlers on a document — first-party C implementations, not interpreted callbacks:

  • str:replace, tokenize (character-set delimiters), split (whole-pattern substrings), concat, padding
  • set:distinct, intersection, difference, leading, trailing
  • math:max, min, abs, sqrt, power

From Ruby: doc.exslt then query as usual. The extension is per-document — enabling it costs nothing until an extension function is called.

Thread safety

One document per thread needs no locking; sharing documents read-only across threads is safe. The XPath AST/bytecode cache is mutex-guarded and pin-counted, and leptris_thread_cleanup()drains per-thread caches when worker threads exit. Error reporting is thread-local, with per-document snapshots for XPath failures — the full suite runs ThreadSanitizer-clean.

Using it

c
LeptrisXPathResult r = leptris_xpath_eval(doc, NULL, "//item[@price > 10]");
if (r) {
    size_t n = leptris_xpath_result_count(r);
    for (size_t i = 0; i < n; i++) {
        LeptrisNodeRef node = leptris_xpath_result_node(r, i);
        printf("  %s\n", leptris_node_name(node));
    }
    leptris_xpath_result_free(r);
}

From Ruby: doc.xpath("//item"); from Python:doc.xpath("//item"); from the shell:leptris xpath doc.xml "//item". SeeRuby, Python, andCLI.

Where next

This is the curated guide. The repositories are canonical: when this page and the repo disagree, the repo wins. Full documentation lives with the source.