leptris 1.9.232.1 → 1.9.237.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +144 -3
  5. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  6. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  9. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  10. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  11. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  12. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  13. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  14. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  15. data/lib/leptris/version.rb +1 -1
  16. data/lib/leptris/xml/document.rb +20 -3
  17. data/lib/leptris/xml/ffi.rb +7 -0
  18. data/vendor-src/README.md +1 -1
  19. data/vendor-src/libleptris/CHANGELOG.md +113 -4
  20. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  21. data/vendor-src/libleptris/benchmarks/dom/bench_leptris.c +14 -6
  22. data/vendor-src/libleptris/src/include/leptris.h +27 -0
  23. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +20 -0
  24. data/vendor-src/libleptris/src/leptris/leptris_internal.h +5 -0
  25. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +7 -0
  26. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +38 -4
  27. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +5 -1
  28. data/vendor-src/libleptris/src/leptris/xpath/functions.c +86 -7
  29. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +237 -24
  30. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +16 -1
  31. data/vendor-src/libleptris/test/dom/test_dom.cpp +80 -0
  32. data/vendor-src/libleptris/test/xquery/qt3/fn/count.xml +3117 -0
  33. data/vendor-src/libleptris/test/xquery/qt3/fn/empty.xml +502 -0
  34. data/vendor-src/libleptris/test/xquery/qt3/fn/exists.xml +537 -0
  35. data/vendor-src/libleptris/test/xquery/qt3/fn/head.xml +90 -0
  36. data/vendor-src/libleptris/test/xquery/qt3/fn/tail.xml +68 -0
  37. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +124 -93
  38. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +28 -0
  39. data/vendor-src/libleptris/vcpkg.json +1 -1
  40. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 930bd899886c49109f40ce372e089ea775208a8fbf936f480bd6d7ff54b75af1
4
- data.tar.gz: 67272e8d6523e527136c00dc22fe6e43fdb544d382dfb264e938280887c94d90
3
+ metadata.gz: 664af13551043ffb7e8fab25cea28589568c0b3069374cdba6c227ef0dfe1cf3
4
+ data.tar.gz: 23c832153066bb250292b8e96f3e9f98c25b145904669f01cdf0432b15102608
5
5
  SHA512:
6
- metadata.gz: ece58bfa9c03ac493493fe72be20e998043158b87e10e83228c4f099e4f87032f9969334a52190868fc89b288b83a028cdae88df66e48296517d402bedf0f937
7
- data.tar.gz: 0013a56c78fc813ac9863f293ad77dc6818de54d07cb7e7ef2ee8ed1cc2c69173feec20a62700a968174d8914718e8117964d12da082e3938340a5964f611b49
6
+ metadata.gz: cc8ac10e52620a42e0b420a32a228722f221cfce1093c040fae1179858c9fe0de1629f5b110c8ac4da7ee13d8813972dd1a6e5b817bd6ee410b833d0f8b9224b
7
+ data.tar.gz: 6715dd6344773aae9e0c38cbce572c12ab71d2bd86a15468978085230b0995922bfc1eeba09ac914235bfe7da351bf1efe0e9c35fe392dae5dca84c084cbf1c1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.237.0] - 2026-09-24
9
+
10
+ ### Added
11
+
12
+ - `Document#create_element` accepts an attributes hash:
13
+ `doc.create_element("book", "id" => "1", "lang" => "en")` creates
14
+ the element with every attribute in ONE engine crossing
15
+ (leptris_element_new_with_attributes, #1344) — builder paths
16
+ collapse from 1 + N crossings to one. Duplicate names replace,
17
+ last wins; `nil` values store the empty string (engine
18
+ semantics). The native face falls back to create + per-pair sets
19
+ on pins predating #1344.
20
+
21
+ ### Performance — engine sync (1.9.237)
22
+
23
+ - Vendored libleptris 1.9.237: typed atoms for QT3 equality
24
+ (typed constructors, float32, the xs: constructor surface),
25
+ per-function equality semantics (index-of/distinct-values/
26
+ deep-equal), the F&O fn:subsequence window with NaN safety and
27
+ member-type preservation, the xpath per-eval registry init fix,
28
+ QT3 slice 3 (405 gated cases), the matched-shape traversal
29
+ benchmark correction (#1341: the structural gap vs libxml2 is
30
+ 1.5-1.7x, not 1.8-4.2x), and the single-crossing element
31
+ constructor (#1344).
32
+
8
33
  ## [1.9.232.0] - 2026-09-24
9
34
 
10
35
  ### Added
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.232"
11
+ LIBLEPTRIS_VERSION = "1.9.237"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -42,6 +42,8 @@ typedef size_t (*node_offset_fn)(void *);
42
42
  typedef const char *(*element_text_fn)(void *);
43
43
  typedef void *(*doc_create_fn)(void);
44
44
  typedef void *(*elem_create_fn)(void *, const char *);
45
+ typedef void *(*elem_new_attrs_fn)(void *, const char *, const char **,
46
+ const char **, size_t);
45
47
  typedef void *(*text_create_fn)(void *, const char *);
46
48
  typedef void *(*create_child_fn)(void *, const char *);
47
49
  typedef int (*append_child_fn)(void *, void *);
@@ -75,10 +77,14 @@ static node_offset_fn f_node_offset;
75
77
  static element_text_fn f_element_text;
76
78
  static doc_create_fn f_doc_create;
77
79
  static elem_create_fn f_elem_create;
80
+ static elem_new_attrs_fn f_elem_new_with_attrs;
78
81
  static text_create_fn f_text_create;
79
82
  static create_child_fn f_create_child;
80
83
  static append_child_fn f_append_child;
81
84
  static append_child_fn f_prepend_child, f_insert_after, f_insert_before;
85
+ typedef void *(*elem_new_attrs_fn)(void *, const char *, const char **,
86
+ const char **, size_t);
87
+ static elem_new_attrs_fn f_elem_new_with_attrs;
82
88
  static set_attr_fn f_set_attr;
83
89
  typedef int (*xp_type_fn)(void *);
84
90
  typedef void (*xp_free_fn)(void *);
@@ -218,6 +224,11 @@ static void resolve_symbols(const char *lib_path)
218
224
  f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
219
225
  f_doc_create = (doc_create_fn)lib_sym(h, "leptris_document_create");
220
226
  f_elem_create = (elem_create_fn)lib_sym(h, "leptris_element_create");
227
+ /* Single-crossing element construction (libleptris 1.9.237,
228
+ * #1344): NULL in older engine pins; the create path falls
229
+ * back to create + per-pair sets. */
230
+ f_elem_new_with_attrs =
231
+ (elem_new_attrs_fn)lib_sym(h, "leptris_element_new_with_attributes");
221
232
  f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
222
233
  f_create_child = (create_child_fn)lib_sym(h, "leptris_element_create_child");
223
234
  f_append_child = (append_child_fn)lib_sym(h, "leptris_element_append_child");
@@ -225,6 +236,11 @@ static void resolve_symbols(const char *lib_path)
225
236
  f_insert_after = (append_child_fn)lib_sym(h, "leptris_element_insert_after");
226
237
  f_insert_before = (append_child_fn)lib_sym(h, "leptris_element_insert_before");
227
238
  f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
239
+ /* Engine single-crossing construction (leptris#1344, 1.9.236+):
240
+ * optional; absent engines use the create_child + set_attr
241
+ * loop, which is still one Ruby->C crossing. */
242
+ f_elem_new_with_attrs =
243
+ (elem_new_attrs_fn)lib_sym(h, "leptris_element_new_with_attributes");
228
244
  f_xp_type = (xp_type_fn)lib_sym(h, "leptris_xpath_result_type");
229
245
  f_xp_free = (xp_free_fn)lib_sym(h, "leptris_xpath_result_free");
230
246
  f_first_child = (first_child_fn)lib_sym(h, "leptris_node_first_child");
@@ -1638,15 +1654,88 @@ static VALUE nf_bulk_attributes(VALUE self, VALUE addr)
1638
1654
  /* ---- Binding create via C (TODO.perf/09): one call creates the
1639
1655
  * node AND constructs the binding wrapper (class dispatch, ivars,
1640
1656
  * identity-cache store) — no FFI marshaling, no wrap_fresh path. */
1657
+ static int set_attr_pair(VALUE key, VALUE value, VALUE data)
1658
+ {
1659
+ void *elem =
1660
+ (void *)(uintptr_t)NUM2ULL(rb_ary_entry(data, 0));
1661
+ VALUE k = RB_TYPE_P(key, T_STRING) ? key : rb_obj_as_string(key);
1662
+ VALUE v = NIL_P(value) ? rb_usascii_str_new(0, 0)
1663
+ : (RB_TYPE_P(value, T_STRING) ? value
1664
+ : rb_obj_as_string(value));
1665
+
1666
+ rb_ary_push(rb_ary_entry(data, 1), k);
1667
+ rb_ary_push(rb_ary_entry(data, 2), v);
1668
+ f_set_attr(elem, RSTRING_PTR(k), RSTRING_PTR(v));
1669
+ return ST_CONTINUE;
1670
+ }
1671
+
1641
1672
  static VALUE nf_create_binding_element(VALUE self, VALUE document,
1642
- VALUE name)
1673
+ VALUE name, VALUE attrs)
1643
1674
  {
1644
1675
  void *doc = doc_ptr_of(document);
1645
- void *ptr = f_elem_create(doc, RSTRING_PTR(StringValue(name)));
1676
+ void *ptr;
1677
+ VALUE name_str = StringValue(name);
1646
1678
  VALUE node, cache, key;
1647
1679
 
1648
1680
  (void)self;
1649
1681
  resolve_binding_classes();
1682
+ if (attrs == Qnil ||
1683
+ (RB_TYPE_P(attrs, T_HASH) && RHASH_SIZE(attrs) == 0)) {
1684
+ ptr = f_elem_create(doc, RSTRING_PTR(name_str));
1685
+ } else if (f_elem_new_with_attrs != NULL) {
1686
+ /* #1344: element + every attribute in ONE crossing.
1687
+ * Duplicate names replace, last wins; NULL values store
1688
+ * the empty string — engine semantics, identical to
1689
+ * per-pair set_attribute.
1690
+ *
1691
+ * Iteration goes through keys + aref (no rb_hash_foreach
1692
+ * state pointer): every converted key/value is pushed into
1693
+ * Ruby arrays BEFORE its pointer enters the C arrays, so
1694
+ * temporaries (non-String keys, nil -> "") survive until
1695
+ * the engine call returns — no C pointer rides through a
1696
+ * boxed VALUE (long is 32-bit on Windows/LLP64; the
1697
+ * foreach+boxed-struct shape truncated it and segfaulted
1698
+ * the windows CI leg). */
1699
+ VALUE keys, names, values;
1700
+ const char **np, **vp;
1701
+ long cap, i;
1702
+ Check_Type(attrs, T_HASH);
1703
+ cap = (long)RHASH_SIZE(attrs);
1704
+ keys = rb_funcall(attrs, rb_intern("keys"), 0);
1705
+ names = rb_ary_new_capa(cap);
1706
+ values = rb_ary_new_capa(cap);
1707
+ np = ruby_xmalloc(sizeof(char *) * (cap + 1));
1708
+ vp = ruby_xmalloc(sizeof(char *) * (cap + 1));
1709
+ for (i = 0; i < cap; i++) {
1710
+ VALUE k = rb_ary_entry(keys, i);
1711
+ VALUE v = rb_hash_aref(attrs, k);
1712
+ k = RB_TYPE_P(k, T_STRING) ? k : rb_obj_as_string(k);
1713
+ v = NIL_P(v) ? rb_usascii_str_new(0, 0)
1714
+ : (RB_TYPE_P(v, T_STRING) ? v
1715
+ : rb_obj_as_string(v));
1716
+ rb_ary_push(names, k);
1717
+ rb_ary_push(values, v);
1718
+ np[i] = RSTRING_PTR(k);
1719
+ vp[i] = RSTRING_PTR(v);
1720
+ }
1721
+ ptr = f_elem_new_with_attrs(doc, RSTRING_PTR(name_str),
1722
+ np, vp, (size_t)cap);
1723
+ ruby_xfree(np);
1724
+ ruby_xfree(vp);
1725
+ } else {
1726
+ /* Older pin: create + per-pair sets (N+1 crossings). */
1727
+ VALUE addr = Qnil;
1728
+ VALUE anchor;
1729
+ Check_Type(attrs, T_HASH);
1730
+ anchor = rb_ary_new_capa((long)RHASH_SIZE(attrs));
1731
+ ptr = f_elem_create(doc, RSTRING_PTR(name_str));
1732
+ if (!ptr)
1733
+ return Qnil;
1734
+ addr = ULL2NUM((uint64_t)(uintptr_t)ptr);
1735
+ rb_hash_foreach(attrs, set_attr_pair,
1736
+ rb_ary_new3(3, addr, anchor,
1737
+ rb_ary_new_capa(0)));
1738
+ }
1650
1739
  if (!ptr)
1651
1740
  return Qnil; /* caller raises with the error channel */
1652
1741
  node = rb_obj_alloc(c_b_element);
@@ -2194,6 +2283,56 @@ static VALUE nf_plan_structs_typed_p(VALUE self)
2194
2283
  * to the per-attribute FFI chain (first/next/get_name/get_value,
2195
2284
  * 4 crossings each) — the win amplifies with attribute density
2196
2285
  * (40-attr elements: ~7x on the listing). */
2286
+ /* ---- Single-crossing element construction (leptris#1344) --------
2287
+ * Creates an element attached under parent (parent_addr 0 = free
2288
+ *floating, owner document only) with all attributes set — ONE
2289
+ * Ruby->C crossing for the whole construction. Internal calls are
2290
+ * C->C dlsym dispatches (create_child = create+attach in one
2291
+ * engine call; set_attr per attribute). Returns the new node's
2292
+ * address for wrapper minting. */
2293
+ static VALUE nf_create_element_with_attrs(VALUE self, VALUE document,
2294
+ VALUE parent_addr, VALUE name,
2295
+ VALUE attrs)
2296
+ {
2297
+ (void)self;
2298
+ (void)document;
2299
+ void *parent = NIL_P(parent_addr)
2300
+ ? NULL
2301
+ : (void *)(uintptr_t)NUM2ULL(parent_addr);
2302
+ const char *cname = StringValueCStr(name);
2303
+
2304
+ if (f_elem_new_with_attrs && parent && RARRAY_LEN(attrs) > 0 &&
2305
+ RARRAY_LEN(attrs) % 2 == 0) {
2306
+ /* Engine single-crossing construction (1.9.236+): arena
2307
+ * create + all attributes with no per-attr calls at all. */
2308
+ long n = RARRAY_LEN(attrs) / 2;
2309
+ const char **names = ALLOCA_N(const char *, n);
2310
+ const char **values = ALLOCA_N(const char *, n);
2311
+ for (long i = 0; i < n; i++) {
2312
+ /* Hoist to lvalues: truffleruby's StringValueCStr takes
2313
+ * the address of its argument (RARRAY_AREF is not one). */
2314
+ VALUE an = RARRAY_AREF(attrs, 2 * i);
2315
+ VALUE av = RARRAY_AREF(attrs, 2 * i + 1);
2316
+ names[i] = StringValueCStr(an);
2317
+ values[i] = StringValueCStr(av);
2318
+ }
2319
+ void *elem = f_elem_new_with_attrs(parent, cname, names, values,
2320
+ n);
2321
+ return elem ? ULL2NUM((uintptr_t)elem) : Qnil;
2322
+ }
2323
+
2324
+ void *elem = parent ? f_create_child(parent, cname) : NULL;
2325
+ if (!elem) return Qnil;
2326
+
2327
+ long i, n = RARRAY_LEN(attrs);
2328
+ for (i = 0; i + 1 < n; i += 2) {
2329
+ VALUE an = RARRAY_AREF(attrs, i);
2330
+ VALUE av = RARRAY_AREF(attrs, i + 1);
2331
+ f_set_attr(elem, StringValueCStr(an), StringValueCStr(av));
2332
+ }
2333
+ return ULL2NUM((uintptr_t)elem);
2334
+ }
2335
+
2197
2336
  static VALUE nf_attribute_rows(VALUE self, VALUE document, VALUE addr)
2198
2337
  {
2199
2338
  (void)self;
@@ -2808,6 +2947,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
2808
2947
  nf_plan_structs, 3);
2809
2948
  rb_define_module_function(m_native, "attribute_rows",
2810
2949
  nf_attribute_rows, 2);
2950
+ rb_define_module_function(m_native, "create_element_with_attrs",
2951
+ nf_create_element_with_attrs, 4);
2811
2952
  rb_define_module_function(m_native, "plan_structs_typed?",
2812
2953
  nf_plan_structs_typed_p, 0);
2813
2954
  rb_define_module_function(m_native, "doc_handle_attach",
@@ -2822,7 +2963,7 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
2822
2963
  rb_define_module_function(m_native, "bulk_attr_faces",
2823
2964
  nf_bulk_attr_faces, 2);
2824
2965
  rb_define_module_function(m_native, "create_binding_element",
2825
- nf_create_binding_element, 2);
2966
+ nf_create_binding_element, 3);
2826
2967
  rb_define_module_function(m_native, "create_binding_text",
2827
2968
  nf_create_binding_text, 2);
2828
2969
  rb_define_module_function(m_native, "bulk_children", nf_bulk_children, 2);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.232.1"
4
+ VERSION = "1.9.237.0"
5
5
  end
@@ -389,15 +389,32 @@ class Leptris::XML::Document
389
389
  element
390
390
  end
391
391
 
392
- def create_element(name)
392
+ # attributes: optional Hash of {name => value} applied in the
393
+ # SAME engine crossing as construction
394
+ # (leptris_element_new_with_attributes, #1344) — builder paths
395
+ # collapse from 1 + N crossings to one. nil values store the
396
+ # empty string; duplicate names replace, last wins (engine
397
+ # semantics, identical to per-pair set_attribute).
398
+ def create_element(name, attributes = nil)
393
399
  # TODO.perf/09: one C call creates + wraps when the ext is
394
400
  # loaded — no FFI marshaling, no wrap_fresh frames.
395
401
  if defined?(Leptris::XML::NATIVE_FAST)
396
- node = Leptris::XML::Native.create_binding_element(self, name.to_s)
402
+ node = Leptris::XML::Native.create_binding_element(
403
+ self, name.to_s, attributes)
397
404
  raise Leptris::XML::Error, "leptris_element_create failed" if node.nil?
398
405
  return node
399
406
  end
400
- ptr = Leptris::XML::FFI.leptris_element_create(c_ptr, name)
407
+ ptr =
408
+ if attributes && !attributes.empty?
409
+ names = attributes.map { |k, _| k.to_s }
410
+ values = attributes.map { |_, v| v.nil? ? "" : v.to_s }
411
+ nb, na = Leptris::XML::CStringArray.to_c(names)
412
+ vb, va = Leptris::XML::CStringArray.to_c(values)
413
+ Leptris::XML::FFI.leptris_element_new_with_attributes(
414
+ c_ptr, name.to_s, nb, vb, names.size)
415
+ else
416
+ Leptris::XML::FFI.leptris_element_create(c_ptr, name)
417
+ end
401
418
  raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
402
419
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ELEMENT)
403
420
  end
@@ -716,6 +716,13 @@ attach_function :leptris_parse_string,
716
716
  attach_function :leptris_element_previous_sibling_any,
717
717
  [:leptris_element], :leptris_element
718
718
 
719
+ # Single-crossing element construction (libleptris 1.9.237,
720
+ # #1344): element + every attribute in one C call. The name
721
+ # and value arrays are count-bounded char** (the NULL
722
+ # terminators CStringArray appends are simply never read).
723
+ attach_function :leptris_element_new_with_attributes,
724
+ [:leptris_document, :string, :pointer, :pointer, :size_t],
725
+ :leptris_element
719
726
  attach_function :leptris_element_create,
720
727
  [:leptris_document, :string], :leptris_element
721
728
  attach_function :leptris_element_set_name,
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.232
4
+ - libleptris v1.9.237
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,102 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.237] - 2026-09-24
4
+
5
+ <!-- Edit this section with the actual release notes. -->
6
+ <!-- See https://keepachangelog.com for format guidance. -->
7
+
8
+ ### Added
9
+
10
+ - dom: `leptris_element_new_with_attributes` — single-crossing
11
+ element construction (#1344). One call creates the element in the
12
+ document arena with every attribute attached — identical per-pair
13
+ semantics to leptris_element_set_attribute (duplicate names
14
+ replace, last wins; NULL values store the empty string). Builder
15
+ paths collapse from 1 + N crossings plus wrapper allocations per
16
+ node to a single crossing.
17
+
18
+
19
+ ## [1.9.236] - 2026-09-24
20
+
21
+ ### Added
22
+
23
+ - xpath: fn:subsequence implements the F&O window in position space —
24
+ NaN start/length yields empty, the window is NaN-safe including
25
+ -INF + INF = NaN, positions are never clamped to 1, the 2-arg form
26
+ is unbounded, and start/length round half-up like fn:round
27
+ (cbcl-subsequence-001..009, K2-5).
28
+ - xpath: boolean and xs:decimal sequence members carry their type
29
+ (\x03B/\x03D markers) — a false boolean stays falsy through
30
+ fn:subsequence/fn:remove round-trips (EBV reads the marker) and
31
+ xs:decimal equality compares lexically where the double carrier
32
+ cannot hold 30 significant digits.
33
+ - xpath: fn:subsequence and fn:remove preserve member types, so
34
+ `instance of`, deep-equal and effective boolean value agree across
35
+ the round-trip.
36
+ - QT3 runner: assert-permutation evaluates expected items as
37
+ expressions.
38
+
39
+ ### Fixed
40
+
41
+ - QT3 corpus: subsequence 51→61, distinct-values 84→87, remove
42
+ 21→23 adopted; typed-atom pins 86→67.
43
+
44
+
45
+
46
+ ## [1.9.235] - 2026-09-24
47
+
48
+ ### Added
49
+
50
+ - xpath: typed atoms for QT3 equality — an atomic_type channel on
51
+ XPath results (typed constructors carry interned type names),
52
+ float32 precision for xs:float (float(1.01) ne double(1.01) while
53
+ decimal-vs-float promotes per F&O rank), typed \x03F sequence
54
+ markers, and the missing constructor surface (xs:untypedAtomic,
55
+ xs:hexBinary, xs:base64Binary, xs:gYear/gYearMonth/gMonthDay/
56
+ gMonth/gDay).
57
+
58
+ ### Fixed
59
+
60
+ - xpath: per-function equality semantics — fn:index-of never matches
61
+ NaN and does not coerce numbers to strings; fn:distinct-values
62
+ dedups NaN with itself and keeps the first spelling
63
+ (distinct((1, 2.0, 3, 2)) = 1, 2.0, 3); fn:reverse, fn:index-of and
64
+ fn:distinct-values preserve sequence-member types.
65
+ - xpath: fn:deep-equal ignores comment/PI children (F&O 3.0),
66
+ compares typed string classes (xs:date vs string = false), and
67
+ accepts the 3-argument collation form.
68
+ - QT3 corpus: deep-equal 180→192, distinct-values 74→84, index-of
69
+ 34→42, reverse 53→57 adopted (125 typed-atom pins → 86).
70
+
71
+
72
+
73
+ ## [1.9.234] - 2026-09-24
74
+
75
+ ### Fixed
76
+
77
+ - benchmarks: the DOM Tree Traversal row now walks the documented
78
+ O(1) iteration shape (`leptris_element_first_child_any` +
79
+ `leptris_element_next_sibling_any`). The previous shape used
80
+ `child_count()` + indexed `child(elem, i)` — O(N^2/2) re-seeking
81
+ per parent — which measured the indexed-lookup algorithm instead of
82
+ per-hop engine cost, overstating the libxml2 gap at 1.8-4.2x.
83
+ Matched-shape gap vs libxml2 is 1.5-1.7x (#1341, closed as the
84
+ structural cost of the 64-byte compact node design).
85
+
86
+
87
+
88
+ ## [1.9.233] - 2026-09-24
89
+
90
+ ### Added
91
+
92
+ - QT3 corpus slice 3 — empty/exists/head/tail/count, 405 gated cases (xquery)
93
+
94
+ ### Fixed
95
+
96
+ - initialize registry_borrowed on every context init (xpath)
97
+
98
+
99
+
3
100
  ## [1.9.232] - 2026-09-24
4
101
 
5
102
  ### Added
@@ -591,9 +688,15 @@ W3C QT3 suite as the gate:
591
688
  <!-- Edit this section with the actual release notes. -->
592
689
  <!-- See https://keepachangelog.com for format guidance. -->
593
690
 
594
- ### Changed
691
+ ### Added
595
692
 
596
- - (describe changes here)
693
+ - dom: `leptris_element_new_with_attributes` — single-crossing
694
+ element construction (#1344). One call creates the element in the
695
+ document arena with every attribute attached — identical per-pair
696
+ semantics to leptris_element_set_attribute (duplicate names
697
+ replace, last wins; NULL values store the empty string). Builder
698
+ paths collapse from 1 + N crossings plus wrapper allocations per
699
+ node to a single crossing.
597
700
 
598
701
 
599
702
  ## [1.9.200] - 2026-09-18
@@ -8293,9 +8396,15 @@ fall back to the legacy parser.
8293
8396
  <!-- Edit this section with the actual release notes. -->
8294
8397
  <!-- See https://keepachangelog.com for format guidance. -->
8295
8398
 
8296
- ### Changed
8399
+ ### Added
8297
8400
 
8298
- - (describe changes here)
8401
+ - dom: `leptris_element_new_with_attributes` — single-crossing
8402
+ element construction (#1344). One call creates the element in the
8403
+ document arena with every attribute attached — identical per-pair
8404
+ semantics to leptris_element_set_attribute (duplicate names
8405
+ replace, last wins; NULL values store the empty string). Builder
8406
+ paths collapse from 1 + N crossings plus wrapper allocations per
8407
+ node to a single crossing.
8299
8408
 
8300
8409
 
8301
8410
  All notable changes to Leptris will be documented in this file.
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.232
6
+ VERSION 1.9.237
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -35,7 +35,14 @@ typedef struct {
35
35
  LeptrisDocument doc;
36
36
  } doc_ctx_t;
37
37
 
38
- /* Recursive tree traversal helper */
38
+ /* Recursive tree traversal helper.
39
+ *
40
+ * Walks the same shape as the libxml2 bench (hop EVERY child via the
41
+ * sibling link, recurse into elements): first_child_any +
42
+ * next_sibling_any is the documented O(1)-per-step iteration. The
43
+ * previous shape — child_count() + child(elem, i) — re-seeked the
44
+ * child list from its head per access, O(N²/2) hops per parent, which
45
+ * measured the indexed-lookup algorithm, not per-hop engine cost. */
39
46
  static void traverse_tree(LeptrisElement elem) {
40
47
  if (!elem) return;
41
48
 
@@ -43,11 +50,12 @@ static void traverse_tree(LeptrisElement elem) {
43
50
  const char* name = leptris_element_name(elem);
44
51
  (void)name;
45
52
 
46
- /* Traverse children */
47
- size_t child_count = leptris_element_child_count(elem);
48
- for (size_t i = 0; i < child_count; i++) {
49
- LeptrisElement child = leptris_element_child(elem, i);
50
- traverse_tree(child);
53
+ /* Traverse children (hop every child; recurse into elements) */
54
+ for (LeptrisElement child = leptris_element_first_child_any(elem);
55
+ child; child = leptris_element_next_sibling_any(child)) {
56
+ if (leptris_node_get_type((LeptrisNodeRef)child) ==
57
+ LEPTRIS_NODE_TYPE_ELEMENT)
58
+ traverse_tree(child);
51
59
  }
52
60
  }
53
61
 
@@ -1984,6 +1984,33 @@ LEPTRIS_API size_t leptris_element_hash_value(LeptrisElement elem);
1984
1984
  */
1985
1985
  LEPTRIS_API LeptrisElement leptris_element_create(LeptrisDocument doc, const char* name);
1986
1986
 
1987
+ /**
1988
+ * Create a new element with attributes set in one call (#1344)
1989
+ *
1990
+ * The fused twin of create + N x set_attribute: one crossing
1991
+ * builds the element with every attribute attached — the
1992
+ * builder-shape answer for programmatic document construction.
1993
+ * Attribute semantics are identical to leptris_element_set_attribute
1994
+ * per pair (same name handling, same value copy); a duplicate name
1995
+ * replaces, last wins.
1996
+ *
1997
+ * @param doc Document that will own the element
1998
+ * @param name Element name (QName prefixes handled like
1999
+ * leptris_element_create)
2000
+ * @param attr_names Attribute names; may be NULL when attr_count
2001
+ * is 0. A NULL element or NULL name string fails
2002
+ * the call.
2003
+ * @param attr_values Attribute values; a NULL entry stores the
2004
+ * empty string
2005
+ * @param attr_count Number of name/value pairs
2006
+ * @return New element or NULL on error
2007
+ *
2008
+ * Memory: Element owned by document, freed when document freed
2009
+ */
2010
+ LEPTRIS_API LeptrisElement leptris_element_new_with_attributes(
2011
+ LeptrisDocument doc, const char* name, const char** attr_names,
2012
+ const char** attr_values, size_t attr_count);
2013
+
1987
2014
  /**
1988
2015
  * Set element name (rename element tag)
1989
2016
  *
@@ -232,6 +232,26 @@ LeptrisElement leptris_element_create(LeptrisDocument doc, const char* name) {
232
232
  return elem;
233
233
  }
234
234
 
235
+ /* #1344: fused create + attribute-set — one crossing for the
236
+ * builder path. Identical per-pair semantics to
237
+ * leptris_element_set_attribute. */
238
+ LeptrisElement leptris_element_new_with_attributes(
239
+ LeptrisDocument doc, const char* name, const char** attr_names,
240
+ const char** attr_values, size_t attr_count) {
241
+ LeptrisElement elem = leptris_element_create(doc, name);
242
+ if (!elem) return NULL;
243
+ if (attr_count && !attr_names) return NULL;
244
+ for (size_t i = 0; i < attr_count; i++) {
245
+ if (!attr_names[i]) return NULL;
246
+ if (leptris_element_set_attribute(
247
+ elem, attr_names[i],
248
+ (attr_values && attr_values[i]) ? attr_values[i] : "") !=
249
+ LEPTRIS_OK)
250
+ return NULL;
251
+ }
252
+ return elem;
253
+ }
254
+
235
255
  /**
236
256
  * Append child element (Public API)
237
257
  */
@@ -649,6 +649,11 @@ struct leptris_xpath_result {
649
649
  * free-list. */
650
650
  int is_int;
651
651
  long long int_value;
652
+ /* Typed-atom tag (QT3 slice): interned type name ("xs:date",
653
+ * "xs:float", ...) when the atom came from a typed constructor,
654
+ * NULL for the untyped/string class. NOT owned — static
655
+ * literals only. Cleared in result_new (free-list recycle). */
656
+ const char* atomic_type;
652
657
  };
653
658
 
654
659
  /* Namespace mapping for XPath context (v0.8.0) */
@@ -126,6 +126,12 @@ void xpath_context_init(XPathContext* context,
126
126
  context->input_len = 0;
127
127
 
128
128
  extern XPathFunctionRegistry* leptris_xpath_build_custom_registry(struct leptris_document*);
129
+ /* The borrow flag MUST be established on every init: the
130
+ * context is stack storage and cleanup's registry-free is
131
+ * gated on this flag — stale stack bytes read as borrowed=1
132
+ * and leak the per-eval XQuery registry (the common no-custom
133
+ * -fns path never wrote the flag). */
134
+ context->registry_borrowed = 0;
129
135
  context->function_registry = leptris_xpath_build_custom_registry(document);
130
136
  if (!context->function_registry) {
131
137
  context->function_registry = xpath_function_registry_get_standard();
@@ -736,6 +742,7 @@ struct leptris_xpath_result* xpath_result_new(XPathResultType type) {
736
742
 
737
743
  result->type = type;
738
744
  result->is_int = 0; /* free-list recycling would leak it */
745
+ result->atomic_type = NULL;
739
746
 
740
747
  /* Initialize union based on type. XPATH_RESULT_CACHED is the
741
748
  * free-list sentinel — a fresh result never carries it. */