yeptris 0.1.13.3-x86_64-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.
@@ -0,0 +1,438 @@
1
+ /* yeptris_native.c — Ruby C-API materializer over libyeptris visit
2
+ * (TODO.restructure/22). One Ruby→C call builds the whole object
3
+ * graph via rb_hash_new / rb_ary_push / rb_str_new_len — the same
4
+ * shape as JSON.parse, so the binding can beat it on the fused JSON
5
+ * path. The extension is optional: LoadError falls back to the FFI
6
+ * Marshal ladder. */
7
+
8
+ #include <ruby.h>
9
+ #include <ruby/encoding.h>
10
+
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+
14
+ #include <yeptris/api.h>
15
+ #include <yeptris/error.h>
16
+ #include <yeptris/resolve.h>
17
+ #include <yeptris/visit.h>
18
+
19
+ #define YEP_RB_MAX_DEPTH 1024
20
+
21
+ static rb_encoding* utf8_enc;
22
+
23
+ typedef struct {
24
+ VALUE stack[YEP_RB_MAX_DEPTH];
25
+ VALUE keys[YEP_RB_MAX_DEPTH]; /* pending map key at this depth */
26
+ int is_map[YEP_RB_MAX_DEPTH];
27
+ int sp;
28
+ VALUE root;
29
+ VALUE anchors; /* Hash name=>object for YAML identity */
30
+ VALUE pending_anchor; /* String name awaiting the next value */
31
+ int failed;
32
+ } rb_ctx;
33
+
34
+ static void rb_fail(rb_ctx* c) {
35
+ c->failed = 1;
36
+ }
37
+
38
+ static int rb_push_value(rb_ctx* c, VALUE v) {
39
+ if (c->failed) {
40
+ return -1;
41
+ }
42
+ /* bind pending anchor */
43
+ if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
44
+ rb_hash_aset(c->anchors, c->pending_anchor, v);
45
+ c->pending_anchor = Qnil;
46
+ }
47
+ if (c->sp == 0) {
48
+ c->root = v;
49
+ return 0;
50
+ }
51
+ VALUE parent = c->stack[c->sp - 1];
52
+ if (c->is_map[c->sp - 1]) {
53
+ VALUE key = c->keys[c->sp - 1];
54
+ if (NIL_P(key)) {
55
+ rb_fail(c);
56
+ return -1;
57
+ }
58
+ rb_hash_aset(parent, key, v);
59
+ c->keys[c->sp - 1] = Qnil;
60
+ } else {
61
+ rb_ary_push(parent, v);
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ static int on_null(void* ctx) {
67
+ return rb_push_value((rb_ctx*)ctx, Qnil);
68
+ }
69
+
70
+ static int on_bool(void* ctx, int truthy) {
71
+ return rb_push_value((rb_ctx*)ctx, truthy ? Qtrue : Qfalse);
72
+ }
73
+
74
+ static int on_int(void* ctx, int64_t v) {
75
+ return rb_push_value((rb_ctx*)ctx, LL2NUM(v));
76
+ }
77
+
78
+ static int on_float(void* ctx, double v) {
79
+ return rb_push_value((rb_ctx*)ctx, DBL2NUM(v));
80
+ }
81
+
82
+ static rb_encoding* utf8_enc;
83
+
84
+ static VALUE rb_utf8_str(const char* p, size_t len) {
85
+ return rb_enc_str_new(p, (long)len, utf8_enc);
86
+ }
87
+
88
+ /* One-shot interned string (no intermediate alloc) — keys and the
89
+ * short repeated values ("a"/"b"/…) share one VALUE. */
90
+ static VALUE rb_utf8_interned(const char* p, size_t len) {
91
+ return rb_enc_interned_str(p, (long)len, utf8_enc);
92
+ }
93
+
94
+ static int on_string(void* ctx, const char* p, size_t len) {
95
+ rb_ctx* c = (rb_ctx*)ctx;
96
+ /* short strings are almost always repeated tokens in JSON corpora;
97
+ * intern them. Longer payloads stay unique. */
98
+ VALUE s = (len <= 16) ? rb_utf8_interned(p, len) : rb_utf8_str(p, len);
99
+ return rb_push_value(c, s);
100
+ }
101
+
102
+ static int on_key(void* ctx, const char* p, size_t len) {
103
+ rb_ctx* c = (rb_ctx*)ctx;
104
+ if (c->sp == 0 || !c->is_map[c->sp - 1]) {
105
+ rb_fail(c);
106
+ return -1;
107
+ }
108
+ c->keys[c->sp - 1] = rb_utf8_interned(p, len);
109
+ return 0;
110
+ }
111
+
112
+ static int on_seq_start(void* ctx) {
113
+ rb_ctx* c = (rb_ctx*)ctx;
114
+ if (c->failed || c->sp >= YEP_RB_MAX_DEPTH) {
115
+ rb_fail(c);
116
+ return -1;
117
+ }
118
+ VALUE a = rb_ary_new();
119
+ /* bind anchor to the container before placing it */
120
+ if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
121
+ rb_hash_aset(c->anchors, c->pending_anchor, a);
122
+ c->pending_anchor = Qnil;
123
+ }
124
+ if (c->sp == 0) {
125
+ c->root = a;
126
+ } else {
127
+ VALUE parent = c->stack[c->sp - 1];
128
+ if (c->is_map[c->sp - 1]) {
129
+ VALUE key = c->keys[c->sp - 1];
130
+ if (NIL_P(key)) {
131
+ rb_fail(c);
132
+ return -1;
133
+ }
134
+ rb_hash_aset(parent, key, a);
135
+ c->keys[c->sp - 1] = Qnil;
136
+ } else {
137
+ rb_ary_push(parent, a);
138
+ }
139
+ }
140
+ c->stack[c->sp] = a;
141
+ c->is_map[c->sp] = 0;
142
+ c->keys[c->sp] = Qnil;
143
+ c->sp++;
144
+ return 0;
145
+ }
146
+
147
+ static int on_seq_end(void* ctx) {
148
+ rb_ctx* c = (rb_ctx*)ctx;
149
+ if (c->sp <= 0) {
150
+ rb_fail(c);
151
+ return -1;
152
+ }
153
+ c->sp--;
154
+ return 0;
155
+ }
156
+
157
+ static int on_map_start(void* ctx) {
158
+ rb_ctx* c = (rb_ctx*)ctx;
159
+ if (c->failed || c->sp >= YEP_RB_MAX_DEPTH) {
160
+ rb_fail(c);
161
+ return -1;
162
+ }
163
+ VALUE h = rb_hash_new();
164
+ if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
165
+ rb_hash_aset(c->anchors, c->pending_anchor, h);
166
+ c->pending_anchor = Qnil;
167
+ }
168
+ if (c->sp == 0) {
169
+ c->root = h;
170
+ } else {
171
+ VALUE parent = c->stack[c->sp - 1];
172
+ if (c->is_map[c->sp - 1]) {
173
+ VALUE key = c->keys[c->sp - 1];
174
+ if (NIL_P(key)) {
175
+ rb_fail(c);
176
+ return -1;
177
+ }
178
+ rb_hash_aset(parent, key, h);
179
+ c->keys[c->sp - 1] = Qnil;
180
+ } else {
181
+ rb_ary_push(parent, h);
182
+ }
183
+ }
184
+ c->stack[c->sp] = h;
185
+ c->is_map[c->sp] = 1;
186
+ c->keys[c->sp] = Qnil;
187
+ c->sp++;
188
+ return 0;
189
+ }
190
+
191
+ static int on_map_end(void* ctx) {
192
+ rb_ctx* c = (rb_ctx*)ctx;
193
+ if (c->sp <= 0) {
194
+ rb_fail(c);
195
+ return -1;
196
+ }
197
+ c->sp--;
198
+ return 0;
199
+ }
200
+
201
+ static int on_anchor(void* ctx, const char* name, size_t len) {
202
+ rb_ctx* c = (rb_ctx*)ctx;
203
+ c->pending_anchor = rb_utf8_str(name, len);
204
+ return 0;
205
+ }
206
+
207
+ static int on_alias(void* ctx, const char* name, size_t len) {
208
+ rb_ctx* c = (rb_ctx*)ctx;
209
+ VALUE key = rb_utf8_str(name, len);
210
+ VALUE v = rb_hash_lookup2(c->anchors, key, Qundef);
211
+ if (v == Qundef) {
212
+ v = Qnil;
213
+ }
214
+ return rb_push_value(c, v);
215
+ }
216
+
217
+ static int on_doc(void* ctx) {
218
+ /* multi-doc: for load_all we'd collect; single-load takes first.
219
+ * reset root so subsequent docs replace — load_stream uses a
220
+ * different entry that accumulates. */
221
+ (void)ctx;
222
+ return 0;
223
+ }
224
+
225
+ static const YeptrisVisitVTable k_vt = {
226
+ on_null, on_bool, on_int, on_float, on_string,
227
+ on_seq_start, on_seq_end, on_map_start, on_map_end,
228
+ on_key, on_doc, on_anchor, on_alias,
229
+ };
230
+
231
+ /* fused JSON→Ruby (json_ruby.c) — no vtable, beats JSON.parse */
232
+ VALUE yep_rb_parse_json(const char* p, size_t len);
233
+
234
+ static VALUE ctx_result(rb_ctx* c, YeptrisStatus st) {
235
+ if (st != YEPTRIS_OK || c->failed) {
236
+ if (st == YEPTRIS_ERROR_PARSE) {
237
+ rb_raise(rb_path2class("Yeptris::ParseError"), "native parse failed");
238
+ }
239
+ if (st == YEPTRIS_ERROR_MEMORY) {
240
+ rb_raise(rb_eNoMemError, "yeptris native");
241
+ }
242
+ rb_raise(rb_path2class("Yeptris::Error"), "native materialize failed (%d)", (int)st);
243
+ }
244
+ return c->root;
245
+ }
246
+
247
+ static VALUE native_load_json(VALUE self, VALUE input) {
248
+ (void)self;
249
+ StringValue(input);
250
+ return yep_rb_parse_json(RSTRING_PTR(input), (size_t)RSTRING_LEN(input));
251
+ }
252
+
253
+ static VALUE native_load(VALUE self, VALUE input, VALUE schema) {
254
+ (void)self;
255
+ StringValue(input);
256
+ int sch = YEPTRIS_SCHEMA_11_COMPAT;
257
+ if (!NIL_P(schema)) {
258
+ Check_Type(schema, T_SYMBOL);
259
+ if (rb_sym2id(schema) == rb_intern("core_12")) {
260
+ sch = YEPTRIS_SCHEMA_12_CORE;
261
+ }
262
+ }
263
+ rb_ctx c;
264
+ memset(&c, 0, sizeof(c));
265
+ c.root = Qnil;
266
+ c.pending_anchor = Qnil;
267
+ c.anchors = rb_hash_new();
268
+ VALUE already = rb_gc_disable();
269
+ YeptrisStatus st = yeptris_visit(RSTRING_PTR(input), (size_t)RSTRING_LEN(input),
270
+ (YeptrisSchema)sch, &k_vt, &c);
271
+ if (already == Qfalse) {
272
+ rb_gc_enable();
273
+ }
274
+ return ctx_result(&c, st);
275
+ }
276
+
277
+ /* load_stream: accumulate documents into an Array. */
278
+ typedef struct {
279
+ rb_ctx inner;
280
+ VALUE docs;
281
+ int in_doc;
282
+ } rb_stream_ctx;
283
+
284
+ static int stream_on_doc(void* ctx) {
285
+ rb_stream_ctx* s = (rb_stream_ctx*)ctx;
286
+ if (s->in_doc && !NIL_P(s->inner.root)) {
287
+ rb_ary_push(s->docs, s->inner.root);
288
+ }
289
+ s->inner.root = Qnil;
290
+ s->inner.sp = 0;
291
+ s->in_doc = 1;
292
+ return 0;
293
+ }
294
+
295
+ static VALUE native_load_stream(VALUE self, VALUE input, VALUE schema) {
296
+ (void)self;
297
+ StringValue(input);
298
+ int sch = YEPTRIS_SCHEMA_11_COMPAT;
299
+ if (!NIL_P(schema) && rb_sym2id(schema) == rb_intern("core_12")) {
300
+ sch = YEPTRIS_SCHEMA_12_CORE;
301
+ }
302
+ rb_stream_ctx s;
303
+ memset(&s, 0, sizeof(s));
304
+ s.inner.root = Qnil;
305
+ s.inner.pending_anchor = Qnil;
306
+ s.inner.anchors = rb_hash_new();
307
+ s.docs = rb_ary_new();
308
+ YeptrisVisitVTable vt = k_vt;
309
+ vt.on_doc = stream_on_doc;
310
+ /* trick: the ctx for scalar callbacks is &s.inner, but on_doc needs
311
+ * &s. Use a unified ctx — rebind all callbacks to take stream ctx
312
+ * by making inner the first field (already is). on_doc uses outer;
313
+ * others use inner via same pointer since inner is first field. */
314
+ YeptrisStatus st = yeptris_visit(RSTRING_PTR(input), (size_t)RSTRING_LEN(input),
315
+ (YeptrisSchema)sch, &vt, &s);
316
+ if (st == YEPTRIS_OK && !s.inner.failed) {
317
+ if (!NIL_P(s.inner.root) || s.in_doc) {
318
+ rb_ary_push(s.docs, s.inner.root);
319
+ }
320
+ return s.docs;
321
+ }
322
+ return ctx_result(&s.inner, st == YEPTRIS_OK ? YEPTRIS_ERROR_INTERNAL : st);
323
+ }
324
+
325
+ /* GC-strategy surface (TODO.restructure/34): ENV at load sets the
326
+ * default; the setter re-picks at runtime so the CI referee can A/B
327
+ * in-process. Symbols: :disable, :none, :start. */
328
+ extern int yep_rb_gc_mode(void);
329
+ extern void yep_rb_set_gc_mode(int mode);
330
+ extern int yep_rb_ins_mode(void);
331
+ extern void yep_rb_set_ins_mode(int mode);
332
+ extern int yep_rb_cache_mode(void);
333
+ extern void yep_rb_set_cache_mode(int mode);
334
+ extern int yep_rb_shape_mode(void);
335
+ extern void yep_rb_set_shape_mode(int mode);
336
+ extern double yep_rb_scan_time(const char* p, size_t len, int n);
337
+
338
+ static VALUE native_gc_mode(VALUE self) {
339
+ (void)self;
340
+ switch (yep_rb_gc_mode()) {
341
+ case 1: return ID2SYM(rb_intern("none"));
342
+ case 2: return ID2SYM(rb_intern("start"));
343
+ default: return ID2SYM(rb_intern("disable"));
344
+ }
345
+ }
346
+
347
+ static VALUE native_ins_mode(VALUE self) {
348
+ (void)self;
349
+ return yep_rb_ins_mode() == 1 ? ID2SYM(rb_intern("aset")) : ID2SYM(rb_intern("bulk"));
350
+ }
351
+
352
+ static VALUE native_ins_mode_set(VALUE self, VALUE mode) {
353
+ (void)self;
354
+ Check_Type(mode, T_SYMBOL);
355
+ ID id = rb_sym2id(mode);
356
+ if (id == rb_intern("bulk")) yep_rb_set_ins_mode(0);
357
+ else if (id == rb_intern("aset")) yep_rb_set_ins_mode(1);
358
+ else rb_raise(rb_eArgError, "ins_mode must be :bulk or :aset");
359
+ return mode;
360
+ }
361
+
362
+ static VALUE native_cache_mode(VALUE self) {
363
+ (void)self;
364
+ return yep_rb_cache_mode() == 1 ? ID2SYM(rb_intern("off")) : ID2SYM(rb_intern("on"));
365
+ }
366
+
367
+ static VALUE native_cache_mode_set(VALUE self, VALUE mode) {
368
+ (void)self;
369
+ Check_Type(mode, T_SYMBOL);
370
+ ID id = rb_sym2id(mode);
371
+ if (id == rb_intern("on")) yep_rb_set_cache_mode(0);
372
+ else if (id == rb_intern("off")) yep_rb_set_cache_mode(1);
373
+ else rb_raise(rb_eArgError, "cache_mode must be :on or :off");
374
+ return mode;
375
+ }
376
+
377
+ static VALUE native_scan_time(VALUE self, VALUE input, VALUE count) {
378
+ (void)self;
379
+ StringValue(input);
380
+ int n = NUM2INT(count);
381
+ double secs = yep_rb_scan_time(RSTRING_PTR(input), (size_t)RSTRING_LEN(input), n);
382
+ return DBL2NUM(secs / (double)n);
383
+ }
384
+
385
+ static VALUE native_shape_mode(VALUE self) {
386
+ (void)self;
387
+ return yep_rb_shape_mode() == 1 ? ID2SYM(rb_intern("natural")) : ID2SYM(rb_intern("pre"));
388
+ }
389
+
390
+ static VALUE native_shape_mode_set(VALUE self, VALUE mode) {
391
+ (void)self;
392
+ Check_Type(mode, T_SYMBOL);
393
+ ID id = rb_sym2id(mode);
394
+ if (id == rb_intern("pre")) yep_rb_set_shape_mode(0);
395
+ else if (id == rb_intern("natural")) yep_rb_set_shape_mode(1);
396
+ else rb_raise(rb_eArgError, "shape_mode must be :pre or :natural");
397
+ return mode;
398
+ }
399
+
400
+ static VALUE native_gc_mode_set(VALUE self, VALUE mode) {
401
+ (void)self;
402
+ Check_Type(mode, T_SYMBOL);
403
+ ID id = rb_sym2id(mode);
404
+ if (id == rb_intern("disable")) yep_rb_set_gc_mode(0);
405
+ else if (id == rb_intern("none")) yep_rb_set_gc_mode(1);
406
+ else if (id == rb_intern("start")) yep_rb_set_gc_mode(2);
407
+ else rb_raise(rb_eArgError, "gc_mode must be :disable, :none, or :start");
408
+ return mode;
409
+ }
410
+
411
+ RUBY_FUNC_EXPORTED void Init_native(void) {
412
+ utf8_enc = rb_utf8_encoding();
413
+ VALUE mYep = rb_define_module("Yeptris");
414
+ VALUE mNat = rb_define_module_under(mYep, "Native");
415
+ rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
416
+ rb_define_singleton_method(mNat, "load", native_load, 2);
417
+ rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
418
+ rb_define_singleton_method(mNat, "gc_mode", native_gc_mode, 0);
419
+ rb_define_singleton_method(mNat, "gc_mode=", native_gc_mode_set, 1);
420
+ rb_define_singleton_method(mNat, "ins_mode", native_ins_mode, 0);
421
+ rb_define_singleton_method(mNat, "ins_mode=", native_ins_mode_set, 1);
422
+ rb_define_singleton_method(mNat, "cache_mode", native_cache_mode, 0);
423
+ rb_define_singleton_method(mNat, "cache_mode=", native_cache_mode_set, 1);
424
+ rb_define_singleton_method(mNat, "shape_mode", native_shape_mode, 0);
425
+ rb_define_singleton_method(mNat, "shape_mode=", native_shape_mode_set, 1);
426
+ rb_define_singleton_method(mNat, "scan_time", native_scan_time, 2);
427
+ rb_define_const(mNat, "AVAILABLE", Qtrue);
428
+ const char* env = getenv("YEPTRIS_NATIVE_GC");
429
+ if (env != NULL && strcmp(env, "none") == 0) yep_rb_set_gc_mode(1);
430
+ else if (env != NULL && strcmp(env, "start") == 0) yep_rb_set_gc_mode(2);
431
+ else if (env != NULL && strcmp(env, "disable") == 0) yep_rb_set_gc_mode(0);
432
+ const char* ins = getenv("YEPTRIS_NATIVE_INSERT");
433
+ if (ins != NULL && strcmp(ins, "aset") == 0) yep_rb_set_ins_mode(1);
434
+ const char* cache = getenv("YEPTRIS_NATIVE_CACHE");
435
+ if (cache != NULL && strcmp(cache, "off") == 0) yep_rb_set_cache_mode(1);
436
+ const char* shape = getenv("YEPTRIS_NATIVE_SHAPE");
437
+ if (shape != NULL && strcmp(shape, "natural") == 0) yep_rb_set_shape_mode(1);
438
+ }
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Yeptris::Document
4
+ attr_reader :c_ptr
5
+
6
+ # Shared between the instance and its GC finalizer (Procs close over
7
+ # variables by reference) — the leptris-ruby double-free fix: the
8
+ # explicit free path and the finalizer both flip the same flag.
9
+ Freed = Struct.new(:state) # :alive | :freed
10
+
11
+ def initialize(c_ptr = nil, freed = Freed.new(:alive))
12
+ @c_ptr = c_ptr
13
+ @freed = freed
14
+ @readonly = false
15
+ # Strong wrapper cache keyed on the C node address: the same C node
16
+ # always yields the same Ruby object (identity for aliases/eql?),
17
+ # cleared at free — no stale entries, no GC-race weak maps.
18
+ @wrapper_cache = {}
19
+ ObjectSpace.define_finalizer(self, self.class.finalize(c_ptr, freed)) unless c_ptr.null?
20
+ end
21
+
22
+ def self.parse(yaml, schema: :core_12, max_depth: 0)
23
+ yaml = Yeptris.read_input(yaml)
24
+ yaml = yaml.to_s
25
+ doc =
26
+ if schema == :core_12 && max_depth.zero?
27
+ Yeptris::FFI.yeptris_parse(yaml, yaml.bytesize, nil)
28
+ else
29
+ opts = Yeptris::FFI::ParseOptions.new
30
+ opts[:schema] = schema == :compat_11 ? Yeptris::FFI::SCHEMA_11_COMPAT : Yeptris::FFI::SCHEMA_12_CORE
31
+ opts[:max_depth] = max_depth
32
+ Yeptris::FFI.yeptris_parse_ex(yaml, yaml.bytesize, opts, nil)
33
+ end
34
+ if doc.null?
35
+ # the status out-param is skipped: the thread-local error
36
+ # channel carries the failure detail (measurable on small docs)
37
+ raise Yeptris::ParseError,
38
+ "parse failed: #{Yeptris::FFI.last_error_message}"
39
+ end
40
+ wrap(doc)
41
+ end
42
+
43
+ def self.parse_json(json)
44
+ json = Yeptris.read_input(json)
45
+ json = json.to_s
46
+ doc = Yeptris::FFI.yeptris_parse_json(json, json.bytesize, nil)
47
+ raise Yeptris::ParseError,
48
+ "json parse failed: #{Yeptris::FFI.last_error_message}" if doc.null?
49
+
50
+ wrap(doc)
51
+ end
52
+
53
+ # An empty document for from-scratch construction (TODO.impl/11 p3).
54
+ def self.create
55
+ doc = Yeptris::FFI.yeptris_document_new
56
+ raise Yeptris::Error, "yeptris_document_new failed" if doc.null?
57
+
58
+ wrap(doc)
59
+ end
60
+
61
+ # @api private
62
+ def self.wrap(c_ptr)
63
+ new(c_ptr)
64
+ end
65
+
66
+ def ensure_alive!
67
+ raise Yeptris::FreedError, "document is freed" if @freed.state == :freed
68
+ end
69
+
70
+ def free
71
+ return if @freed.state == :freed
72
+
73
+ @freed.state = :freed
74
+ @wrapper_cache.clear
75
+ Yeptris::FFI.yeptris_document_free(@c_ptr)
76
+ end
77
+
78
+ def freed?
79
+ @freed.state == :freed
80
+ end
81
+
82
+ def readonly!
83
+ @readonly = true
84
+ self
85
+ end
86
+
87
+ def readonly?
88
+ @readonly
89
+ end
90
+
91
+ # @api private — memo table for readonly materialization: node ids
92
+ # that already produced their Ruby object keep it (leptris pattern:
93
+ # readonly documents never change, so the memo is forever valid).
94
+ def readonly_memo
95
+ @readonly_memo ||= {}
96
+ end
97
+
98
+ # @api private — the single Node construction path. Query handles
99
+ # are transient C allocations; the wrapper cache is keyed on the
100
+ # STABLE node id (yeptris_node_id), so the same node always yields
101
+ # the same Ruby object no matter which query produced the handle.
102
+ def wrap_node(c_ptr)
103
+ ensure_alive!
104
+ return nil if c_ptr.null?
105
+
106
+ id = Yeptris::FFI.yeptris_node_id(c_ptr)
107
+ @wrapper_cache[id] ||= Yeptris::Node.new(c_ptr, self)
108
+ end
109
+
110
+ def document_count
111
+ ensure_alive!
112
+ Yeptris::FFI.yeptris_document_count(@c_ptr)
113
+ end
114
+
115
+ # Root node of stream document i (0-based).
116
+ def root(index = 0)
117
+ ensure_alive!
118
+ wrap_node(Yeptris::FFI.yeptris_document_root(@c_ptr, index))
119
+ end
120
+
121
+ # Bulk build (TODO.impl/15 phase D): one call raises the whole
122
+ # tree from a flat entry array + blob (see YAML::BulkBuilder).
123
+ def build_entries(entries, count, blob, blob_len)
124
+ ensure_alive!
125
+ Yeptris::FFI.yeptris_document_build(@c_ptr, entries, count, blob, blob_len)
126
+ end
127
+
128
+ def serialize(canonical: false, best_width: 0)
129
+ ensure_alive!
130
+ len = ::FFI::MemoryPointer.new(:uint64)
131
+ ptr =
132
+ if canonical || best_width.positive?
133
+ opts = Yeptris::FFI::EmitOptions.new
134
+ opts[:size] = Yeptris::FFI::EmitOptions.size
135
+ opts[:canonical] = canonical ? 1 : 0
136
+ opts[:best_width] = best_width
137
+ Yeptris::FFI.yeptris_serialize_ex(@c_ptr, opts, len)
138
+ else
139
+ Yeptris::FFI.yeptris_serialize(@c_ptr, len)
140
+ end
141
+ Yeptris::FFI::Owned.string(ptr, len)
142
+ end
143
+
144
+ def serialize_json
145
+ ensure_alive!
146
+ len = ::FFI::MemoryPointer.new(:uint64)
147
+ Yeptris::FFI::Owned.string(Yeptris::FFI.yeptris_serialize_json(@c_ptr, len), len)
148
+ end
149
+
150
+ def to_s
151
+ serialize
152
+ end
153
+
154
+ # The Ruby object graph of stream document i (Psych-compatible
155
+ # materialization; alias identity preserved via the memo).
156
+ def to_ruby(index = 0)
157
+ ensure_alive!
158
+ r = root(index)
159
+ r.nil? ? nil : r.to_ruby
160
+ end
161
+
162
+ # ---- construction conveniences (TODO.impl/11 phase 3) ----
163
+
164
+ def new_mapping
165
+ ensure_alive!
166
+ wrap_node(Yeptris::FFI.yeptris_node_new_mapping(@c_ptr)) or
167
+ raise Yeptris::Error, "yeptris_node_new_mapping failed"
168
+ end
169
+
170
+ def new_sequence
171
+ ensure_alive!
172
+ wrap_node(Yeptris::FFI.yeptris_node_new_sequence(@c_ptr)) or
173
+ raise Yeptris::Error, "yeptris_node_new_sequence failed"
174
+ end
175
+
176
+ # style: :plain / :single_quoted / :double_quoted / :literal / :folded.
177
+ # The value is copied into the document (nothing is borrowed).
178
+ def new_scalar(text, style = :plain)
179
+ ensure_alive!
180
+ code = Yeptris::Node::STYLES.key(style) or
181
+ raise ArgumentError, "unknown scalar style #{style.inspect}"
182
+ text = text.to_s
183
+ n = Yeptris::FFI.yeptris_node_new_scalar(@c_ptr, text, text.bytesize, code)
184
+ wrap_node(n) or raise Yeptris::Error, "yeptris_node_new_scalar failed"
185
+ end
186
+
187
+ # An alias node: display name + the target it resolves to.
188
+ def new_alias(target, name)
189
+ ensure_alive!
190
+ n = Yeptris::FFI.yeptris_node_new_alias(@c_ptr, target.c_ptr, name, name.bytesize)
191
+ wrap_node(n) or raise Yeptris::Error, "yeptris_node_new_alias failed"
192
+ end
193
+
194
+ def set_root(node)
195
+ ensure_alive!
196
+ rc = Yeptris::FFI.yeptris_document_set_root(@c_ptr, node.c_ptr)
197
+ Yeptris::FFI.check_status(rc, "yeptris_document_set_root")
198
+ self
199
+ end
200
+
201
+ # GC safety net: an explicit #free already ran is fine; a miss here
202
+ # frees C memory that would otherwise leak.
203
+ def self.finalize(c_ptr, freed)
204
+ proc do
205
+ Yeptris::FFI.yeptris_document_free(c_ptr) if freed.state == :alive
206
+ freed.state = :freed
207
+ end
208
+ end
209
+ end