leptris 1.9.308.0 → 1.9.309.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ef02878e818136f9074d6bdeeb54a55db40d0d6287974a96c81979afe7ed525
4
- data.tar.gz: 23468d7fdb2db0aae806d90c0d4d312a9190fe2c69f28333ef202a125c504675
3
+ metadata.gz: 3cc7498cffb29da21d5b13691fd7ac8de3b4abdf89765699ecf79d03b186c1e1
4
+ data.tar.gz: 897683cbf8a8b510f733e51e3990e508d3d72ef45b1ef47985d8c601aecc68ff
5
5
  SHA512:
6
- metadata.gz: 25aa1f2055a658fbbafdf9031d765311c6a83de1bdbc61d4535a755ae103315e49c4cd99531faa5db5389c2c889175dde051747c86411e278999083c442d161f
7
- data.tar.gz: 1a5b9875f8fb26486d82e19ad6f70d0a76ce8cb5128e2b7e1fffa5f6c3ac284085199e067e6dc97aa6d2ecbc67909df490d055dd8728b55bf4e8e4179d4073b3
6
+ metadata.gz: 481e075161468c9de96a2e6cdb66cb904b003f007aeb53188e5ad1919b059265b176c87ddcc944d43d3ef093431733a1d8525630f879d0e102888e02e895d51e
7
+ data.tar.gz: 0b31da872763118c3bad5f0b6fa1398e758fc6732863dbb27b3f25858eed3f5824073248ebacdd51332ba7fa406a62144933cd9e7cf9cd6e72a36fe8a4411325
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [1.9.308.0] - 2026-10-05
2
+
3
+ ### Engine sync (libleptris 1.9.308)
4
+
5
+ - **QT3 #1182 tail (leptris#1540)** — the 13 lever cases:
6
+ fn:avg's numeric promotion ladder with int64-exact decimal
7
+ division, the O(xs:double) carrier mark, typeswitch case `$v as
8
+ TYPE` binding arms, fn:apply function conversion (zero-arity,
9
+ atomization, sequence members), and fn:sum's per-type canonical
10
+ duration zero. Full QT3 61/61.
11
+ - **Per-engine crates, step 1 (leptris#1541)** — the shared
12
+ leptris-descriptor ABI crate in a new Cargo workspace, with the
13
+ runtime layout gate (Rust size_of vs engine introspection +
14
+ build/walk round trip) and the build.rs stale-link cache fix.
15
+ Ruby-side impact: none (Rust-side surface; zero header deltas,
16
+ audit unchanged 369/368, suite 866/0).
17
+
1
18
  ## [1.9.307.0] - 2026-10-05
2
19
 
3
20
  ### 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.308"
11
+ LIBLEPTRIS_VERSION = "1.9.309"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.308.0"
4
+ VERSION = "1.9.309.0"
5
5
  end
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.308
4
+ - libleptris v1.9.309
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,40 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.309] - 2026-10-05
4
+
5
+ ### Fixed
6
+
7
+ - **XPath lexer: signed exponent literals truncated at the `E` —
8
+ `1E+1` evaluated to `2` and `3.567812E+3 eq 3567.812` was false.**
9
+ The number scanner consumed the exponent digits but not its sign,
10
+ so `1E+1` tokenized as `1E` followed by `+ 1`. The sign is now part
11
+ of the literal (also `5.2E-3` and friends).
12
+
13
+ ### Added
14
+
15
+ - **XQuery numeric literal typing (XQuery 3.1 §3.13.1) with
16
+ end-to-end type discrimination — 17 more QT3 cases adopted
17
+ (abs +5, ceiling +3, floor +3, round-half-to-even +6), 32 with the
18
+ same-batch string-to-codepoints and min/max gates.**
19
+ - Exponent literals are `xs:double`, decimals with a point are
20
+ `xs:decimal`, plain integers `xs:integer`; `1.1 instance of
21
+ xs:double` is now `false` per spec.
22
+ - `instance of` / `typeswitch` match labeled numeric results by
23
+ XDM derivation (`xs:integer` <: `xs:decimal`); `case $v as TYPE`
24
+ arms bind their variable inside the arm. Unlabeled numerics keep
25
+ the historical any-numeric match — no XPath 1.0 surface change.
26
+ - `fn:abs` preserves its argument's numeric type (integer/decimal/
27
+ float/double, float through float32); `fn:ceiling`/`fn:floor`
28
+ label float results; `fn:round-half-to-even` computes its scale
29
+ in double (no `int` truncation UB at 2^32) and guards huge
30
+ scales; `fn:min`/`fn:max` label results by promoted input type;
31
+ FLWOR bindings preserve float/decimal/double marks.
32
+ - Vendor corpus: `fn/string-to-codepoints.xml` (+28), `fn/min`
33
+ and `fn/max` +1 each.
34
+ - Remaining pinned QT3 reds are documented by lever in the test
35
+ harness (HOF node-carrier, map iteration order, typed grouping).
36
+
37
+
3
38
  ## [1.9.308] - 2026-10-05
4
39
 
5
40
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.308
6
+ VERSION 1.9.309
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -973,6 +973,10 @@ static struct leptris_xpath_result* evaluate_literal(XPathContext* ctx,
973
973
  struct leptris_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER);
974
974
  if (!result) return NULL;
975
975
  result->value.number_value = ast->number_value;
976
+ /* XQuery literal typing from the lexical (the parser
977
+ * stashed "xs:decimal"/"xs:double" in value; integers
978
+ * stay unlabeled). */
979
+ result->atomic_type = ast->value;
976
980
  return result;
977
981
  }
978
982
 
@@ -777,6 +777,7 @@ int xpath_result_matches_type(struct leptris_xpath_result* v,
777
777
  ? ((XPathTextNode*)n)->content : NULL;
778
778
  int is_num_member = mc && mc[0] == '\x03' &&
779
779
  (mc[1] == 'N' || mc[1] == 'O' ||
780
+ mc[1] == 'D' ||
780
781
  (mc[1] == 'F' &&
781
782
  !((mc[2] == 'N' && mc[3] == '\x02') ||
782
783
  mc[2] == 'R')));
@@ -809,7 +810,32 @@ int xpath_result_matches_type(struct leptris_xpath_result* v,
809
810
  }
810
811
  if (is_string_ty) return v->type == XPATH_RESULT_STRING;
811
812
  if (is_bool_ty) return v->type == XPATH_RESULT_BOOLEAN;
812
- if (is_num_ty) return v->type == XPATH_RESULT_NUMBER;
813
+ if (is_num_ty) {
814
+ if (v->type != XPATH_RESULT_NUMBER) return 0;
815
+ /* A labeled numeric result discriminates its subtype for
816
+ * typeswitch / instance of (fn-min/max-10). Unlabeled
817
+ * numbers keep the historical any-numeric match. XDM
818
+ * derivation: xs:integer matches a xs:decimal test. */
819
+ if (v->atomic_type &&
820
+ strncmp(v->atomic_type, "xs:", 3) == 0) {
821
+ const char* at = v->atomic_type + 3;
822
+ int at_num =
823
+ strcmp(at, "integer") == 0 ||
824
+ strcmp(at, "decimal") == 0 ||
825
+ strcmp(at, "float") == 0 ||
826
+ strcmp(at, "double") == 0;
827
+ int b_num =
828
+ strcmp(base, "xs:integer") == 0 ||
829
+ strcmp(base, "xs:decimal") == 0 ||
830
+ strcmp(base, "xs:float") == 0 ||
831
+ strcmp(base, "xs:double") == 0;
832
+ if (at_num && b_num)
833
+ return strcmp(at, base + 3) == 0 ||
834
+ (strcmp(base, "xs:decimal") == 0 &&
835
+ strcmp(at, "integer") == 0);
836
+ }
837
+ return 1;
838
+ }
813
839
  if (strcmp(base, "item()") == 0) return 1;
814
840
  return 0; /* node kinds: a scalar is not a node */
815
841
  }
@@ -2887,6 +2913,7 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2887
2913
  int is_num_member = mc && mc[0] == '\x03' &&
2888
2914
  (mc[1] == 'N' ||
2889
2915
  mc[1] == 'O' ||
2916
+ mc[1] == 'D' ||
2890
2917
  (mc[1] == 'F' &&
2891
2918
  !((mc[2] == 'N' &&
2892
2919
  mc[3] == '\x02') ||
@@ -2956,6 +2983,28 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2956
2983
  }
2957
2984
  else if (is_num_ty) {
2958
2985
  m = v->type == XPATH_RESULT_NUMBER;
2986
+ /* A labeled numeric result discriminates its
2987
+ * subtype (K2-ABSFunc/fn-abs-1: abs preserves
2988
+ * the argument type); unlabeled numbers keep
2989
+ * the any-numeric match. */
2990
+ if (m && v->atomic_type &&
2991
+ strncmp(v->atomic_type, "xs:", 3) == 0) {
2992
+ const char* at = v->atomic_type + 3;
2993
+ int at_num =
2994
+ strcmp(at, "integer") == 0 ||
2995
+ strcmp(at, "decimal") == 0 ||
2996
+ strcmp(at, "float") == 0 ||
2997
+ strcmp(at, "double") == 0;
2998
+ int b_num =
2999
+ strcmp(base, "xs:integer") == 0 ||
3000
+ strcmp(base, "xs:decimal") == 0 ||
3001
+ strcmp(base, "xs:float") == 0 ||
3002
+ strcmp(base, "xs:double") == 0;
3003
+ if (at_num && b_num)
3004
+ m = strcmp(at, base + 3) == 0 ||
3005
+ (strcmp(base, "xs:decimal") == 0 &&
3006
+ strcmp(at, "integer") == 0);
3007
+ }
2959
3008
  if (m && is_int_ty)
2960
3009
  m = v->is_int ||
2961
3010
  (v->value.number_value ==
@@ -2544,9 +2544,20 @@ static struct leptris_xpath_result* xpath_func_floor(XPathContext* context,
2544
2544
 
2545
2545
  struct leptris_xpath_result* arg_result = xpath_evaluate(context, args[0]);
2546
2546
  if (!arg_result) return NULL;
2547
+ /* XQuery surface: floor(()) is the empty sequence
2548
+ * (K-FloorFunc-3); XPath 1.0 coercion gives NaN. */
2549
+ if (context->xquery_spelling &&
2550
+ arg_result->type == XPATH_RESULT_NODESET &&
2551
+ (!arg_result->value.nodeset_value ||
2552
+ xpath_nodeset_count(arg_result->value.nodeset_value) == 0)) {
2553
+ xpath_result_free(arg_result);
2554
+ return xpath_result_new(XPATH_RESULT_NODESET);
2555
+ }
2547
2556
 
2548
2557
  int iv_int = arg_result->is_int;
2549
2558
  long long iv = arg_result->int_value;
2559
+ int fl = xpath_result_is_float(arg_result);
2560
+ int xq = arg_result->xq_spelling;
2550
2561
  double num = result_to_number(arg_result);
2551
2562
  xpath_result_free(arg_result);
2552
2563
 
@@ -2560,7 +2571,14 @@ static struct leptris_xpath_result* xpath_func_floor(XPathContext* context,
2560
2571
  result->int_value = iv;
2561
2572
  return result;
2562
2573
  }
2574
+ if (fl) {
2575
+ result->value.number_value = (double)(float)floor(num);
2576
+ result->atomic_type = "xs:float";
2577
+ result->xq_spelling = xq;
2578
+ return result;
2579
+ }
2563
2580
  result->value.number_value = floor(num);
2581
+ result->xq_spelling = xq;
2564
2582
  return result;
2565
2583
  }
2566
2584
 
@@ -2577,9 +2595,20 @@ static struct leptris_xpath_result* xpath_func_ceiling(XPathContext* context,
2577
2595
 
2578
2596
  struct leptris_xpath_result* arg_result = xpath_evaluate(context, args[0]);
2579
2597
  if (!arg_result) return NULL;
2598
+ /* XQuery surface: ceiling(()) is the empty sequence
2599
+ * (K-CeilingFunc-3); XPath 1.0 coercion gives NaN. */
2600
+ if (context->xquery_spelling &&
2601
+ arg_result->type == XPATH_RESULT_NODESET &&
2602
+ (!arg_result->value.nodeset_value ||
2603
+ xpath_nodeset_count(arg_result->value.nodeset_value) == 0)) {
2604
+ xpath_result_free(arg_result);
2605
+ return xpath_result_new(XPATH_RESULT_NODESET);
2606
+ }
2580
2607
 
2581
2608
  int iv_int = arg_result->is_int;
2582
2609
  long long iv = arg_result->int_value;
2610
+ int fl = xpath_result_is_float(arg_result);
2611
+ int xq = arg_result->xq_spelling;
2583
2612
  double num = result_to_number(arg_result);
2584
2613
  xpath_result_free(arg_result);
2585
2614
 
@@ -2591,7 +2620,16 @@ static struct leptris_xpath_result* xpath_func_ceiling(XPathContext* context,
2591
2620
  result->int_value = iv;
2592
2621
  return result;
2593
2622
  }
2623
+ /* F&O: the result of an xs:float argument is xs:float —
2624
+ * float32-computed and float-spelled (fn-ceilingflt1args). */
2625
+ if (fl) {
2626
+ result->value.number_value = (double)(float)ceil(num);
2627
+ result->atomic_type = "xs:float";
2628
+ result->xq_spelling = xq;
2629
+ return result;
2630
+ }
2594
2631
  result->value.number_value = ceil(num);
2632
+ result->xq_spelling = xq;
2595
2633
  return result;
2596
2634
  }
2597
2635
 
@@ -1066,7 +1066,7 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
1066
1066
  return out;
1067
1067
  }
1068
1068
  }
1069
- } else if (!fallthrough && any_flt) {
1069
+ } else if (!fallthrough && any_flt && !any_dbl) {
1070
1070
  /* F&O: when xs:float is the common type, EVERY item
1071
1071
  * converts to xs:float before summing (Saxon computes
1072
1072
  * the float32-converted addends in double, then the
@@ -1096,7 +1096,10 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
1096
1096
  } else if (!fallthrough && any_dbl) {
1097
1097
  /* xs:double in the mix: label the result so typeswitch
1098
1098
  * sees it (cbcl-avg-008); value stays the generic
1099
- * double accumulation. */
1099
+ * double accumulation. NOTE the guard order above: the
1100
+ * float branch is gated on !any_dbl, so the ladder
1101
+ * checks widest-first — float+double routes HERE, not
1102
+ * to the float path. */
1100
1103
  out->atomic_type = "xs:double";
1101
1104
  }
1102
1105
  free(kinds);
@@ -1149,6 +1152,27 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
1149
1152
  * fn-*-flt1args battery). */
1150
1153
  if (all_float && out->type == XPATH_RESULT_NUMBER)
1151
1154
  out->atomic_type = "xs:float";
1155
+ /* fn:min/max label the result with the promoted input type so
1156
+ * typeswitch / instance of discriminate the numeric subtype
1157
+ * (fn-min/max-10: integer -> decimal -> float -> double as the
1158
+ * prefix grows). Widest-first, same ladder as fn:avg. */
1159
+ if (which >= 2 && kinds && out->type == XPATH_RESULT_NUMBER &&
1160
+ !out->atomic_type) {
1161
+ int a_dbl = 0, a_flt = 0, a_dec = 0, a_int = 1;
1162
+ for (size_t k = 0; k < cnt; k++) {
1163
+ switch (kinds[k]) {
1164
+ case AVG_KIND_INT: break;
1165
+ case AVG_KIND_DBL: a_dbl = 1; a_int = 0; break;
1166
+ case AVG_KIND_FLT: a_flt = 1; a_int = 0; break;
1167
+ case AVG_KIND_DEC: a_dec = 1; a_int = 0; break;
1168
+ default: a_int = 0; break;
1169
+ }
1170
+ }
1171
+ if (a_int) out->atomic_type = "xs:integer";
1172
+ else if (a_dbl) out->atomic_type = "xs:double";
1173
+ else if (a_flt) out->atomic_type = "xs:float";
1174
+ else if (a_dec) out->atomic_type = "xs:decimal";
1175
+ }
1152
1176
  free_items(items, cnt);
1153
1177
  free(kinds);
1154
1178
  return out;
@@ -1198,10 +1222,33 @@ static struct leptris_xpath_result* fn_math1(XPathContext* ctx,
1198
1222
  XPathASTNode** args, size_t n, double (*f)(double)) {
1199
1223
  struct leptris_xpath_result* r = xpath_evaluate(ctx, args[0]);
1200
1224
  if (!r) return NULL;
1225
+ /* ceiling(()) is the empty sequence on the XQuery surface
1226
+ * (K-CeilingFunc-3); XPath 1.0 coercion gives NaN. */
1227
+ if (ctx->xquery_spelling &&
1228
+ r->type == XPATH_RESULT_NODESET &&
1229
+ (!r->value.nodeset_value ||
1230
+ r->value.nodeset_value->count == 0)) {
1231
+ leptris_xpath_result_free(r);
1232
+ return xpath_result_new(XPATH_RESULT_NODESET);
1233
+ }
1234
+ int fl = xpath_result_is_float(r);
1235
+ int xq = r->xq_spelling;
1201
1236
  double d = leptris_xpath_result_number(r);
1202
1237
  leptris_xpath_result_free(r);
1203
1238
  struct leptris_xpath_result* out = xpath_result_new(XPATH_RESULT_NUMBER);
1204
- if (out && n) out->value.number_value = f(d);
1239
+ if (out && n) {
1240
+ /* F&O: ceiling/floor/round of an xs:float stays xs:float —
1241
+ * compute in float32 and spell at float precision
1242
+ * (fn-ceilingflt1args: the double spelling of the same
1243
+ * value differs). */
1244
+ if (fl) {
1245
+ out->value.number_value = (double)(float)f(d);
1246
+ out->atomic_type = "xs:float";
1247
+ } else {
1248
+ out->value.number_value = f(d);
1249
+ }
1250
+ out->xq_spelling = xq;
1251
+ }
1205
1252
  return out;
1206
1253
  }
1207
1254
 
@@ -1272,20 +1319,61 @@ static struct leptris_xpath_result* fn_abs(XPathContext* ctx,
1272
1319
  return xpath_result_new(XPATH_RESULT_NODESET);
1273
1320
  }
1274
1321
  int fl = xpath_result_is_float(r);
1322
+ int r_dec = r->atomic_type &&
1323
+ strcmp(r->atomic_type, "xs:decimal") == 0;
1324
+ int r_dbl = 0;
1325
+ /* A FOR-bound argument is a single-item NODESET carrying the
1326
+ * typed-scalar MARK (the scalar atomic_type is long gone):
1327
+ * read D/O back off the mark (fn-abs-1). */
1328
+ if (r->type == XPATH_RESULT_NODESET && r->value.nodeset_value &&
1329
+ r->value.nodeset_value->count == 1) {
1330
+ void* nd = r->value.nodeset_value->nodes[0];
1331
+ if ((int)XPATH_NODE_TYPE(nd) == LEPTRIS_NODE_TEXT) {
1332
+ const char* mc = ((XPathTextNode*)nd)->content;
1333
+ if (mc && mc[0] == '\x03') {
1334
+ if (mc[1] == 'D') r_dec = 1;
1335
+ else if (mc[1] == 'O') r_dbl = 1;
1336
+ }
1337
+ }
1338
+ }
1339
+ int xq = r->xq_spelling;
1275
1340
  int iv_int = r->is_int;
1276
1341
  long long iv = r->int_value;
1277
1342
  double d = leptris_xpath_result_number(r);
1343
+ /* A bare-N synthetic (FLWOR-bound plain integer, fn-abs-1's
1344
+ * walk over (1, xs:decimal(2), ...)) loses the is_int bit:
1345
+ * an integer lexical on an UNLABELED number is an integer. */
1346
+ if (!iv_int && !fl && !r_dec && !r->atomic_type && !r_dbl) {
1347
+ char* ps = xpath_number_to_string_xq_typed(d, 0);
1348
+ if (ps && num_int_lexical(ps)) {
1349
+ iv_int = 1;
1350
+ iv = (long long)d;
1351
+ }
1352
+ LEPTRIS_FREE(ps);
1353
+ }
1278
1354
  leptris_xpath_result_free(r);
1279
1355
  struct leptris_xpath_result* out = xpath_result_new(XPATH_RESULT_NUMBER);
1280
1356
  if (out && n) {
1357
+ out->xq_spelling = xq;
1281
1358
  if (iv_int) {
1282
1359
  out->value.number_value = fabs(d);
1283
1360
  out->is_int = 1;
1284
1361
  out->int_value = iv < 0 ? -iv : iv;
1285
1362
  } else {
1363
+ if (fl) d = (double)(float)fabs(d);
1286
1364
  out->value.number_value = fabs(d);
1287
1365
  }
1288
- if (fl) out->atomic_type = "xs:float";
1366
+ /* F&O: abs preserves the argument type (fn-abs-1 walks
1367
+ * integer/decimal/float/double through instance-of). */
1368
+ if (fl) {
1369
+ out->atomic_type = "xs:float";
1370
+ } else if (r_dec) {
1371
+ out->atomic_type = "xs:decimal";
1372
+ } else if (!iv_int) {
1373
+ /* Unlabeled non-integers are doubles (XQuery literal
1374
+ * typing also routes exponents here). */
1375
+ out->atomic_type = r_dbl ? "xs:double" : "xs:double";
1376
+ }
1289
1377
  }
1290
1378
  return out;
1291
1379
  }
@@ -1305,26 +1393,86 @@ static struct leptris_xpath_result* fn_round_half_even(XPathContext* ctx,
1305
1393
  }
1306
1394
  double v = leptris_xpath_result_number(r);
1307
1395
  int fl = xpath_result_is_float(r);
1396
+ int xq = r->xq_spelling;
1308
1397
  leptris_xpath_result_free(r);
1309
- int prec = 0;
1398
+ double prec = 0;
1310
1399
  if (n >= 2) {
1311
1400
  struct leptris_xpath_result* p = xpath_evaluate(ctx, args[1]);
1312
- prec = p ? (int)leptris_xpath_result_number(p) : 0;
1401
+ /* The scale can exceed int range (cbcl-round-half-to-even:
1402
+ * 4294967296) — keep it in double throughout; an (int) cast
1403
+ * is UB past INT_MAX. */
1404
+ prec = p ? leptris_xpath_result_number(p) : 0;
1313
1405
  if (p) leptris_xpath_result_free(p);
1314
1406
  }
1315
- double scale = pow(10.0, prec);
1316
- double x = v * scale;
1407
+ if (prec > 0) {
1408
+ /* Positive scale: round(x*10^p)/10^p, ties to even on the
1409
+ * scaled integer via fmod (no long-long truncation). A
1410
+ * scale beyond the double's precision leaves the value
1411
+ * unchanged (pow overflows to INF — cbcl-round-half-to-even
1412
+ * rounds at scale 2^32). */
1413
+ double scale = pow(10.0, prec);
1414
+ if (!(scale < INFINITY)) {
1415
+ struct leptris_xpath_result* out =
1416
+ xpath_result_new(XPATH_RESULT_NUMBER);
1417
+ if (out) {
1418
+ out->value.number_value = v;
1419
+ if (fl) out->atomic_type = "xs:float";
1420
+ out->xq_spelling = xq;
1421
+ }
1422
+ return out;
1423
+ }
1424
+ double x = v * scale;
1425
+ double f = floor(x);
1426
+ double diff = x - f;
1427
+ double rounded;
1428
+ if (diff == 0.5)
1429
+ rounded = (fmod(f, 2.0) == 0) ? f : f + 1;
1430
+ else
1431
+ rounded = floor(x + 0.5);
1432
+ struct leptris_xpath_result* out =
1433
+ xpath_result_new(XPATH_RESULT_NUMBER);
1434
+ if (out) {
1435
+ out->value.number_value = rounded / scale;
1436
+ if (fl) out->atomic_type = "xs:float";
1437
+ out->xq_spelling = xq;
1438
+ }
1439
+ return out;
1440
+ }
1441
+ if (prec < 0) {
1442
+ /* Negative scale: round to a multiple of 10^-prec, ties to
1443
+ * even on the quotient. */
1444
+ double scale = pow(10.0, -prec);
1445
+ double x = v / scale;
1446
+ double f = floor(x);
1447
+ double diff = x - f;
1448
+ double rounded;
1449
+ if (diff == 0.5)
1450
+ rounded = (fmod(f, 2.0) == 0) ? f : f + 1;
1451
+ else
1452
+ rounded = floor(x + 0.5);
1453
+ struct leptris_xpath_result* out =
1454
+ xpath_result_new(XPATH_RESULT_NUMBER);
1455
+ if (out) {
1456
+ out->value.number_value = rounded * scale;
1457
+ if (fl) out->atomic_type = "xs:float";
1458
+ out->xq_spelling = xq;
1459
+ }
1460
+ return out;
1461
+ }
1462
+ /* prec == 0: plain round-half-to-even. */
1463
+ double x = v;
1317
1464
  double f = floor(x);
1318
1465
  double diff = x - f;
1319
1466
  double rounded;
1320
1467
  if (diff == 0.5)
1321
- rounded = (((long)f) % 2 == 0) ? f : f + 1; /* tie -> even */
1468
+ rounded = (fmod(f, 2.0) == 0) ? f : f + 1; /* tie -> even */
1322
1469
  else
1323
1470
  rounded = floor(x + 0.5);
1324
1471
  struct leptris_xpath_result* out = xpath_result_new(XPATH_RESULT_NUMBER);
1325
1472
  if (out) {
1326
- out->value.number_value = rounded / scale;
1473
+ out->value.number_value = rounded;
1327
1474
  if (fl) out->atomic_type = "xs:float";
1475
+ out->xq_spelling = xq;
1328
1476
  }
1329
1477
  return out;
1330
1478
  }
@@ -546,6 +546,15 @@ parse_number:
546
546
  if (isdigit((unsigned char)*probe)) {
547
547
  lexer->pos++;
548
548
  lexer->column++;
549
+ /* The SIGN is part of the literal (1E+1, 5.2E-3):
550
+ * without consuming it the token truncates at 'E'
551
+ * and the parser reads "1E + 1" as arithmetic
552
+ * (1E+1 == 2). */
553
+ if (lexer->pos < lexer->end &&
554
+ (*lexer->pos == '+' || *lexer->pos == '-')) {
555
+ lexer->pos++;
556
+ lexer->column++;
557
+ }
549
558
  while (lexer->pos < lexer->end &&
550
559
  isdigit((unsigned char)*lexer->pos)) {
551
560
  lexer->pos++;
@@ -2815,6 +2815,16 @@ static XPathASTNode* parse_primary_expr(XPathParser* parser) {
2815
2815
  char* num_str = token_to_string(tok);
2816
2816
  if (num_str) {
2817
2817
  node->number_value = strtod(num_str, NULL);
2818
+ /* XQuery literal typing (2.5.1): a '.' (no exponent) is
2819
+ * xs:decimal, an exponent is xs:double, else
2820
+ * xs:integer. The evaluator labels the result from
2821
+ * this lexical kind (value is unused for NUMBER
2822
+ * nodes); integers stay NULL so the hot path stays
2823
+ * allocation-free. */
2824
+ if (strchr(num_str, 'e') || strchr(num_str, 'E'))
2825
+ node->value = leptris_strdup("xs:double");
2826
+ else if (strchr(num_str, '.'))
2827
+ node->value = leptris_strdup("xs:decimal");
2818
2828
  LEPTRIS_FREE(num_str);
2819
2829
  }
2820
2830
  advance_token(parser);
@@ -2013,7 +2013,22 @@ static int xq_bind(XPathContext* ctx, const char* name,
2013
2013
  char* marked = (char*)malloc(sl + 3);
2014
2014
  if (marked) {
2015
2015
  marked[0] = '\x03';
2016
- marked[1] = 'N';
2016
+ /* Preserve the typed-scalar label through the
2017
+ * binding (same vocabulary as the sequence
2018
+ * fold): fn:min/max over `let $x := (...)`
2019
+ * classifies its items from these marks — a
2020
+ * bare 'N' demotes xs:double-spelled-"2" to
2021
+ * integer (fn-min/max-10). */
2022
+ marked[1] =
2023
+ (v->atomic_type &&
2024
+ strcmp(v->atomic_type, "xs:float") == 0)
2025
+ ? 'F'
2026
+ : (v->atomic_type &&
2027
+ strcmp(v->atomic_type, "xs:decimal") == 0)
2028
+ ? 'D'
2029
+ : (v->atomic_type &&
2030
+ strcmp(v->atomic_type, "xs:double") == 0)
2031
+ ? 'O' : 'N';
2017
2032
  if (sl) memcpy(marked + 2, s, sl);
2018
2033
  marked[2 + sl] = 0;
2019
2034
  XPathTextNode* tn =
@@ -704,6 +704,10 @@ void run_test_set(const char* set_path,
704
704
  leptris_document_free(ts);
705
705
  }
706
706
 
707
+ TEST(Qt3Subset, FnStringToCodepoints) {
708
+ run_test_set("fn/string-to-codepoints.xml", {}, 28);
709
+ }
710
+
707
711
  TEST(Qt3Subset, FnSubstring) {
708
712
  run_test_set("fn/substring.xml",
709
713
  {{"concepts", "fn/substring/concepts.xml"}},
@@ -941,45 +945,30 @@ TEST(Qt3Subset, FnCount) {
941
945
  * assertions, mixed-type aggregates, HOF zero-arity apply) —
942
946
  * see the batch notes in #1182. */
943
947
  TEST(Qt3Subset, FnAbs) {
944
- run_test_set("fn/abs.xml", {}, 38, {},
945
- {"K2-ABSFunc-27",
946
- "K2-ABSFunc-28",
947
- "K2-ABSFunc-29",
948
- "K2-ABSFunc-30",
949
- "fn-abs-1"});
948
+ run_test_set("fn/abs.xml", {}, 43, {},
949
+ {});
950
950
  }
951
951
  TEST(Qt3Subset, FnAvg) {
952
952
  run_test_set("fn/avg.xml", {}, 182, {}, {});
953
953
  }
954
954
  TEST(Qt3Subset, FnCeiling) {
955
- run_test_set("fn/ceiling.xml", {}, 39, {},
956
- {"K-CeilingFunc-3",
957
- "fn-ceilingflt1args-1",
958
- "fn-ceilingflt1args-3"});
955
+ run_test_set("fn/ceiling.xml", {}, 42, {},
956
+ {});
959
957
  }
960
958
  TEST(Qt3Subset, FnFloor) {
961
- run_test_set("fn/floor.xml", {}, 39, {},
962
- {"K-FloorFunc-3",
963
- "fn-floorflt1args-1",
964
- "fn-floorflt1args-3"});
959
+ run_test_set("fn/floor.xml", {}, 42, {},
960
+ {});
965
961
  }
966
962
  TEST(Qt3Subset, FnMax) {
967
- run_test_set("fn/max.xml", {}, 176, {},
968
- {"fn-max-10"});
963
+ run_test_set("fn/max.xml", {}, 177, {}, {});
969
964
  }
970
965
  TEST(Qt3Subset, FnMin) {
971
- run_test_set("fn/min.xml", {}, 175, {},
972
- {"fn-min-10"});
966
+ run_test_set("fn/min.xml", {}, 176, {}, {});
973
967
  }
974
968
  TEST(Qt3Subset, FnRound) { run_test_set("fn/round.xml", {}, 220); }
975
969
  TEST(Qt3Subset, FnRoundHalfToEven) {
976
- run_test_set("fn/round-half-to-even.xml", {}, 76, {},
977
- {"K2-RoundEvenFunc-6",
978
- "K2-RoundEvenFunc-7",
979
- "cbcl-round-half-to-even-001",
980
- "cbcl-round-half-to-even-010",
981
- "cbcl-round-half-to-even-011",
982
- "cbcl-round-half-to-even-012"});
970
+ run_test_set("fn/round-half-to-even.xml", {}, 82, {},
971
+ {});
983
972
  }
984
973
  TEST(Qt3Subset, FnSum) {
985
974
  run_test_set("fn/sum.xml", {}, 210, {}, {});
@@ -989,35 +978,52 @@ TEST(Qt3Subset, FnApply) {
989
978
  }
990
979
  TEST(Qt3Subset, FnFilter) { run_test_set("fn/filter.xml", {}, 1); }
991
980
  TEST(Qt3Subset, FnForEach) {
981
+ /* Re-pinned pending the HOF node-carrier lever: fold/for-each
982
+ * callbacks receive node arguments as synthesized strings, so
983
+ * node-identity shapes (intersect, union, deep-equal on
984
+ * elements) disagree. Re-adopt when the HOF call paths carry
985
+ * nodes by reference. */
992
986
  run_test_set("fn/for-each.xml", {}, 0, {},
993
987
  {"for-each-010"});
994
988
  }
995
989
  TEST(Qt3Subset, FnForEachPair) {
990
+ /* Re-pinned pending the HOF node-carrier lever: fold/for-each
991
+ * callbacks receive node arguments as synthesized strings, so
992
+ * node-identity shapes (intersect, union, deep-equal on
993
+ * elements) disagree. Re-adopt when the HOF call paths carry
994
+ * nodes by reference. */
996
995
  run_test_set("fn/for-each-pair.xml", {}, 3, {},
997
996
  {"fn-for-each-pair-024",
998
997
  "fn-for-each-pair-026",
999
998
  "fn-for-each-pair-027"});
1000
999
  }
1001
1000
  TEST(Qt3Subset, FnFoldLeft) {
1001
+ /* fold-left-016 re-pinned: it folds over a MAP and agrees only
1002
+ * when the entry iteration order is favorable — nondeterministic
1003
+ * across runs (the map-order lever). Re-adopt when the fold
1004
+ * orders entries deterministically. */
1005
+ /* Re-pinned pending the HOF node-carrier lever: fold/for-each
1006
+ * callbacks receive node arguments as synthesized strings, so
1007
+ * node-identity shapes (intersect, union, deep-equal on
1008
+ * elements) disagree. Re-adopt when the HOF call paths carry
1009
+ * nodes by reference. */
1002
1010
  run_test_set("fn/fold-left.xml", {}, 7, {},
1003
- {"fold-left-009",
1004
- "fold-left-016",
1005
- "fold-left-019",
1006
- "fold-left-020",
1007
- "fold-left-021",
1008
- "fold-left-101",
1009
- "fold-left-102",
1010
- "fold-left-103",
1011
+ {"fold-left-009", "fold-left-016",
1012
+ "fold-left-019", "fold-left-020",
1013
+ "fold-left-021", "fold-left-101",
1014
+ "fold-left-102", "fold-left-103",
1011
1015
  "fold-left-104"});
1012
1016
  }
1013
1017
  TEST(Qt3Subset, FnFoldRight) {
1018
+ /* Re-pinned pending the HOF node-carrier lever: fold/for-each
1019
+ * callbacks receive node arguments as synthesized strings, so
1020
+ * node-identity shapes (intersect, union, deep-equal on
1021
+ * elements) disagree. Re-adopt when the HOF call paths carry
1022
+ * nodes by reference. */
1014
1023
  run_test_set("fn/fold-right.xml", {}, 9, {},
1015
- {"fold-right-013",
1016
- "fold-right-020",
1017
- "fold-right-101",
1018
- "fold-right-102",
1019
- "fold-right-103",
1020
- "fold-right-104"});
1024
+ {"fold-right-013", "fold-right-020",
1025
+ "fold-right-101", "fold-right-102",
1026
+ "fold-right-103", "fold-right-104"});
1021
1027
  }
1022
1028
 
1023
1029
  /* The regex trio (matches/replace/tokenize/analyze-string) compiles
@@ -1233,16 +1239,12 @@ TEST(Qt3Subset, FnStringJoin) {
1233
1239
 
1234
1240
  TEST(Qt3Subset, FnIndexOf) {
1235
1241
  run_test_set("fn/index-of.xml", {}, 44, {},
1236
- {
1237
- });
1242
+ {});
1238
1243
  }
1239
1244
 
1240
1245
  TEST(Qt3Subset, FnInsertBefore) {
1241
- run_test_set("fn/insert-before.xml", {}, 18, {},
1242
- {
1243
- "K-SeqInsertBeforeFunc-16", /* lazy error() semantics */
1244
-
1245
- });
1246
+ run_test_set("fn/insert-before.xml", {}, 19, {},
1247
+ {});
1246
1248
  }
1247
1249
 
1248
1250
  TEST(Qt3Subset, FnRemove) {
@@ -1250,11 +1252,13 @@ TEST(Qt3Subset, FnRemove) {
1250
1252
  }
1251
1253
 
1252
1254
  TEST(Qt3Subset, FnReverse) {
1255
+ /* Re-pinned pending the HOF node-carrier lever: fold/for-each
1256
+ * callbacks receive node arguments as synthesized strings, so
1257
+ * node-identity shapes (intersect, union, deep-equal on
1258
+ * elements) disagree. Re-adopt when the HOF call paths carry
1259
+ * nodes by reference. */
1253
1260
  run_test_set("fn/reverse.xml", {}, 60, {},
1254
- {
1255
- "K2-SeqReverseFunc-1", /* assert-xml over direct ctors */
1256
-
1257
- });
1261
+ {"K2-SeqReverseFunc-1"});
1258
1262
  }
1259
1263
 
1260
1264
  TEST(Qt3Subset, FnSubsequence) {
@@ -1286,12 +1290,14 @@ TEST(Qt3Subset, FnDistinctValues) {
1286
1290
  * evaluate_predicate_for_node AND the single-item base path in
1287
1291
  * the XPATH_AST_PREDICATE handler — xs:dayTimeDuration("PT0S")[$p]
1288
1292
  * takes the latter). */
1293
+ /* Re-pinned pending the HOF node-carrier lever: fold/for-each
1294
+ * callbacks receive node arguments as synthesized strings, so
1295
+ * node-identity shapes (intersect, union, deep-equal on
1296
+ * elements) disagree. Re-adopt when the HOF call paths carry
1297
+ * nodes by reference. */
1289
1298
  run_test_set("fn/distinct-values.xml", {}, 99, {},
1290
- {
1291
- "cbcl-distinct-values-002",
1292
- "cbcl-distinct-values-002b",
1293
-
1294
- });
1299
+ {"cbcl-distinct-values-002",
1300
+ "cbcl-distinct-values-002b"});
1295
1301
  }
1296
1302
 
1297
1303
  TEST(Qt3Subset, FnDeepEqual) {
@@ -215,7 +215,11 @@ TEST(XQueryCore, InstanceOfIntegerRequiresIntegrality) {
215
215
  EXPECT_EQ(seq_string(doc, "fn:abs(1.1) instance of xs:integer"), "false");
216
216
  EXPECT_EQ(seq_string(doc, "fn:abs(-3) instance of xs:integer"), "true");
217
217
  EXPECT_EQ(seq_string(doc, "xs:byte(4) instance of xs:integer"), "true");
218
- EXPECT_EQ(seq_string(doc, "1.1 instance of xs:double"), "true");
218
+ /* XQuery literal typing (3.13.1): a '.' literal is xs:decimal
219
+ * (K2-ABSFunc-31: fn:abs(1.1) instance of xs:decimal) — NOT
220
+ * xs:double. */
221
+ EXPECT_EQ(seq_string(doc, "1.1 instance of xs:double"), "false");
222
+ EXPECT_EQ(seq_string(doc, "1.1e0 instance of xs:double"), "true");
219
223
  EXPECT_EQ(seq_string(doc, "1.5 instance of xs:decimal"), "true");
220
224
  leptris_document_free(doc);
221
225
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.308",
4
+ "version": "1.9.309",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.308.0
4
+ version: 1.9.309.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-05 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi