leptris 1.9.216.0-arm-linux → 1.9.221.0-arm-linux

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: 9a2622f48a888b3a14f647caa24a19bc9a622dd843f62b55300523c7a658b7e7
4
- data.tar.gz: '059f9bc0fe353103f5f9a12e540dfd3a17f9a700db9201253525d265a413e9db'
3
+ metadata.gz: acdcd23826e0678ede2a367350b1871a3ccc00d8cd77b5d6faabaf05c6f7039c
4
+ data.tar.gz: f3763c24dcc489a498e74e71746430029394e699f1be50c94e7e191719bdff0a
5
5
  SHA512:
6
- metadata.gz: 92137716049c65c944745eb9772e1731984bbb20c0a4bfb7e4e04cc5bd5fd33c9208c79392a02c6713bfdbaf71498beb63b780b5365cb4b665d37c353291abf8
7
- data.tar.gz: f1360b1b0ef61c5c2380bd8a8aee10f5fc90d0e3b2416ffde0091a6e83fa986ebf8dd244d9331e02ccd96c5475674c4b3ddaac81d41b5ea50880493c411493ee
6
+ metadata.gz: 4e582cff85f5f651b3edea67acb0ad59d83f38a1b767c8b3c86b06a9006224db00cb9a3cb438947c01d5625877e5b871bb2835e55776a45bd530d79dd4bf7f50
7
+ data.tar.gz: 6563e1b22669bb462c433183246c7ce6551ec76e244459463445d2c1b97d9bb24b47b338c18ae107b7c8f8009e09613e18306759967d8a004a9c4fb345e3ff80
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.221.0] - 2026-09-22
9
+
10
+ ### Performance — engine sync (1.9.221)
11
+
12
+ - Vendored libleptris 1.9.221: per-document overflow teardown in
13
+ the dispatch row (#682). No binding-facing surface changes;
14
+ audit 342/342, suite 770/0.
15
+
16
+ ## [1.9.220.0] - 2026-09-22
17
+
18
+ ### Performance — engine sync (1.9.217–1.9.220)
19
+
20
+ - Vendored libleptris 1.9.220: four perf releases — the HTML
21
+ parse tag-id classifier stack + in-place borrowed text runs
22
+ (#1218 slices 1-2), the authoritative content_len text-join
23
+ pass (#1285 slice 1), and borrowed string-values for
24
+ `value-of select="."` (#682). No binding-facing surface
25
+ changes; audit 342/342, suite 770/0.
26
+
8
27
  ## [1.9.216.0] - 2026-09-21
9
28
 
10
29
  ### Added — the plan-ABI consumer contract (#298 fully bound; engine 1.9.214–216)
data/README.adoc CHANGED
@@ -504,10 +504,69 @@ Row kinds: `:scalar`, `:collection`, `:nested` (via `plan:`), `:raw`
504
504
  offset + `type_tag` echo, matching `Node#byte_offset`). Namespace
505
505
  binding lives on plans: `ns:` is `:none` (default), `:any`, or
506
506
  `{ exact: "urn:..." }`; `flags:` also accepts `:cdata`, `:ordered`,
507
- `:ns_lenient` (#754 out-of-namespace adoption). Children the plan
508
- does not describe are skipped; undescribed document order within a
509
- row is preserved. `#walk` returns a lazy `PlanValue` tree (the
510
- result outlives the document); `#to_ruby` materializes it.
507
+ `:ns_lenient` (#754 out-of-namespace adoption), and `:order_spine`
508
+ (#1273 — unmatched sibling text/comment/PI runs emit as positioned
509
+ scalars so ordered hosts rebuild element order without re-parsing).
510
+ Children the plan does not describe are skipped; undescribed document
511
+ order within a row is preserved. `#walk` returns a lazy `PlanValue`
512
+ tree (the result outlives the document); `#to_ruby` materializes it.
513
+
514
+ === Typed scalars and the fused entry (#230/#298)
515
+
516
+ Plan rows accept `type: :integer | :float | :boolean` — the walk
517
+ parses the values natively (in-pass, libleptris #1269a) and
518
+ `#to_ruby` / the bulk face return `Integer` / `Float` / `true` /
519
+ `false`. Unparseable values fall back to the raw String;
520
+ `PlanValue#string_value` stays the raw escape, and
521
+ `#int_value` / `#float_value` / `#bool_value` expose the parsed
522
+ accessors directly.
523
+
524
+ `Descriptor#materialize(source)` is the fused entry: source bytes →
525
+ typed rows in ONE C call — parse, walk, and free happen natively
526
+ (#1269b); the standalone `PlanValue` tree is all that surfaces.
527
+
528
+ [source,ruby]
529
+ ----
530
+ descriptor = Leptris::XML::Descriptor.build(
531
+ name: "iso",
532
+ children: [{ name: "row", kind: :collection, plan: {
533
+ name: "row",
534
+ attributes: [{ name: "id", kind: :scalar, type: :integer }],
535
+ children: [
536
+ { name: "price", kind: :scalar, type: :float },
537
+ { name: "active", kind: :scalar, type: :boolean },
538
+ ] } },
539
+ ])
540
+
541
+ rows = descriptor.materialize(xml_source) # one C call
542
+ rows.as_kwarg_hash[:children]["row"] # typed, hydrator-ready
543
+ ----
544
+
545
+ === Predicate rows (#1272) and order identity (#1273)
546
+
547
+ Same-wire-name rows can partition on attribute values with `when:` —
548
+ AND across pairs, exclusive routing (first matching row wins per
549
+ occurrence):
550
+
551
+ [source,ruby]
552
+ ----
553
+ Leptris::XML::Descriptor.build(
554
+ name: "r",
555
+ children: [
556
+ { name: "item", kind: :collection, when: { "kind" => "a" },
557
+ plan: { name: "item", children: [{ name: "name", kind: :scalar }] } },
558
+ { name: "item", kind: :collection, when: { "kind" => "b" },
559
+ plan: { name: "item", children: [{ name: "name", kind: :scalar }] } },
560
+ ])
561
+ ----
562
+
563
+ `PlanValue#node_kind` and `#order_index` carry document-order
564
+ identity for matched rows (and, under `:order_spine`, for the
565
+ interleaved text runs) — ordered/mixed content reconstructs
566
+ without fragment re-parsing. `PlanValue#as_kwarg_hash` is the
567
+ bulk hydrator face: one Ruby call per node returns the typed
568
+ attribute/children hash (the crossings floor on the 5k-row ISO
569
+ fixture is ~4 FFI crossings per row, pinned by spec).
511
570
 
512
571
  == XQuery
513
572
 
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.216"
11
+ LIBLEPTRIS_VERSION = "1.9.221"
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.216.0"
4
+ VERSION = "1.9.221.0"
5
5
  end
data/lib/libleptris.so CHANGED
Binary file
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.216
4
+ - libleptris v1.9.221
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,73 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.221] - 2026-09-22
4
+
5
+ ### Performance
6
+
7
+ - per-document overflow teardown (#682 dispatch row) (dom)
8
+
9
+
10
+
11
+ ## [1.9.220] - 2026-09-21
12
+
13
+ ### Performance
14
+
15
+ - in-place borrowed text runs (#1218 slice 2) (html)
16
+
17
+
18
+
19
+ ## [1.9.219] - 2026-09-21
20
+
21
+ ### Performance
22
+
23
+ - authoritative content_len in the text-join passes (#1285 slice 1) (dom)
24
+
25
+
26
+
27
+ ## [1.9.218] - 2026-09-21
28
+
29
+ ### Performance
30
+
31
+ - borrowed string-value for value-of select='.' (#682) (xslt)
32
+
33
+
34
+
35
+ ## [1.9.217] - 2026-09-21
36
+
37
+ ### Performance
38
+
39
+ - tag-id classifier stack + single-lookup start tags (#1218) (html)
40
+
41
+
42
+
43
+ ## [1.9.217] - 2026-09-21
44
+
45
+ ### Added — plan-ABI additive v1 (descriptor materialization, PR #1275)
46
+
47
+ Closes #1272, #1273, #1269 — the binding-facing plan upgrades:
48
+
49
+ - **#1272 attribute-predicate rows**: child rows and attribute plans
50
+ accept `leptris_attr_predicate` pairs; same-wire siblings partition
51
+ by predicate (AND across pairs, exclusive routing only when
52
+ predicates are present — the #1115 multi-namespace behavior is
53
+ untouched).
54
+ - **#1273 document-order identity**: every result value carries an
55
+ `order_index` cumulative across rows (strictly increasing in
56
+ document order); `LEPTRIS_PLAN_FLAG_EMIT_ORDER_SPINE` optionally
57
+ appends unmatched text runs so interleavings reconstruct without
58
+ fragment re-parses.
59
+ - **#1269 in-pass types + fusion**: rows carry a `type_tag`
60
+ (1=int / 2=float / 3=bool) executed during the walk with soft-fail
61
+ to string; `leptris_plan_materialize` fuses parse→walk→free into
62
+ one call for the binding hot path.
63
+ - New accessors: `leptris_plan_value_node_kind/_order_index/_int/
64
+ _float/_bool`. `LEPTRIS_PLAN_ABI_VERSION` stays 1 — all additions
65
+ are trailing fields, aggregate initializers keep compiling.
66
+
67
+ Five new specs in `test/abi/test_descriptor.cpp` (Plan1272/1273/
68
+ 1269a/1269b); full ctest green across all 24 CI legs.
69
+
70
+
3
71
  ## [1.9.216] - 2026-09-21
4
72
 
5
73
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.216
6
+ VERSION 1.9.221
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -1,6 +1,16 @@
1
1
  # Lever 7: Lane 16.6 — leptris validate CLI + bindings
2
2
 
3
- Status: TODO. `leptris validate` subcommand (XML well-formedness
4
- + DTD + RNG paths through the public API) and the binding-side
5
- validate wrappers. Small, closed scope; CLI command registration
6
- follows cli/CLI_ARCHITECTURE.md (new file in cli/commands/).
3
+ Status: DONE (issue #1183). All three legs shipped and verified on
4
+ main:
5
+
6
+ - **CLI**: `cli/commands/validate.c` (286 lines) — RELAX NG +
7
+ Schematron validation, stdin/file input; registered in
8
+ `cli/main.c:97`, listed in the help text (`leptris validate`).
9
+ - **leptris-ruby**: `RelaxNG#validate/#valid?/#validate_errors/
10
+ #validate_report`, `Schematron#validate/#valid?`,
11
+ `DTD#validate/#valid?` (lib/leptris/xml/{relaxng,schematron,dtd}.rb).
12
+ - **leptris-py**: `RelaxNG`, `DTD`, `Schematron` exported from the
13
+ package root (leptris/__init__.py).
14
+
15
+ Well-formedness validation is `leptris_parse_string` itself (line:col
16
+ error contract); no separate wrapper required.
@@ -67,7 +67,28 @@ static size_t gen_entities(char* out, int paras) {
67
67
  return len;
68
68
  }
69
69
 
70
+ /* Table-of-rows shape (#1218): well-formed medium HTML — one big
71
+ * tbody, no error-recovery cases, matching the serialbench
72
+ * medium.html fixture class the issue reports 3.4-7x on. */
73
+ static size_t gen_table_rows(char* out, int rows) {
74
+ size_t len = 0;
75
+ len += (size_t)sprintf(out + len,
76
+ "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'>"
77
+ "<title>Report</title></head><body>"
78
+ "<table class='data'><thead><tr><th>Id</th><th>Name</th>"
79
+ "<th>Value</th></tr></thead><tbody>");
80
+ for (int r = 0; r < rows; r++)
81
+ len += (size_t)sprintf(out + len,
82
+ "<tr class='r%d'><td>%d</td><td>row-%d</td>"
83
+ "<td data-v='%d'>%d.%02d</td></tr>",
84
+ r & 1, r, r, r, r, r & 99);
85
+ len += (size_t)sprintf(out + len,
86
+ "</tbody></table></body></html>");
87
+ return len;
88
+ }
89
+
70
90
  int main(void) {
91
+ const char* only = getenv("HTML_BENCH_ONLY"); /* page|entities|table */
71
92
  size_t cap = 1u << 22;
72
93
  char* html = (char*)malloc(cap);
73
94
  if (!html) return 1;
@@ -80,6 +101,7 @@ int main(void) {
80
101
 
81
102
  int reps = getenv("HTML_BENCH_REPS") ? atoi(getenv("HTML_BENCH_REPS")) : 9;
82
103
  double best = 1e18;
104
+ if (!only || strcmp(only, "page") == 0) {
83
105
  for (int i = 0; i < reps; i++) {
84
106
  double t0 = benchmark_time_us();
85
107
  LeptrisDocument d = leptris_parse_html_string(html, len, &st);
@@ -89,8 +111,10 @@ int main(void) {
89
111
  }
90
112
  printf("leptris html parse: %zu bytes, %.0f us, %.1f MB/s (best of %d)\n",
91
113
  len, best, (double)len / best, reps);
114
+ }
92
115
 
93
116
  /* Entity-laden second shape (nokogiri reference: same page). */
117
+ if (!only || strcmp(only, "entities") == 0) {
94
118
  len = gen_entities(html, 20000);
95
119
  warm = leptris_parse_html_string(html, len, &st);
96
120
  if (!warm) { printf("entity parse failed\n"); return 1; }
@@ -106,6 +130,26 @@ int main(void) {
106
130
  printf("leptris html parse (entity-laden): %zu bytes, %.0f us, "
107
131
  "%.1f MB/s (best of %d)\n",
108
132
  len, best, (double)len / best, reps);
133
+ }
134
+
135
+ /* Table-of-rows ~2MB shape (#1218, serialbench medium class). */
136
+ if (!only || strcmp(only, "table") == 0) {
137
+ len = gen_table_rows(html, 32000);
138
+ warm = leptris_parse_html_string(html, len, &st);
139
+ if (!warm) { printf("table parse failed\n"); return 1; }
140
+ leptris_document_free(warm);
141
+ best = 1e18;
142
+ for (int i = 0; i < reps; i++) {
143
+ double t0 = benchmark_time_us();
144
+ LeptrisDocument d = leptris_parse_html_string(html, len, &st);
145
+ double us = benchmark_time_us() - t0;
146
+ leptris_document_free(d);
147
+ if (us < best) best = us;
148
+ }
149
+ printf("leptris html parse (table-rows): %zu bytes, %.0f us, "
150
+ "%.1f MB/s (best of %d)\n",
151
+ len, best, (double)len / best, reps);
152
+ }
109
153
  free(html);
110
154
  return 0;
111
155
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  #include "compact.h"
12
+ #include "../leptris_internal.h" /* struct leptris_document (overflow_entries) */
12
13
  #include "../common/port.h"
13
14
  #include <stdlib.h>
14
15
  #include <string.h>
@@ -158,6 +159,14 @@ int leptris_compact_overflow_set(LeptrisCompactOverflowTable* table,
158
159
  table->buckets[index] = entry;
159
160
  table->entry_count++;
160
161
 
162
+ /* #682: parallel per-document chain — teardown walks exactly
163
+ * this doc's entries instead of scanning the whole table. */
164
+ if (doc) {
165
+ entry->doc_next =
166
+ (struct leptris_compact_overflow_entry*)doc->overflow_entries;
167
+ doc->overflow_entries = entry;
168
+ }
169
+
161
170
  if (table->entry_count >= table->bucket_count) {
162
171
  overflow_table_grow(table);
163
172
  }
@@ -208,26 +217,40 @@ void leptris_compact_cleanup(void) {
208
217
  void leptris_compact_cleanup_document(struct leptris_document* doc) {
209
218
  if (!doc || !g_overflow_table) return;
210
219
 
211
- size_t removed_count = 0;
220
+ /* #682: walk THIS document's entry chain (linked at set time)
221
+ * instead of scanning every bucket of the global table — the
222
+ * old full-table sweep ran on every document free and dominated
223
+ * apply-heavy XSLT profiles (result docs register thousands of
224
+ * cross-block edge entries; the sampler pinned 35% of the
225
+ * dispatch bench in this loop). */
226
+ LeptrisCompactOverflowEntry* e =
227
+ (LeptrisCompactOverflowEntry*)doc->overflow_entries;
228
+ doc->overflow_entries = NULL;
212
229
 
213
- for (size_t i = 0; i < g_overflow_table->bucket_count; i++) {
214
- LeptrisCompactOverflowEntry** entry_ptr = &g_overflow_table->buckets[i];
215
- LeptrisCompactOverflowEntry* entry = *entry_ptr;
216
-
217
- while (entry) {
218
- if (entry->doc == doc) {
219
- /* Unlink only — entries live in SLABS owned by the
220
- * table; free()ing them corrupted the heap whenever
221
- * a document had overflow entries (common since the
222
- * #450 sibling-edge work routed far links here). */
223
- *entry_ptr = entry->next;
224
- entry = *entry_ptr;
225
- removed_count++;
226
- } else {
227
- entry_ptr = &entry->next;
228
- entry = entry->next;
230
+ size_t removed_count = 0;
231
+ while (e) {
232
+ LeptrisCompactOverflowEntry* next = e->doc_next;
233
+ if (e->doc == doc) {
234
+ /* Stale chain links (entry re-registered under another
235
+ * doc) are skipped — the ->doc check keeps them alive
236
+ * for their true owner. */
237
+ size_t index = hash_pointer(e->key,
238
+ g_overflow_table->bucket_count);
239
+ LeptrisCompactOverflowEntry** entry_ptr =
240
+ &g_overflow_table->buckets[index];
241
+ while (*entry_ptr) {
242
+ if (*entry_ptr == e) {
243
+ /* Unlink only — entries live in SLABS owned by
244
+ * the table; free()ing them corrupted the heap
245
+ * (#450 sibling-edge follow-up). */
246
+ *entry_ptr = e->next;
247
+ removed_count++;
248
+ break;
249
+ }
250
+ entry_ptr = &(*entry_ptr)->next;
229
251
  }
230
252
  }
253
+ e = next;
231
254
  }
232
255
 
233
256
  g_overflow_table->entry_count -= removed_count;
@@ -60,6 +60,9 @@ typedef struct leptris_compact_overflow_entry {
60
60
  void* value; /* Actual pointer value */
61
61
  struct leptris_document* doc; /* Document that owns this entry */
62
62
  struct leptris_compact_overflow_entry* next;
63
+ /* #682: per-document chain — teardown walks THIS doc's entries
64
+ * instead of scanning every bucket of the global table. */
65
+ struct leptris_compact_overflow_entry* doc_next;
63
66
  } LeptrisCompactOverflowEntry;
64
67
 
65
68
  /**
@@ -979,7 +979,12 @@ static size_t calculate_text_length_recursive(LeptrisNode* node) {
979
979
  * text->content_len directly. */
980
980
  const char* content = leptris_text_get_content(text);
981
981
  if (content) {
982
- len += strlen(content);
982
+ /* #1285 slice 1: frozen (parse-created,
983
+ * immutable) texts carry an authoritative
984
+ * content_len — skip the scan. */
985
+ size_t clen = child->frozen ? text->content_len
986
+ : strlen(content);
987
+ len += clen;
983
988
  }
984
989
  } else if (child->type == LEPTRIS_NODE_TYPE_CDATA) {
985
990
  LeptrisCDATANode* cdata = (LeptrisCDATANode*)child;
@@ -1013,7 +1018,10 @@ static void copy_text_content_recursive(LeptrisNode* node, char* result, size_t*
1013
1018
  LeptrisTextNode* text = (LeptrisTextNode*)child;
1014
1019
  const char* content = leptris_text_get_content(text);
1015
1020
  if (content) {
1016
- size_t clen = strlen(content);
1021
+ /* #1285 slice 1: frozen texts — authoritative
1022
+ * content_len (mirrors the length pass). */
1023
+ size_t clen = child->frozen ? text->content_len
1024
+ : strlen(content);
1017
1025
  memcpy(result + *offset, content, clen);
1018
1026
  *offset += clen;
1019
1027
  }
@@ -102,6 +102,16 @@ const char* leptris_text_get_content(LeptrisTextNode* text) {
102
102
  if (!text) return NULL;
103
103
 
104
104
  if (text->borrowed && text->pool) {
105
+ /* #1218 slice 2: an in-place terminated run (the parser
106
+ * wrote the NUL into the doc-owned input copy) is already
107
+ * materialized — serve it directly when it carries no
108
+ * entity. A run WITH '&' still takes the expansion path
109
+ * below (dp-borrowed nodes rely on it). */
110
+ if (text->content_len > 0 &&
111
+ text->content[text->content_len] == '\0' &&
112
+ memchr(text->content, '&', text->content_len) == NULL) {
113
+ return text->content;
114
+ }
105
115
  /* Entity expansion: if the borrowed content contains '&',
106
116
  * expand predefined XML entities (&amp;, &lt;, etc.) and
107
117
  * numeric character references (&#65;, &#x42;) into a new