teptris 0.3.4.1 → 0.3.6.2

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.
@@ -9,7 +9,15 @@
9
9
  #define FMT_FORBID_TIME 2
10
10
  #define FMT_FORBID_DATE 4
11
11
 
12
- static VALUE eParseError, eError, cDate;
12
+ static VALUE eParseError, eError, cDate, cDoc;
13
+ /* rb_raise's varargs formatting segfaults in its format dispatch on
14
+ * windows x64-mingw-ucrt ruby 4.0 (diag #158/#160: the identical raise
15
+ * crashes across PGO on/off while direct exception construction runs
16
+ * clean, and every rb_exc_new site has always been fine) — build the
17
+ * message with snprintf and raise via rb_exc_new. */
18
+ static void raise_teptris(const char *msg) {
19
+ rb_exc_raise(rb_exc_new(eError, msg, (long)strlen(msg)));
20
+ }
13
21
 
14
22
  /* civil date -> days since epoch (Hinnant, inverse of the dump side) */
15
23
  static int64_t days_from_civil(int32_t y, uint8_t m, uint8_t d) {
@@ -223,14 +231,14 @@ static VALUE obj_from_node(const teptris_node *n, unsigned flags,
223
231
  if (flags & FMT_STRING_DATES) return dt_string(&d, kind);
224
232
  if (kind == TEPTRIS_DATETIME_OFFSET || kind == TEPTRIS_DATETIME_LOCAL) {
225
233
  if (flags & FMT_FORBID_TIME)
226
- rb_raise(eError, "value materializes Time, which is not in "
227
- "permitted_classes (use datetime_policy: :string)");
234
+ raise_teptris("value materializes Time, which is not in "
235
+ "permitted_classes (use datetime_policy: :string)");
228
236
  return time_from_dt(&d, kind == TEPTRIS_DATETIME_OFFSET);
229
237
  }
230
238
  if (kind == TEPTRIS_DATE_LOCAL) {
231
239
  if (flags & FMT_FORBID_DATE)
232
- rb_raise(eError, "value materializes Date, which is not in "
233
- "permitted_classes (use datetime_policy: :string)");
240
+ raise_teptris("value materializes Date, which is not in "
241
+ "permitted_classes (use datetime_policy: :string)");
234
242
  return rb_funcall(cDate, rb_intern("new"), 3,
235
243
  INT2FIX(d.year), INT2FIX(d.month), INT2FIX(d.day));
236
244
  }
@@ -297,6 +305,66 @@ static VALUE ext_load(int argc, VALUE *argv, VALUE self) {
297
305
  return out;
298
306
  }
299
307
 
308
+ /* ---------------- native-doc API: the retained DOM ----------------
309
+ * load_doc parses into an engine document the wrapper OWNS, together
310
+ * with a private copy of the input - the doc's keys/values are
311
+ * zero-copy views into that copy, so both live and die together.
312
+ * dump_doc serializes with the C emitter: the roundtrip
313
+ * dump_doc(load_doc(s)) never materializes Ruby objects. */
314
+
315
+ static void dump_check(teptris_status st);
316
+
317
+ typedef struct {
318
+ teptris_document *doc;
319
+ char *buf;
320
+ } doc_owner;
321
+
322
+ static void doc_owner_free(void *p) {
323
+ doc_owner *o = (doc_owner *)p;
324
+ if (o->doc != NULL) teptris_document_free(o->doc);
325
+ if (o->buf != NULL) xfree(o->buf);
326
+ xfree(o);
327
+ }
328
+ static size_t doc_owner_size(const void *p) { return sizeof(doc_owner); }
329
+ static const rb_data_type_t doc_owner_type = {
330
+ "TeptrisExt/Doc",
331
+ {0, doc_owner_free, doc_owner_size, NULL, 0},
332
+ NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY};
333
+
334
+ static VALUE ext_load_doc(VALUE self, VALUE str) {
335
+ StringValue(str);
336
+ long len = RSTRING_LEN(str);
337
+ char *cbuf = ALLOC_N(char, (size_t)len + 1);
338
+ memcpy(cbuf, RSTRING_PTR(str), (size_t)len);
339
+ teptris_document *doc = NULL;
340
+ struct gvl_parse_arg arg = {cbuf, (size_t)len, &doc, TEPTRIS_OK};
341
+ rb_thread_call_without_gvl(parse_without_gvl, &arg, RUBY_UBF_IO, NULL);
342
+ if (arg.st != TEPTRIS_OK) {
343
+ const teptris_error *e = teptris_document_error(doc);
344
+ VALUE ex = rb_exc_new(eParseError, e->message, (long)strlen(e->message));
345
+ rb_iv_set(ex, "@line", SIZET2NUM(e->line));
346
+ rb_iv_set(ex, "@column", SIZET2NUM(e->column));
347
+ xfree(cbuf);
348
+ teptris_document_free(doc);
349
+ rb_exc_raise(ex);
350
+ }
351
+ doc_owner *o = ALLOC(doc_owner);
352
+ o->doc = doc;
353
+ o->buf = cbuf;
354
+ return TypedData_Wrap_Struct(cDoc, &doc_owner_type, o);
355
+ }
356
+
357
+ static VALUE ext_dump_doc(VALUE self, VALUE rbdoc) {
358
+ doc_owner *o;
359
+ TypedData_Get_Struct(rbdoc, doc_owner, &doc_owner_type, o);
360
+ char *buf = NULL;
361
+ size_t len = 0;
362
+ dump_check(teptris_document_emit(o->doc, &buf, &len));
363
+ VALUE out = rb_utf8_str_new(buf, (long)len);
364
+ free(buf);
365
+ return out;
366
+ }
367
+
300
368
  static VALUE ext_version(VALUE self) {
301
369
  return rb_str_new_cstr(teptris_version_string());
302
370
  }
@@ -305,11 +373,15 @@ static VALUE ext_version(VALUE self) {
305
373
 
306
374
  static ID id_utc_offset, id_to_s, id_to_i, id_nsec;
307
375
 
376
+
308
377
  static void dump_check(teptris_status st) {
309
378
  if (st == TEPTRIS_OK) return;
310
379
  if (st == TEPTRIS_ERR_ALLOC)
311
- rb_raise(eError, "out of memory while dumping");
312
- rb_raise(eError, "cannot dump value (%s)", teptris_status_string(st));
380
+ raise_teptris("out of memory while dumping");
381
+ char msg[128];
382
+ snprintf(msg, sizeof msg, "cannot dump value (%s)",
383
+ teptris_status_string(st));
384
+ raise_teptris(msg);
313
385
  }
314
386
 
315
387
  /* days since 1970-01-01 -> proleptic-Gregorian y/m/d (Hinnant) */
@@ -384,7 +456,9 @@ static void dump_scalar(teptris_builder *b, const char *key, size_t klen,
384
456
  TEPTRIS_DATE_LOCAL, &dt));
385
457
  return;
386
458
  }
387
- rb_raise(eError, "cannot dump %s", rb_obj_classname(v));
459
+ char msg[192];
460
+ snprintf(msg, sizeof msg, "cannot dump %s", rb_obj_classname(v));
461
+ raise_teptris(msg);
388
462
  }
389
463
  }
390
464
 
@@ -446,21 +520,28 @@ static VALUE dump_with(VALUE obj,
446
520
  rb_obj_classname(obj));
447
521
  teptris_builder *b = teptris_builder_new();
448
522
  if (b == NULL)
449
- rb_raise(eError, "out of memory while dumping");
523
+ raise_teptris("out of memory while dumping");
450
524
  rb_hash_foreach(obj, table_each, (VALUE)b);
451
525
  teptris_document *doc = NULL;
452
526
  teptris_status st = teptris_builder_finish(b, &doc);
453
527
  if (st != TEPTRIS_OK) {
454
528
  teptris_builder_free(b);
455
- rb_raise(eError, "cannot dump (%s)", teptris_status_string(st));
529
+ char msg[128];
530
+ snprintf(msg, sizeof msg, "cannot dump (%s)",
531
+ teptris_status_string(st));
532
+ raise_teptris(msg);
456
533
  }
457
534
  char *buf = NULL;
458
535
  size_t len = 0;
459
536
  st = emit(doc, &buf, &len);
460
537
  teptris_document_free(doc);
461
538
  teptris_builder_free(b); /* doc already transferred/freed */
462
- if (st != TEPTRIS_OK)
463
- rb_raise(eError, "dump failed (%s)", teptris_status_string(st));
539
+ if (st != TEPTRIS_OK) {
540
+ char msg[128];
541
+ snprintf(msg, sizeof msg, "dump failed (%s)",
542
+ teptris_status_string(st));
543
+ raise_teptris(msg);
544
+ }
464
545
  VALUE out = rb_enc_str_new(buf, (long)len, rb_utf8_encoding());
465
546
  free(buf);
466
547
  return out;
@@ -674,7 +755,7 @@ static VALUE ext_plan_build(VALUE self, VALUE rows, VALUE first_row)
674
755
  long nrows = RARRAY_LEN(rows);
675
756
  long nplans = RARRAY_LEN(first_row) - 1;
676
757
  if (nplans < 1 || nrows < 1) {
677
- rb_raise(eError, "plan needs >= 1 plan and >= 1 row");
758
+ raise_teptris("plan needs >= 1 plan and >= 1 row");
678
759
  }
679
760
  teptris_plan_row *crows = calloc((size_t)nrows, sizeof(*crows));
680
761
  /* bytes-vs-count: this must be (nplans+1) * sizeof(uint32_t) — the
@@ -684,7 +765,7 @@ static VALUE ext_plan_build(VALUE self, VALUE rows, VALUE first_row)
684
765
  if (crows == NULL || cfirst == NULL) {
685
766
  free(crows);
686
767
  free(cfirst);
687
- rb_raise(eError, "out of memory building plan");
768
+ raise_teptris("out of memory building plan");
688
769
  }
689
770
  for (long i = 0; i < nrows; i++) {
690
771
  VALUE row = rb_ary_entry(rows, i);
@@ -705,7 +786,9 @@ static VALUE ext_plan_build(VALUE self, VALUE rows, VALUE first_row)
705
786
  free(crows);
706
787
  free(cfirst);
707
788
  if (plan == NULL) {
708
- rb_raise(eError, "invalid plan spec (status %d)", (int)st);
789
+ char msg[96];
790
+ snprintf(msg, sizeof msg, "invalid plan spec (status %d)", (int)st);
791
+ raise_teptris(msg);
709
792
  }
710
793
  return TypedData_Wrap_Struct(rb_cObject, &plan_type, plan);
711
794
  }
@@ -730,7 +813,9 @@ static VALUE ext_plan_emit(VALUE self, VALUE rb_plan, VALUE toml)
730
813
  teptris_plan_walk(plan, teptris_document_root(doc), &st);
731
814
  if (res == NULL) {
732
815
  teptris_document_free(doc);
733
- rb_raise(eError, "plan walk failed (status %d)", (int)st);
816
+ char msg[96];
817
+ snprintf(msg, sizeof msg, "plan walk failed (status %d)", (int)st);
818
+ raise_teptris(msg);
734
819
  }
735
820
  VALUE out = asm_rows(plan, res, 0);
736
821
  teptris_plan_result_free(res);
@@ -866,7 +951,7 @@ static VALUE lazy_each(VALUE self) {
866
951
  break;
867
952
  }
868
953
  default:
869
- rb_raise(eError, "scalar node has nothing to iterate");
954
+ raise_teptris("scalar node has nothing to iterate");
870
955
  }
871
956
  return self;
872
957
  }
@@ -897,7 +982,7 @@ static VALUE lazy_get(VALUE self, VALUE key) {
897
982
  break;
898
983
  }
899
984
  default:
900
- rb_raise(eError, "scalar node is not subscriptable");
985
+ raise_teptris("scalar node is not subscriptable");
901
986
  }
902
987
  if (child == NULL) return Qnil;
903
988
  return lazy_wrap(v->owner, child);
@@ -1011,7 +1096,12 @@ static VALUE ext_load_lazy_batch(VALUE self, VALUE strings) {
1011
1096
  long n = RARRAY_LEN(strings);
1012
1097
  if (n == 0) return rb_ary_new();
1013
1098
  batch_scratch *s = batch_scratch_alloc(n);
1014
- if (s == NULL) rb_raise(eError, "out of memory batching %ld documents", n);
1099
+ if (s == NULL) {
1100
+ char msg[96];
1101
+ snprintf(msg, sizeof msg,
1102
+ "out of memory batching %ld documents", n);
1103
+ raise_teptris(msg);
1104
+ }
1015
1105
  /* the batch parse is pure C: pin every input (one tmp lock per
1016
1106
  * string - repeated objects dup; frozen strings cannot take the
1017
1107
  * lock, yet compaction can move them) and release the GVL for the
@@ -1163,7 +1253,12 @@ static VALUE ext_load_batch(int argc, VALUE *argv, VALUE self) {
1163
1253
  if (n == 0) return rb_ary_new();
1164
1254
  unsigned flags = flags_from_opts(opts);
1165
1255
  batch_scratch *s = batch_scratch_alloc(n);
1166
- if (s == NULL) rb_raise(eError, "out of memory batching %ld documents", n);
1256
+ if (s == NULL) {
1257
+ char msg[96];
1258
+ snprintf(msg, sizeof msg,
1259
+ "out of memory batching %ld documents", n);
1260
+ raise_teptris(msg);
1261
+ }
1167
1262
  for (long i = 0; i < n; i++) {
1168
1263
  VALUE str = rb_ary_entry(strings, i);
1169
1264
  StringValue(str);
@@ -1175,7 +1270,10 @@ static VALUE ext_load_batch(int argc, VALUE *argv, VALUE self) {
1175
1270
  s->docs, s->statuses);
1176
1271
  if (batch != TEPTRIS_OK) {
1177
1272
  batch_scratch_free(s);
1178
- rb_raise(eError, "batch parse failed (status %d)", (int)batch);
1273
+ char msg[96];
1274
+ snprintf(msg, sizeof msg, "batch parse failed (status %d)",
1275
+ (int)batch);
1276
+ raise_teptris(msg);
1179
1277
  }
1180
1278
  long first_fail = -1;
1181
1279
  for (long i = 0; i < n; i++) {
@@ -1209,7 +1307,13 @@ void Init_teptris_ext(void) {
1209
1307
  * call state (key cache, arenas, types) is per-object or per-call */
1210
1308
  rb_ext_ractor_safe(true);
1211
1309
  VALUE m = rb_define_module("TeptrisExt");
1310
+ cDoc = rb_define_class_under(m, "Doc", rb_cObject);
1311
+ /* opaque handle: no Ruby-side allocation (silences the
1312
+ * allocator-undefine warning on reload) */
1313
+ rb_undef_alloc_func(cDoc);
1212
1314
  rb_define_module_function(m, "load", ext_load, -1);
1315
+ rb_define_module_function(m, "load_doc", ext_load_doc, 1);
1316
+ rb_define_module_function(m, "dump_doc", ext_dump_doc, 1);
1213
1317
  rb_define_module_function(m, "load_lazy", ext_load_lazy, 1);
1214
1318
  rb_define_module_function(m, "load_batch", ext_load_batch, -1);
1215
1319
  rb_define_module_function(m, "load_slice", ext_load_slice, -1);
data/lib/teptris/toml.rb CHANGED
@@ -18,6 +18,24 @@ module Teptris::TOML
18
18
  TeptrisExt.load(toml)
19
19
  end
20
20
 
21
+ # Parse into a RETAINED engine document (an opaque TeptrisExt::Doc
22
+ # handle; no Ruby objects are materialized). Pair with dump_doc for
23
+ # a roundtrip that runs entirely at C speed - reserialization of
24
+ # an unchanged document skips both Ruby-object walks. The doc owns
25
+ # a private copy of the input: its values are views into it, so
26
+ # mutating the source string afterwards is invisible (and the doc
27
+ # keeps the copy alive).
28
+ def load_doc(toml)
29
+ raise ArgumentError, "input must be a String" unless toml.is_a?(String)
30
+
31
+ TeptrisExt.load_doc(toml)
32
+ end
33
+
34
+ # Emit a load_doc() document at C speed (canonical TOML).
35
+ def dump_doc(doc)
36
+ TeptrisExt.dump_doc(doc)
37
+ end
38
+
21
39
  # Safe-load shaped entry point for frameworks: permitted_classes
22
40
  # restricts datetime materialization (raise when a value would
23
41
  # materialize an unpermitted class); datetime_policy :string keeps
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teptris
4
- VERSION = "0.3.4.1"
4
+ VERSION = "0.3.6.2"
5
5
  end
data/sig/teptris.rbs CHANGED
@@ -22,6 +22,8 @@ module Teptris
22
22
  # Time for local datetimes, Date for dates, "HH:MM:SS" String for
23
23
  # local times.
24
24
  def self.load: (String toml) -> Hash[String, untyped]
25
+ def self.load_doc: (String toml) -> untyped
26
+ def self.dump_doc: (untyped doc) -> String
25
27
 
26
28
  def self.load_file: (String path) -> Hash[String, untyped]
27
29
 
@@ -122,6 +124,10 @@ module TeptrisExt
122
124
 
123
125
  def self.load_lazy_batch: (Array[String] tomls) -> Array[LazyValue]
124
126
 
127
+ def self.load_doc: (String toml) -> untyped
128
+
129
+ def self.dump_doc: (untyped doc) -> String
130
+
125
131
  def self.dump: (untyped obj) -> String
126
132
 
127
133
  def self.dump_json_natural: (untyped obj) -> String
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4.1
4
+ version: 0.3.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - teptris contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A native C extension over libteptris, a C99 TOML 1.1 parser/emitter.
14
14
  API shape mirrors the tomlib gem.
@@ -39,8 +39,10 @@ files:
39
39
  - ext/teptris_ext/engine/src/teptris/common/simd.h
40
40
  - ext/teptris_ext/engine/src/teptris/dom/dom.c
41
41
  - ext/teptris_ext/engine/src/teptris/dom/dom.h
42
+ - ext/teptris_ext/engine/src/teptris/emit/emit_shared.h
42
43
  - ext/teptris_ext/engine/src/teptris/emit/emitter.c
43
44
  - ext/teptris_ext/engine/src/teptris/emit/emitter.h
45
+ - ext/teptris_ext/engine/src/teptris/emit/writer.c
44
46
  - ext/teptris_ext/engine/src/teptris/memory/arena.c
45
47
  - ext/teptris_ext/engine/src/teptris/memory/arena.h
46
48
  - ext/teptris_ext/engine/src/teptris/parse/datetime.c