leptris 1.9.231.1 → 1.9.232.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +35 -4
  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/ffi.rb +4 -0
  17. data/lib/leptris/xml/node.rb +28 -0
  18. data/vendor-src/README.md +1 -1
  19. data/vendor-src/libleptris/CHANGELOG.md +15 -0
  20. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  21. data/vendor-src/libleptris/src/include/leptris/descriptor.h +5 -1
  22. data/vendor-src/libleptris/src/include/leptris.h +17 -0
  23. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +26 -6
  24. data/vendor-src/libleptris/src/leptris/dom/node.c +24 -7
  25. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +22 -8
  26. data/vendor-src/libleptris/src/leptris/xpath/functions.c +29 -8
  27. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +30 -1
  28. data/vendor-src/libleptris/src/leptris/xpath/parser.c +106 -6
  29. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +68 -0
  30. data/vendor-src/libleptris/test/dom/test_dom.cpp +56 -0
  31. data/vendor-src/libleptris/test/xquery/qt3/fn/codepoints-to-string.xml +808 -0
  32. data/vendor-src/libleptris/test/xquery/qt3/fn/deep-equal.xml +2709 -0
  33. data/vendor-src/libleptris/test/xquery/qt3/fn/distinct-values.xml +1223 -0
  34. data/vendor-src/libleptris/test/xquery/qt3/fn/index-of.xml +493 -0
  35. data/vendor-src/libleptris/test/xquery/qt3/fn/insert-before.xml +399 -0
  36. data/vendor-src/libleptris/test/xquery/qt3/fn/remove.xml +480 -0
  37. data/vendor-src/libleptris/test/xquery/qt3/fn/reverse.xml +653 -0
  38. data/vendor-src/libleptris/test/xquery/qt3/fn/string-to-codepoints.xml +452 -0
  39. data/vendor-src/libleptris/test/xquery/qt3/fn/subsequence.xml +1041 -0
  40. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +179 -1
  41. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +27 -0
  42. data/vendor-src/libleptris/vcpkg.json +1 -1
  43. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d25743a8c9d06599ec09676d0e8eeff1b5cbdf002ba969c2453ca9ba28d6d7d
4
- data.tar.gz: 0d205b589103d95d62664bbe0298769efdd199b75025c48a78133847b46d9b3d
3
+ metadata.gz: b5716284785d7f3ff534b89dbacab287b7479ffe7ab66609db4c0df0cdab2ad0
4
+ data.tar.gz: fa2e4f4a8ab6e5e2aed150b6ee246c63a5711853c72c33179012db27d6122a4d
5
5
  SHA512:
6
- metadata.gz: 34e8b21544c92d33c6087c6b139ffe19e83db044f5135aaee300382250f4e408d04f8f3d283ec88b84a2025623d8c2deba77134bbaebbc87a359c61fc08df2e1
7
- data.tar.gz: 3ce1244314927e7e048ca865b763e510e0e84c9055725fdf89814e89838badab7ea6ce1f539685eabfeb6994bd0912a63c2afa086a40a96ca4b94375cc538b13
6
+ metadata.gz: e2737c46a2462a96a9f98e17106fc7ca921d9f5718273b5a2a6abd424e6351feff2a3af1ac716ee5b54dda923cd68f9b8adc2f044935cc6838b24204474b727b
7
+ data.tar.gz: c3e1340928ec767dc669960b09e83c74f000b559c6a90607e5ec9a5316cc25077d6e7564c90fea50d4900180ce0fb6d1ceb33fc916753e252c7d452e17aaca64
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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.232.0] - 2026-09-24
9
+
10
+ ### Added
11
+
12
+ - `Node#visit_entering` (libleptris #1332): the entering-only walk
13
+ — one C call yields `(node, true, depth)` exactly once per node,
14
+ halving the C→Ruby transitions against `#visit` for walks that
15
+ never acted on the leaving half. Same order and depths as
16
+ `#visit`; the native face falls back to the filtering visit on
17
+ pins predating #1332.
18
+
19
+ ### Performance — engine sync (1.9.232)
20
+
21
+ - Vendored libleptris 1.9.232: the per-eval registry leak fix, the
22
+ range NaN fix, string-literal character-reference expansion, and
23
+ the plan-walker prefixed-name fix — moxml/lutaml hydration of
24
+ `w:*` elements resolves correctly with no binding-side change —
25
+ plus 907 new QT3-gated engine behaviors.
26
+
8
27
  ## [1.9.231.0] - 2026-09-23
9
28
 
10
29
  ### Fixed
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.231"
11
+ LIBLEPTRIS_VERSION = "1.9.232"
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
@@ -971,10 +971,39 @@ static VALUE nf_visit_binding(VALUE self, VALUE document, VALUE addr)
971
971
  st.klass_memo_flag =
972
972
  rb_obj_class(document) == c_iteration_scope ? Qfalse : Qtrue;
973
973
  st.for_visit = 1;
974
- if (f_node_visit_entering)
975
- f_node_visit_entering(st.self_ptr, visit_cb, &st);
976
- else
977
- f_node_visit(st.self_ptr, visit_cb, &st);
974
+ /* ALWAYS the two-event engine call: this seam backs the PUBLIC
975
+ * Node#visit, whose contract (specs since libleptris 1.9.20)
976
+ * is (node, entering, depth) enter/leave PAIRS. Entering-only
977
+ * walks belong to visit_entering_binding below — swapping the
978
+ * call here silently dropped every leaving callback and broke
979
+ * the contract. */
980
+ f_node_visit(st.self_ptr, visit_cb, &st);
981
+ if (st.err != Qnil)
982
+ rb_exc_raise(st.err);
983
+ return Qnil;
984
+ }
985
+
986
+ /* Entering-only walk (libleptris #1332): one callback per node.
987
+ * Yields the same (node, true, depth) shape as visit_binding — the
988
+ * entering argument is constant true here, but keeping the 3-arity
989
+ * yield lets a block be shared between the two walk modes. */
990
+ static VALUE nf_visit_entering_binding(VALUE self, VALUE document,
991
+ VALUE addr)
992
+ {
993
+ struct trav_state st;
994
+
995
+ (void)self;
996
+ if (!f_node_visit_entering)
997
+ return nf_visit_binding(self, document, addr);
998
+ resolve_binding_classes();
999
+ st.document = document;
1000
+ st.err = Qnil;
1001
+ st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
1002
+ st.cache = binding_cache_of(document);
1003
+ st.klass_memo_flag =
1004
+ rb_obj_class(document) == c_iteration_scope ? Qfalse : Qtrue;
1005
+ st.for_visit = 1;
1006
+ f_node_visit_entering(st.self_ptr, visit_cb, &st);
978
1007
  if (st.err != Qnil)
979
1008
  rb_exc_raise(st.err);
980
1009
  return Qnil;
@@ -2739,6 +2768,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
2739
2768
  nf_traverse_binding, 2);
2740
2769
  rb_define_module_function(m_native, "visit_binding",
2741
2770
  nf_visit_binding, 2);
2771
+ rb_define_module_function(m_native, "visit_entering_binding",
2772
+ nf_visit_entering_binding, 2);
2742
2773
  rb_define_module_function(m_native, "fast_text_content",
2743
2774
  nf_fast_text_content, 1);
2744
2775
  rb_define_module_function(m_native, "fast_comment_content",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.231.1"
4
+ VERSION = "1.9.232.0"
5
5
  end
@@ -447,6 +447,10 @@ attach_function :leptris_parse_string,
447
447
  # node walks the document child chain.
448
448
  attach_function :leptris_node_visit,
449
449
  [:leptris_node_ref, :pointer, :pointer], :void
450
+ # Entering-only walk (libleptris #1332): one callback per
451
+ # node. Rides the engine pin that ships it.
452
+ attach_function :leptris_node_visit_entering,
453
+ [:leptris_node_ref, :pointer, :pointer], :void
450
454
  # Document-level PIs (v1.6.0): not tree nodes — enumerate via
451
455
  # these accessors only.
452
456
  attach_function :leptris_document_pi_count,
@@ -545,6 +545,34 @@ class Leptris::XML::Node
545
545
  self
546
546
  end
547
547
 
548
+ # Visits the subtree with ONE C call, yielding ONCE per node
549
+ # (leptris_node_visit_entering, libleptris #1332): every node —
550
+ # elements included — yields (node, true, depth) exactly once, on
551
+ # entering. Halves the C-callback invocations against #visit for
552
+ # walks that never acted on the leaving half (the family-walk
553
+ # shape); the halving happens in the engine, not in a per-node
554
+ # Ruby filter. Same order and depths as #visit; read-only.
555
+ #
556
+ # root.visit_entering { |node, entering, depth| ... }
557
+ def visit_entering(&block)
558
+ return enum_for(:visit_entering) unless block
559
+ ensure_alive!
560
+ if @addr_reads_fast
561
+ Leptris::XML::Native.visit_entering_binding(@document,
562
+ @c_address, &block)
563
+ return self
564
+ end
565
+ document = @document
566
+ visitor = ::FFI::Function.new(
567
+ :void, [:pointer, :pointer, :int, :int], blocking: true) do |_, node_ptr, entering, depth|
568
+ block.call(
569
+ Leptris::XML::Node.wrap(node_ptr, document),
570
+ entering == 1, depth)
571
+ end
572
+ Leptris::XML::FFI.leptris_node_visit_entering(c_ptr, visitor, nil)
573
+ self
574
+ end
575
+
548
576
  # Walks the subtree in post-order DFS (matches Nokogiri's
549
577
  # semantics): the receiver, its descendants, nothing else.
550
578
  #
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.231
4
+ - libleptris v1.9.232
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.232] - 2026-09-24
4
+
5
+ ### Added
6
+
7
+ - QT3 corpus slice 4 — sequence-edit + codepoint families, 501 gated cases; seq member fidelity fixes (xquery)
8
+ - entering-only visit mode — leptris_node_visit_entering (#1332) (dom)
9
+
10
+ ### Fixed
11
+
12
+ - range A to B with an empty operand yields the empty sequence (xpath)
13
+ - expand character and predefined entity references in string literals (xpath)
14
+ - prefixed wire names bind namespaced elements (XSD-002 fallout) (descriptor)
15
+
16
+
17
+
3
18
  ## [1.9.231] - 2026-09-23
4
19
 
5
20
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.231
6
+ VERSION 1.9.232
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -98,7 +98,11 @@ typedef struct {
98
98
  } leptris_attr_plan;
99
99
 
100
100
  typedef struct {
101
- const char* wire_name; /* XML element name as it appears on the wire */
101
+ const char* wire_name; /* XML element name as it appears on the wire:
102
+ * the prefixed form ("w:b") binds that literal
103
+ * prefix+local pair; the bare local form ("b")
104
+ * binds by local name and lets ns_form do the
105
+ * namespace work */
102
106
  uint8_t kind; /* LeptrisPlanKind */
103
107
  uint8_t type_tag; /* host-defined; echoed back verbatim */
104
108
  int32_t child_plan_index; /* NESTED: index into leptris_plan_spec.plans;
@@ -1195,6 +1195,23 @@ LEPTRIS_API void leptris_node_visit(LeptrisNodeRef root,
1195
1195
  LeptrisNodeVisitor visitor,
1196
1196
  void* user_data);
1197
1197
 
1198
+ /**
1199
+ * Entering-only subtree visitation (issue #1332)
1200
+ *
1201
+ * Same walk as leptris_node_visit — document order, same depths —
1202
+ * but the callback fires ONCE per node, on entering. Elements are
1203
+ * not re-visited after their subtree. Halves the C-callback
1204
+ * invocations for consumers that only act on entering=1 (the lazy
1205
+ * family-walk shape).
1206
+ *
1207
+ * @param root Subtree root (any node kind, or a document node)
1208
+ * @param visitor Callback per node (may be NULL — no-op)
1209
+ * @param user_data Opaque context for the callback
1210
+ */
1211
+ LEPTRIS_API void leptris_node_visit_entering(LeptrisNodeRef root,
1212
+ LeptrisNodeVisitor visitor,
1213
+ void* user_data);
1214
+
1198
1215
  /**
1199
1216
  * Count the document's top-level processing instructions
1200
1217
  *
@@ -440,6 +440,29 @@ LEPTRIS_API void leptris_plan_free(LeptrisPlan plan) { dp_plan_free(plan); }
440
440
  /* ---- walk ------------------------------------------------------- */
441
441
 
442
442
  /* Namespace binding for a child element against a row's target form. */
443
+ /* Child-row wire-name match. The ABI documents wire_name as "the
444
+ * XML element name as it appears on the wire": a row may carry the
445
+ * PREFIXED form ("w:b") or the bare local name ("b"). Rows with a
446
+ * colon bind prefix+local (literal prefix, per the wire contract);
447
+ * bare rows keep the historical local-name match with the row's
448
+ * ns_form doing the namespace work. The old matcher compared the
449
+ * local name only, so every prefixed row — the shape hosts
450
+ * compiling from xsd:QName schemas emit — silently bound nothing
451
+ * and whole subtrees hydrated empty (XSD-002 fallout). */
452
+ static int dp_wire_name_matches(LeptrisElement child,
453
+ const char* wire_name) {
454
+ const char* local = NULL;
455
+ const char* prefix = NULL;
456
+ leptris_element_expanded_name(child, &local, &prefix, NULL);
457
+ if (!wire_name || !local) return 0;
458
+ const char* colon = strchr(wire_name, ':');
459
+ if (!colon) return strcmp(local, wire_name) == 0;
460
+ size_t plen = (size_t)(colon - wire_name);
461
+ return prefix != NULL && strlen(prefix) == plen &&
462
+ strncmp(prefix, wire_name, plen) == 0 &&
463
+ strcmp(local, colon + 1) == 0;
464
+ }
465
+
443
466
  static int dp_ns_binds(LeptrisElement elem, uint8_t ns_form,
444
467
  const char* ns_uri, int lenient) {
445
468
  const char* local = NULL;
@@ -617,9 +640,8 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
617
640
  if (leptris_node_get_type(n) != LEPTRIS_NODE_TYPE_ELEMENT)
618
641
  continue;
619
642
  LeptrisElement child = (LeptrisElement)n;
620
- const char* local = NULL;
621
- leptris_element_expanded_name(child, &local, NULL, NULL);
622
- if (!local || strcmp(local, row->wire_name) != 0) continue;
643
+ if (!dp_wire_name_matches(child, row->wire_name))
644
+ continue;
623
645
  if (!dp_ns_binds(child, target->ns_form, target->ns_uri,
624
646
  lenient))
625
647
  continue;
@@ -666,9 +688,7 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
666
688
  if (leptris_node_get_type(n) != LEPTRIS_NODE_TYPE_ELEMENT)
667
689
  continue;
668
690
  LeptrisElement child = (LeptrisElement)n;
669
- const char* local = NULL;
670
- leptris_element_expanded_name(child, &local, NULL, NULL);
671
- if (!local || strcmp(local, row->wire_name) != 0) continue;
691
+ if (!dp_wire_name_matches(child, row->wire_name)) continue;
672
692
  /* #1272: same-wire-name claim — if a previous row bound
673
693
  * this node, skip it. */
674
694
  int rank = -1;
@@ -389,18 +389,20 @@ void leptris_node_set_next_sibling(LeptrisNode* node, LeptrisNode* sibling) {
389
389
  * lazily from the callback instead of materializing per-level
390
390
  * NodeSet/Array allocations. */
391
391
  static void node_visit_rec(LeptrisNode* n, int depth,
392
- LeptrisNodeVisitor visitor, void* user_data) {
392
+ LeptrisNodeVisitor visitor, void* user_data,
393
+ int entering_only) {
393
394
  int is_elem = (n->type == LEPTRIS_NODE_TYPE_ELEMENT);
394
395
  visitor(user_data, (LeptrisNodeRef)n, 1, depth);
395
396
  if (!is_elem) return;
396
397
  for (LeptrisNode* c = leptris_elem_first_child((LeptrisElement)n); c;
397
398
  c = leptris_node_get_next_sibling(c))
398
- node_visit_rec(c, depth + 1, visitor, user_data);
399
- visitor(user_data, (LeptrisNodeRef)n, 0, depth);
399
+ node_visit_rec(c, depth + 1, visitor, user_data, entering_only);
400
+ if (!entering_only)
401
+ visitor(user_data, (LeptrisNodeRef)n, 0, depth);
400
402
  }
401
403
 
402
- void leptris_node_visit(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
403
- void* user_data) {
404
+ static void node_visit_impl(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
405
+ void* user_data, int entering_only) {
404
406
  if (!root || !visitor) return;
405
407
  if (root->type == LEPTRIS_NODE_TYPE_DOCUMENT) {
406
408
  /* Document root: children = the document child chain (#580),
@@ -413,8 +415,23 @@ void leptris_node_visit(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
413
415
  if (!start) start = (LeptrisNode*)d->root;
414
416
  for (LeptrisNode* c = start; c;
415
417
  c = leptris_node_get_next_sibling(c))
416
- node_visit_rec(c, 0, visitor, user_data);
418
+ node_visit_rec(c, 0, visitor, user_data, entering_only);
417
419
  return;
418
420
  }
419
- node_visit_rec((LeptrisNode*)root, 0, visitor, user_data);
421
+ node_visit_rec((LeptrisNode*)root, 0, visitor, user_data,
422
+ entering_only);
423
+ }
424
+
425
+ void leptris_node_visit(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
426
+ void* user_data) {
427
+ node_visit_impl(root, visitor, user_data, 0);
428
+ }
429
+
430
+ /* Issue #1332: entering-only mode — one callback per node. The
431
+ * family-walk consumers all discard the leaving half of
432
+ * leptris_node_visit; this skips those invocations at the source. */
433
+ void leptris_node_visit_entering(LeptrisNodeRef root,
434
+ LeptrisNodeVisitor visitor,
435
+ void* user_data) {
436
+ node_visit_impl(root, visitor, user_data, 1);
420
437
  }
@@ -12,6 +12,7 @@
12
12
  #include <string.h>
13
13
  #include <stdio.h>
14
14
  #include <stdlib.h>
15
+ #include <limits.h>
15
16
  /* ---- ISO 8601 dayTimeDuration value model (op:duration batch,
16
17
  * lever 6 stage-2). Durations ride the STRING lexicon; these
17
18
  * helpers give arithmetic and comparisons a typed view. Strictly
@@ -2037,8 +2038,8 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2037
2038
  struct leptris_xpath_result* hi_r =
2038
2039
  evaluate_expr(ctx, ast->children[1]);
2039
2040
  if (!hi_r) { xpath_result_free(lo_r); return NULL; }
2040
- long lo = (long)xpath_to_number(lo_r);
2041
- long hi = (long)xpath_to_number(hi_r);
2041
+ double lod = xpath_to_number(lo_r);
2042
+ double hid = xpath_to_number(hi_r);
2042
2043
  xpath_result_free(lo_r);
2043
2044
  xpath_result_free(hi_r);
2044
2045
 
@@ -2046,12 +2047,25 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2046
2047
  if (!out) return NULL;
2047
2048
  out->owns_synthetic_text = 1;
2048
2049
  out->is_sequence = 1;
2049
- for (long v = lo; v <= hi && v - lo < 100000; v++) {
2050
- char buf[28];
2051
- int l = snprintf(buf, sizeof buf, "\x03N%ld", v);
2052
- XPathTextNode* tn = synth_text(buf, (size_t)l);
2053
- if (!tn) break;
2054
- xpath_nodeset_add(out, tn);
2050
+ /* XPath 2.0+: an empty (NaN) operand yields the empty
2051
+ * sequence — never cast NaN to long (UB: arm64 gives 0,
2052
+ * x86 gives INT32/64_MIN, so the "range" silently became
2053
+ * a 75/100000-item sequence; cbcl-codepoints-to-string-020).
2054
+ * Huge finite values clamp instead of UB-casting. */
2055
+ if (!isnan(lod) && !isnan(hid)) {
2056
+ long lo = lod > 9.0e18 ? LONG_MAX
2057
+ : lod < -9.0e18 ? LONG_MIN
2058
+ : (long)lod;
2059
+ long hi = hid > 9.0e18 ? LONG_MAX
2060
+ : hid < -9.0e18 ? LONG_MIN
2061
+ : (long)hid;
2062
+ for (long v = lo; v <= hi && v - lo < 100000; v++) {
2063
+ char buf[28];
2064
+ int l = snprintf(buf, sizeof buf, "\x03N%ld", v);
2065
+ XPathTextNode* tn = synth_text(buf, (size_t)l);
2066
+ if (!tn) break;
2067
+ xpath_nodeset_add(out, tn);
2068
+ }
2055
2069
  }
2056
2070
  struct leptris_xpath_result* result =
2057
2071
  xpath_result_new(XPATH_RESULT_NODESET);
@@ -763,7 +763,12 @@ static int de_item_equal(DeItem* a, DeItem* b) {
763
763
  if (a->kind != b->kind) return 0;
764
764
  switch (a->kind) {
765
765
  case 0: return de_node_equal(a->node, b->node);
766
- case 1: return a->num == b->num;
766
+ case 1: {
767
+ /* deep-equal is atomic-eq based: NaN compares equal to
768
+ * NaN (F&O 17.4.1 — unlike the eq operator). */
769
+ if (isnan(a->num) && isnan(b->num)) return 1;
770
+ return a->num == b->num;
771
+ }
767
772
  case 2: {
768
773
  const char* as = a->str ? a->str : (a->borrow ? a->borrow : "");
769
774
  const char* bs = b->str ? b->str : (b->borrow ? b->borrow : "");
@@ -843,17 +848,33 @@ static struct leptris_xpath_result* xpath_func_deep_equal(XPathContext* context,
843
848
  return NULL;
844
849
  }
845
850
 
846
- enum { CAP = 64 };
847
- DeItem ia[CAP], ib[CAP];
848
- size_t na = de_collect(a, ia, CAP);
849
- size_t nb = de_collect(b, ib, CAP);
851
+ /* Growable item arrays: corpus sequences run to tens of
852
+ * thousands of members (cbcl roundtrips over 65536 to 100000);
853
+ * a fixed cap silently compares a prefix. */
854
+ size_t capa = a->type == XPATH_RESULT_NODESET && a->value.nodeset_value
855
+ ? a->value.nodeset_value->count : 1;
856
+ size_t capb = b->type == XPATH_RESULT_NODESET && b->value.nodeset_value
857
+ ? b->value.nodeset_value->count : 1;
858
+ DeItem* ia = (DeItem*)malloc((capa ? capa : 1) * sizeof(DeItem));
859
+ DeItem* ib = (DeItem*)malloc((capb ? capb : 1) * sizeof(DeItem));
860
+ if (!ia || !ib) {
861
+ free(ia);
862
+ free(ib);
863
+ xpath_result_free(a);
864
+ xpath_result_free(b);
865
+ return NULL;
866
+ }
867
+ size_t na = de_collect(a, ia, capa);
868
+ size_t nb = de_collect(b, ib, capb);
850
869
 
851
870
  int equal = na == nb;
852
- for (size_t i = 0; equal && i < na && i < CAP; i++)
871
+ for (size_t i = 0; equal && i < na; i++)
853
872
  equal = de_item_equal(&ia[i], &ib[i]);
854
873
 
855
- for (size_t i = 0; i < na && i < CAP; i++) de_item_clear(&ia[i]);
856
- for (size_t i = 0; i < nb && i < CAP; i++) de_item_clear(&ib[i]);
874
+ for (size_t i = 0; i < na; i++) de_item_clear(&ia[i]);
875
+ for (size_t i = 0; i < nb; i++) de_item_clear(&ib[i]);
876
+ free(ia);
877
+ free(ib);
857
878
  xpath_result_free(a);
858
879
  xpath_result_free(b);
859
880
 
@@ -59,7 +59,20 @@ static void seq_push_str(struct leptris_xpath_result* seq, const char* s) {
59
59
 
60
60
  static void seq_push_num(struct leptris_xpath_result* seq, double d) {
61
61
  char* s = xpath_number_to_string(d);
62
- seq_push_str(seq, s ? s : "");
62
+ if (!s) return;
63
+ /* "\x03N" marks numeric members — same discipline as the
64
+ * sequence constructor: per-member type checks (deep-equal,
65
+ * instance of) must see a NUMBER, not a string that happens
66
+ * to look like one. String consumers strip the marker. */
67
+ size_t sl = strlen(s);
68
+ char* marked = (char*)malloc(sl + 3);
69
+ if (marked) {
70
+ marked[0] = '\x03';
71
+ marked[1] = 'N';
72
+ memcpy(marked + 2, s, sl + 1);
73
+ seq_push_str(seq, marked);
74
+ free(marked);
75
+ }
63
76
  free(s);
64
77
  }
65
78
 
@@ -71,6 +84,11 @@ static char* scalar_str(const struct leptris_xpath_result* r) {
71
84
  return leptris_strdup(r->value.string_value
72
85
  ? r->value.string_value : "");
73
86
  case XPATH_RESULT_NUMBER: {
87
+ /* int64 fidelity: an xs:integer-sourced number must
88
+ * string exactly — the double roundtrip prints
89
+ * 830993497117024304 as 8.30993e+17. */
90
+ if (r->is_int)
91
+ return xpath_int_to_string(r->int_value);
74
92
  char* s = xpath_number_to_string(r->value.number_value);
75
93
  return s ? s : leptris_strdup("");
76
94
  }
@@ -222,6 +240,13 @@ static struct leptris_xpath_result* fn_subsequence(XPathContext* ctx,
222
240
  if (sv) leptris_xpath_result_free(sv);
223
241
  struct leptris_xpath_result* out = seq_new();
224
242
  if (out) {
243
+ /* F&O: a negative $length yields the empty sequence
244
+ * (K-SeqSubsequenceFunc-5) — len < 0 is NOT the "to end"
245
+ * sentinel here. */
246
+ if (n >= 3 && ld < 0) {
247
+ free_items(items, cnt);
248
+ return out;
249
+ }
225
250
  long start = (long)sd;
226
251
  long len = (n >= 3) ? (ld != ld ? -1 : (long)ld) : -1;
227
252
  seq_from_items(out, items, cnt, start, len);
@@ -257,6 +282,10 @@ static struct leptris_xpath_result* fn_insert_before(XPathContext* ctx,
257
282
  size_t icnt;
258
283
  char** ins = collect_items(ctx, args, n, 2, &icnt);
259
284
  struct leptris_xpath_result* out = seq_new();
285
+ /* F&O: position < 1 inserts at 1; a position beyond the tail
286
+ * inserts at the end (K-SeqInsertBeforeFunc-5/10/14/15). */
287
+ if (at < 1) at = 1;
288
+ if (at > (long)cnt + 1) at = (long)cnt + 1;
260
289
  if (out && ins) {
261
290
  for (long k = 1; k <= (long)(cnt + icnt); k++) {
262
291
  if (k == at)