schemurai 2.2.2 → 2.3.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.
@@ -0,0 +1,639 @@
1
+ #include <ruby.h>
2
+ #include <ruby/encoding.h>
3
+ #include <ruby/st.h>
4
+ #include <stdint.h>
5
+ #include <stddef.h>
6
+ #include <math.h>
7
+ #include "protocol.h"
8
+
9
+ static ID id_resolve, id_schemurai, id_resolution_error;
10
+ static ID id_root, id_compile, id_graph, id_dynamic_anchor;
11
+
12
+ /* The C layer adapts CRuby APIs to Rust's opaque VALUEs. */
13
+ #ifdef HAVE_RB_HASH_STLIKE_FOREACH
14
+ /* Exported internal API; extconf checks the symbol and provides a public-API
15
+ * fallback. Its callback below only reads handles under the GVL. */
16
+ extern int rb_hash_stlike_foreach(VALUE, st_foreach_callback_func *, st_data_t);
17
+ #endif
18
+ typedef struct { void *engine; const void *roots; VALUE program; int compiling; } holder;
19
+ extern void *sn_new(void);
20
+ extern int sn_compile(void *, VALUE, VALUE, int, int, VALUE *);
21
+ extern int sn_valid(const void *, VALUE);
22
+ extern void sn_free(void *);
23
+ extern const void *sn_roots(const void *);
24
+ extern void sn_roots_mark(const void *);
25
+ extern size_t sn_roots_size(const void *);
26
+ extern size_t sn_engine_header_size(void);
27
+ extern size_t sn_size(const void *);
28
+
29
+ static void mark(void *ptr) {
30
+ holder *h = ptr;
31
+ rb_gc_mark(h->program);
32
+ if (h->roots) sn_roots_mark(h->roots);
33
+ }
34
+ static void release(void *ptr) {
35
+ holder *h = ptr;
36
+ if (h->engine) sn_free(h->engine);
37
+ xfree(h);
38
+ }
39
+ static size_t size(const void *ptr) {
40
+ const holder *h = ptr;
41
+ return sizeof(*h) + (h->roots ? sn_roots_size(h->roots) : 0)
42
+ + (h->engine ? (h->compiling ? sn_engine_header_size() : sn_size(h->engine)) : 0);
43
+ }
44
+ static const rb_data_type_t engine_type = {
45
+ "Schemurai::Native::Program", {mark, release, size, NULL, {NULL}},
46
+ NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
47
+ };
48
+ static VALUE allocate(VALUE klass) {
49
+ holder *h;
50
+ VALUE obj = TypedData_Make_Struct(klass, holder, &engine_type, h);
51
+ h->engine = NULL;
52
+ h->roots = NULL;
53
+ h->compiling = 0;
54
+ h->program = Qnil;
55
+ return obj;
56
+ }
57
+ static VALUE initialize(VALUE self, VALUE program, VALUE compiler, VALUE format, VALUE content) {
58
+ holder *h;
59
+ TypedData_Get_Struct(self, holder, &engine_type, h);
60
+ if (h->engine) rb_raise(rb_eRuntimeError, "native program is already initialized");
61
+ h->program = program;
62
+ h->engine = sn_new();
63
+ h->roots = sn_roots(h->engine);
64
+ h->compiling = 1;
65
+ VALUE error = Qnil;
66
+ int status = sn_compile(h->engine, program, compiler, RTEST(format), RTEST(content), &error);
67
+ h->compiling = 0;
68
+ if (status < 0) {
69
+ sn_free(h->engine);
70
+ h->engine = NULL;
71
+ h->roots = NULL;
72
+ if (status == SN_STATUS_EXCEPTION) rb_exc_raise(error);
73
+ rb_raise(rb_eRuntimeError, "native compiler panicked");
74
+ }
75
+ return self;
76
+ }
77
+ static VALUE valid(VALUE self, VALUE instance) {
78
+ holder *h;
79
+ TypedData_Get_Struct(self, holder, &engine_type, h);
80
+ if (!h->engine) rb_raise(rb_eRuntimeError, "uninitialized native program");
81
+ if (h->compiling) rb_raise(rb_eRuntimeError, "native program is still compiling");
82
+ int status = sn_valid(h->engine, instance);
83
+ RB_GC_GUARD(instance);
84
+ if (status < 0) rb_raise(rb_eRuntimeError, "native evaluator panicked");
85
+ return status == SN_VERDICT_UNSUPPORTED ? Qnil : (status ? Qtrue : Qfalse);
86
+ }
87
+ void sn_pin(VALUE value) { rb_gc_mark(value); }
88
+ VALUE sn_nil(void) { return Qnil; }
89
+ VALUE sn_undefined(void) { return Qundef; }
90
+ static int ordinary_key(VALUE key, VALUE value, VALUE context) {
91
+ int *ordinary = (int *)context;
92
+ if (!RB_TYPE_P(key, T_STRING) || CLASS_OF(key) != rb_cString) {
93
+ *ordinary = 0;
94
+ return ST_STOP;
95
+ }
96
+ return ST_CONTINUE;
97
+ }
98
+ int sn_shallow_kind(VALUE value) {
99
+ if (NIL_P(value)) return SN_TYPE_NULL;
100
+ if (value == Qtrue || value == Qfalse) return SN_TYPE_BOOLEAN;
101
+ if (RB_TYPE_P(value, T_HASH)) {
102
+ if (CLASS_OF(value) != rb_cHash) return SN_TYPE_UNSUPPORTED;
103
+ return SN_TYPE_OBJECT;
104
+ }
105
+ if (RB_TYPE_P(value, T_ARRAY)) return CLASS_OF(value) == rb_cArray ? SN_TYPE_ARRAY : SN_TYPE_UNSUPPORTED;
106
+ if (RB_INTEGER_TYPE_P(value)) return SN_TYPE_NUMBER | SN_TYPE_INTEGER;
107
+ if (RB_TYPE_P(value, T_FLOAT)) {
108
+ double n = RFLOAT_VALUE(value);
109
+ return SN_TYPE_NUMBER | ((isfinite(n) && trunc(n) == n) ? SN_TYPE_INTEGER : 0);
110
+ }
111
+ if (RB_TYPE_P(value, T_STRING)) return CLASS_OF(value) == rb_cString ? SN_TYPE_STRING : SN_TYPE_UNSUPPORTED;
112
+ return SN_TYPE_UNSUPPORTED; /* Out-of-domain objects are handled by the Ruby VM. */
113
+ }
114
+ int sn_kind(VALUE value) {
115
+ int kind = sn_shallow_kind(value);
116
+ if (kind != SN_TYPE_OBJECT) return kind;
117
+ /* Lookup paths check every key before calling Ruby's hash API. A record
118
+ * walk instead checks keys as it visits them and uses native lookups. */
119
+ int ordinary = 1;
120
+ #ifdef HAVE_RB_HASH_STLIKE_FOREACH
121
+ /* This guard only checks classes; it cannot mutate or invoke Ruby. */
122
+ rb_hash_stlike_foreach(value, ordinary_key, (st_data_t)&ordinary);
123
+ #else
124
+ rb_hash_foreach(value, ordinary_key, (VALUE)&ordinary);
125
+ #endif
126
+ return ordinary ? SN_TYPE_OBJECT : SN_TYPE_UNSUPPORTED;
127
+ }
128
+ /* Classification and borrowed storage for structural fingerprints. Hash
129
+ * keys are checked by the iterator; no preliminary key walk occurs here.
130
+ * The caller keeps the GVL and must not allocate Ruby objects or call Ruby.
131
+ * Only Array/String initialize data and length; Hash/Boolean initialize
132
+ * length; Float (kind SN_TYPE_NUMBER) initializes number. Other kinds write no outputs. */
133
+ int sn_fingerprint_view(VALUE value, const void **data, size_t *length, double *number) {
134
+ if (RB_TYPE_P(value, T_ARRAY)) {
135
+ if (CLASS_OF(value) != rb_cArray) return SN_TYPE_UNSUPPORTED;
136
+ *data = RARRAY_CONST_PTR(value);
137
+ *length = RARRAY_LEN(value);
138
+ return SN_TYPE_ARRAY;
139
+ }
140
+ if (RB_TYPE_P(value, T_STRING)) {
141
+ if (CLASS_OF(value) != rb_cString) return SN_TYPE_UNSUPPORTED;
142
+ *data = RSTRING_PTR(value);
143
+ *length = RSTRING_LEN(value);
144
+ return SN_TYPE_STRING;
145
+ }
146
+ if (RB_TYPE_P(value, T_HASH)) {
147
+ if (CLASS_OF(value) != rb_cHash) return SN_TYPE_UNSUPPORTED;
148
+ *length = RHASH_SIZE(value);
149
+ return SN_TYPE_OBJECT;
150
+ }
151
+ if (RB_TYPE_P(value, T_FLOAT)) {
152
+ *number = RFLOAT_VALUE(value);
153
+ return SN_TYPE_NUMBER;
154
+ }
155
+ int kind = sn_shallow_kind(value);
156
+ if (kind == SN_TYPE_BOOLEAN) {
157
+ *length = value == Qtrue;
158
+ }
159
+ return kind;
160
+ }
161
+ struct small_hash { VALUE *entries; int count; };
162
+ static int small_entry(VALUE key, VALUE value, VALUE context) {
163
+ struct small_hash *h = (struct small_hash *)context;
164
+ if (!RB_TYPE_P(key, T_STRING) || CLASS_OF(key) != rb_cString) {
165
+ h->count = -1;
166
+ return ST_STOP;
167
+ }
168
+ h->entries[2 * h->count] = key;
169
+ h->entries[2 * h->count + 1] = value;
170
+ h->count++;
171
+ return ST_CONTINUE;
172
+ }
173
+ /* Only opaque borrowed handles go into a fixed stack buffer. No Ruby object,
174
+ * instance tree, string, or number is copied or converted for Rust. */
175
+ int sn_small_hash(VALUE value, VALUE *entries) {
176
+ if (RHASH_SIZE(value) > 8) return SN_SMALL_HASH_TOO_LARGE;
177
+ struct small_hash h = {entries, 0};
178
+ #ifdef HAVE_RB_HASH_STLIKE_FOREACH
179
+ rb_hash_stlike_foreach(value, small_entry, (st_data_t)&h);
180
+ #else
181
+ rb_hash_foreach(value, small_entry, (VALUE)&h);
182
+ #endif
183
+ if (h.count < 0) return h.count;
184
+ #ifdef SCHEMURAI_AR_HASH_PROOF
185
+ /* Build-time verified CRuby 3.4/4.0 hint: identity Hashes use ST tables.
186
+ * Only the public object flags are read; no private table is dereferenced.
187
+ * Bit 4 tags ordinary semantics, independently of the initialized count. */
188
+ if (!RB_FL_TEST_RAW(value, RUBY_FL_USER3)) return h.count | SN_SMALL_HASH_ORDINARY;
189
+ #endif
190
+ return h.count;
191
+ }
192
+ int sn_true(VALUE value) { return value == Qtrue; }
193
+ VALUE sn_fixnum_flag(void) {
194
+ /* Enable direct immediate decoding only for the header encoding Rust
195
+ * understands. Zero disables its hints and retains the public adapters. */
196
+ if (sizeof(VALUE) <= sizeof(int64_t) && sizeof(VALUE) == sizeof(intptr_t)
197
+ && RUBY_FIXNUM_FLAG == 1 && INT2FIX(0) == 1 && INT2FIX(1) == 3
198
+ && INT2FIX(-1) == (VALUE)-1)
199
+ return RUBY_FIXNUM_FLAG;
200
+ return 0;
201
+ }
202
+ const VALUE *sn_array_data(VALUE value) { return RARRAY_CONST_PTR(value); }
203
+ int sn_fixnum(VALUE value) { return FIXNUM_P(value); }
204
+ int64_t sn_integer(VALUE value) { return FIX2LONG(value); }
205
+ /* Arithmetic operands only: no Ruby allocation, coercion, or instance tree copy.
206
+ * Callers check INTEGER_TYPE before using this bounded scalar adapter. */
207
+ int sn_integer128(VALUE value, uint64_t *words) {
208
+ return rb_integer_pack(value, words, 2, sizeof(uint64_t), 0,
209
+ INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER);
210
+ }
211
+ /* Runtime callers already checked ordinary Integer. Magnitudes above 128 bits
212
+ * lie outside every bounded fraction limit, so bounds need their sign only.
213
+ * Read public metadata instead of exporting truncated magnitude words. */
214
+ int sn_integer_bounds128(VALUE value, uint64_t *words) {
215
+ if (FIXNUM_P(value)) {
216
+ int64_t n = FIX2LONG(value);
217
+ words[0] = n < 0 ? 0 - (uint64_t)n : (uint64_t)n;
218
+ words[1] = 0;
219
+ return (n > 0) - (n < 0);
220
+ }
221
+ if (RB_TYPE_P(value, T_BIGNUM) && rb_absint_size(value, NULL) > 16)
222
+ return rb_big_sign(value) ? 2 : -2;
223
+ return sn_integer128(value, words);
224
+ }
225
+ /* One ordinary-kind check plus Integer metadata or a Float scalar. Return 3 for
226
+ * Float/non-number input. Float initializes floating; Integer initializes words
227
+ * only when the returned sign has absolute value at most one. All paths set kind. */
228
+ int sn_number_bounds_view(VALUE value, uint64_t *words, int *kind, double *floating, int required) {
229
+ if (RB_TYPE_P(value, T_FLOAT)) {
230
+ double n = RFLOAT_VALUE(value);
231
+ *floating = n;
232
+ *kind = SN_TYPE_NUMBER;
233
+ /* Integer membership is only needed when the required mask excludes
234
+ * general numbers. Keep NaN/Infinity outside the Integer domain. */
235
+ if ((required & SN_TYPE_INTEGER) && !(required & SN_TYPE_NUMBER) && isfinite(n) && trunc(n) == n)
236
+ *kind |= SN_TYPE_INTEGER;
237
+ return SN_ADAPTER_UNSUPPORTED;
238
+ }
239
+ if (RB_TYPE_P(value, T_BIGNUM)) {
240
+ *kind = SN_TYPE_INTEGRAL_NUMBER;
241
+ if (rb_absint_size(value, NULL) > 16)
242
+ return rb_big_sign(value) ? 2 : -2;
243
+ return sn_integer128(value, words);
244
+ }
245
+ if (FIXNUM_P(value)) {
246
+ *kind = SN_TYPE_INTEGRAL_NUMBER;
247
+ return sn_integer_bounds128(value, words);
248
+ }
249
+ *kind = sn_kind(value);
250
+ return SN_ADAPTER_UNSUPPORTED;
251
+ }
252
+ /* Restricted to ordinary integers: rb_big_eql compares existing digits and
253
+ * never enters the allocating/coercing integer-versus-Float comparison path. */
254
+ int sn_big_equal(VALUE big, VALUE other) { return RTEST(rb_big_eql(big, other)); }
255
+ /* Compare only existing Big/Big digits. The Fixnum branch of rb_big_cmp can
256
+ * normalize its left operand, and Float/coercion branches can allocate. */
257
+ int sn_big_order(VALUE left, VALUE right) {
258
+ if (!RB_TYPE_P(left, T_BIGNUM) || !RB_TYPE_P(right, T_BIGNUM)) return SN_ADAPTER_UNSUPPORTED;
259
+ return FIX2INT(rb_big_cmp(left, right));
260
+ }
261
+ /* The bound is an ordinary Integer outside i128. A Fixnum is therefore
262
+ * ordered by the bound's sign. Only Big/Big reaches rb_big_cmp: its Fixnum
263
+ * branch can resize the left operand, and its Float branch allocates. */
264
+ int sn_wide_integer_compare(VALUE value, VALUE bound) {
265
+ if (FIXNUM_P(value)) return rb_big_sign(bound) ? -1 : 1;
266
+ return FIX2INT(rb_big_cmp(value, bound));
267
+ }
268
+ size_t sn_integer_bytes(VALUE value) { return rb_absint_size(value, NULL); }
269
+ int sn_integer1024(VALUE value, uint64_t *words) {
270
+ return rb_integer_pack(value, words, 16, sizeof(uint64_t), 0,
271
+ INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER);
272
+ }
273
+ int sn_is_bignum(VALUE value) { return RB_TYPE_P(value, T_BIGNUM); }
274
+ /* CRuby's rb_hash dispatches T_BIGNUM directly to its digit hash and returns
275
+ * a Fixnum: no Ruby method dispatch or allocation. Never pass other types,
276
+ * whose hash implementation may call Ruby while the array view is borrowed. */
277
+ int sn_big_hash(VALUE value, uint64_t *hash) {
278
+ if (!RB_TYPE_P(value, T_BIGNUM)) return 0;
279
+ *hash = (uint64_t)FIX2LONG(rb_hash(value));
280
+ return 1;
281
+ }
282
+ /* Return low 32-bit magnitude metadata plus sign in bit 32. Bit 33 rejects
283
+ * other kinds before public integer packing. Overflow retains the low word;
284
+ * exact original-handle comparisons and wider retries decide uniqueness. */
285
+ uint64_t sn_big_low_word(VALUE value) {
286
+ if (!RB_TYPE_P(value, T_BIGNUM)) return UINT64_C(1) << 33;
287
+ uint32_t low;
288
+ int sign = rb_integer_pack(value, &low, 1, sizeof(low), 0,
289
+ INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER);
290
+ return (uint64_t)low | ((uint64_t)(sign < 0) << 32);
291
+ }
292
+ int sn_big_low64_word(VALUE value, uint64_t *word) {
293
+ if (!RB_TYPE_P(value, T_BIGNUM)) return SN_ADAPTER_UNSUPPORTED;
294
+ return rb_integer_pack(value, word, 1, sizeof(uint64_t), 0,
295
+ INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER);
296
+ }
297
+ int sn_big256(VALUE value, uint64_t *words) {
298
+ /* 3 means the checked hint did not apply; no coercion or allocation. */
299
+ if (!RB_TYPE_P(value, T_BIGNUM)) return SN_ADAPTER_UNSUPPORTED;
300
+ return rb_integer_pack(value, words, 4, sizeof(uint64_t), 0,
301
+ INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER);
302
+ }
303
+ int sn_is_float(VALUE value) { return RB_TYPE_P(value, T_FLOAT); }
304
+ int sn_is_rational(VALUE value) { return RB_TYPE_P(value, T_RATIONAL); }
305
+ double sn_float(VALUE value) { return RFLOAT_VALUE(value); }
306
+ static int utf8_index, ascii_index, binary_index;
307
+ static size_t string_length(VALUE value) {
308
+ int encoding = RB_ENCODING_GET(value);
309
+ if (encoding == binary_index
310
+ || ((encoding == utf8_index || encoding == ascii_index)
311
+ && RB_ENC_CODERANGE(value) == RUBY_ENC_CODERANGE_7BIT))
312
+ return RSTRING_LEN(value);
313
+ return rb_str_strlen(value);
314
+ }
315
+ size_t sn_len(VALUE value) {
316
+ if (RB_TYPE_P(value, T_ARRAY)) return RARRAY_LEN(value);
317
+ if (RB_TYPE_P(value, T_HASH)) return RHASH_SIZE(value);
318
+ return string_length(value);
319
+ }
320
+ VALUE sn_entry(VALUE array, size_t index) { return rb_ary_entry(array, index); }
321
+ VALUE sn_lookup(VALUE hash, VALUE key) { return rb_hash_lookup2(hash, key, Qundef); }
322
+ int sn_missing(VALUE value) { return value == Qundef; }
323
+ int sn_same(VALUE a, VALUE b) { return a == b; }
324
+ int sn_same_class(VALUE a, VALUE b) { return CLASS_OF(a) == CLASS_OF(b); }
325
+ int sn_string_equal(VALUE a, VALUE b) { return rb_str_cmp(a, b) == 0; }
326
+
327
+ struct access { VALUE object; const char *name; };
328
+ static VALUE get_field(VALUE arg) {
329
+ struct access *a = (struct access *)arg;
330
+ return rb_funcall(a->object, rb_intern_const(a->name), 0);
331
+ }
332
+ /* Rust supplies a fixed, NUL-terminated field name from a static byte string. */
333
+ int sn_field(VALUE object, const char *name, VALUE *value) {
334
+ struct access a = {object, name};
335
+ int state = 0;
336
+ *value = rb_protect(get_field, (VALUE)&a, &state);
337
+ if (state) { *value = rb_errinfo(); rb_set_errinfo(Qnil); }
338
+ return state;
339
+ }
340
+ static struct { ID id; const char *symbol; int opcode; } opcodes[] = {
341
+ #define SN_OPCODE(name, symbol, value) {0, #symbol, SN_OP_##name},
342
+ #define SN_CONSTANT(name, value)
343
+ #include "protocol.def"
344
+ #undef SN_OPCODE
345
+ #undef SN_CONSTANT
346
+ };
347
+ int sn_opcode(VALUE symbol) {
348
+ ID id = SYM2ID(symbol);
349
+ for (size_t i = 0; i < sizeof(opcodes) / sizeof(opcodes[0]); i++)
350
+ if (id == opcodes[i].id) return opcodes[i].opcode;
351
+ return SN_OPCODE_UNKNOWN;
352
+ }
353
+ /* Copy only the schema map's entry handles; instances are never copied. */
354
+ static int append_pair(VALUE key, VALUE value, VALUE array) {
355
+ rb_ary_push(array, key);
356
+ rb_ary_push(array, value);
357
+ return ST_CONTINUE;
358
+ }
359
+ static VALUE pairs(VALUE hash) {
360
+ VALUE array = rb_ary_new();
361
+ rb_hash_foreach(hash, append_pair, array);
362
+ return array;
363
+ }
364
+ int sn_pairs(VALUE hash, VALUE *value) {
365
+ int state = 0;
366
+ *value = rb_protect(pairs, hash, &state);
367
+ if (state) { *value = rb_errinfo(); rb_set_errinfo(Qnil); }
368
+ return state;
369
+ }
370
+ static VALUE key_copy(VALUE key) { return rb_str_dup(key); }
371
+ int sn_key_copy(VALUE key, VALUE *value) {
372
+ int state = 0;
373
+ *value = rb_protect(key_copy, key, &state);
374
+ if (state) { *value = rb_errinfo(); rb_set_errinfo(Qnil); }
375
+ return state;
376
+ }
377
+ /* Output records are built only after Rust traversal has returned. The report
378
+ * is owned by TypedData so exceptions and GC cannot leak it or move its VALUEs. */
379
+ typedef struct {
380
+ const unsigned char *keyword;
381
+ size_t keyword_len;
382
+ const unsigned char *instance_path;
383
+ size_t instance_len;
384
+ const unsigned char *schema_path;
385
+ size_t schema_len;
386
+ int opcode;
387
+ VALUE operand, instance;
388
+ size_t detail;
389
+ } diagnostic_record;
390
+ typedef struct { void *report; } report_holder;
391
+ extern void *sn_diagnose(const void *, VALUE, int *);
392
+ extern size_t sn_report_len(const void *);
393
+ extern size_t sn_report_size(const void *);
394
+ extern void sn_report_record(const void *, size_t, diagnostic_record *);
395
+ extern void sn_report_mark(const void *);
396
+ extern void sn_report_free(void *);
397
+ static void report_mark(void *ptr) {
398
+ report_holder *h = ptr;
399
+ if (h->report) sn_report_mark(h->report);
400
+ }
401
+ static void report_release(void *ptr) {
402
+ report_holder *h = ptr;
403
+ if (h->report) sn_report_free(h->report);
404
+ xfree(h);
405
+ }
406
+ static size_t report_size(const void *ptr) {
407
+ const report_holder *h = ptr;
408
+ return sizeof(*h) + (h->report ? sn_report_size(h->report) : 0);
409
+ }
410
+ static const rb_data_type_t report_type = {
411
+ "Schemurai::Native::Diagnostics", {report_mark, report_release, report_size, NULL, {NULL}},
412
+ NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
413
+ };
414
+ static VALUE diagnostics(VALUE self, VALUE instance) {
415
+ holder *engine;
416
+ TypedData_Get_Struct(self, holder, &engine_type, engine);
417
+ if (!engine->engine) rb_raise(rb_eRuntimeError, "uninitialized native program");
418
+ if (engine->compiling) rb_raise(rb_eRuntimeError, "native program is still compiling");
419
+ report_holder *h;
420
+ VALUE owner = TypedData_Make_Struct(rb_cObject, report_holder, &report_type, h);
421
+ h->report = NULL;
422
+ int status = 0;
423
+ h->report = sn_diagnose(engine->engine, instance, &status);
424
+ if (status < 0) rb_raise(rb_eRuntimeError, "native diagnostics panicked");
425
+ if (status == SN_VERDICT_UNSUPPORTED) return Qnil;
426
+ size_t count = sn_report_len(h->report);
427
+ VALUE output = rb_ary_new_capa(count);
428
+ for (size_t index = 0; index < count; index++) {
429
+ diagnostic_record r;
430
+ sn_report_record(h->report, index, &r);
431
+ VALUE row = rb_ary_new_capa(7);
432
+ rb_ary_push(output, row);
433
+ rb_ary_push(row, rb_utf8_str_new((const char *)r.keyword, r.keyword_len));
434
+ rb_ary_push(row, rb_utf8_str_new((const char *)r.instance_path, r.instance_len));
435
+ rb_ary_push(row, rb_utf8_str_new((const char *)r.schema_path, r.schema_len));
436
+ rb_ary_push(row, INT2NUM(r.opcode));
437
+ rb_ary_push(row, r.operand);
438
+ rb_ary_push(row, r.instance);
439
+ rb_ary_push(row, SIZET2NUM(r.detail));
440
+ }
441
+ RB_GC_GUARD(owner);
442
+ RB_GC_GUARD(instance);
443
+ RB_GC_GUARD(self);
444
+ return output;
445
+ }
446
+ void Init_schemurai_native(void) {
447
+ /* Cache fixed IDs explicitly, independent of compiler-specific macros. */
448
+ id_resolve = rb_intern_const("resolve");
449
+ id_schemurai = rb_intern_const("Schemurai");
450
+ id_resolution_error = rb_intern_const("ResolutionError");
451
+ id_root = rb_intern_const("root");
452
+ id_compile = rb_intern_const("compile");
453
+ id_graph = rb_intern_const("@graph");
454
+ id_dynamic_anchor = rb_intern_const("dynamic_anchor");
455
+ utf8_index = rb_utf8_encindex();
456
+ ascii_index = rb_usascii_encindex();
457
+ binary_index = rb_ascii8bit_encindex();
458
+ for (size_t i = 0; i < sizeof(opcodes) / sizeof(opcodes[0]); i++)
459
+ opcodes[i].id = rb_intern_const(opcodes[i].symbol);
460
+ rb_ext_ractor_safe(true);
461
+ VALUE schemurai = rb_define_module("Schemurai");
462
+ VALUE native = rb_define_module_under(schemurai, "Native");
463
+ VALUE protocol = rb_define_module_under(native, "Protocol");
464
+ #define SN_OPCODE(name, symbol, value) rb_define_const(protocol, "OP_" #name, INT2NUM(SN_OP_##name));
465
+ #define SN_CONSTANT(name, value) rb_define_const(protocol, #name, INT2NUM(SN_##name));
466
+ #include "protocol.def"
467
+ #undef SN_OPCODE
468
+ #undef SN_CONSTANT
469
+ VALUE klass = rb_define_class_under(native, "Program", rb_cObject);
470
+ rb_define_alloc_func(klass, allocate);
471
+ rb_define_method(klass, "initialize", initialize, 4);
472
+ rb_define_method(klass, "valid?", valid, 1);
473
+ rb_define_method(klass, "diagnostics", diagnostics, 1);
474
+ }
475
+
476
+ extern int sn_object_entry(const void *, VALUE, VALUE);
477
+ struct iteration { void *context; int result; };
478
+ static int visit(VALUE key, VALUE value, VALUE arg) {
479
+ struct iteration *iteration = (struct iteration *)arg;
480
+ iteration->result = sn_object_entry(iteration->context, key, value);
481
+ return iteration->result == SN_VERDICT_VALID ? ST_CONTINUE : ST_STOP;
482
+ }
483
+ int sn_object_each(VALUE value, void *context) {
484
+ struct iteration iteration = {context, SN_VERDICT_VALID};
485
+ rb_hash_foreach(value, visit, (VALUE)&iteration);
486
+ return iteration.result;
487
+ }
488
+
489
+
490
+ /* Only compilation calls back into Ruby. rb_protect catches exceptions before
491
+ * returning to Rust; raising is deferred until Rust returns to initialize. */
492
+ struct resolution { VALUE compiler, program, reference; };
493
+ static VALUE resolve_reference(VALUE arg) {
494
+ struct resolution *r = (struct resolution *)arg;
495
+ return rb_funcall(r->compiler, id_resolve, 2, r->program, r->reference);
496
+ }
497
+ int sn_resolve(VALUE compiler, VALUE program, VALUE reference, VALUE *result) {
498
+ struct resolution r = {compiler, program, reference};
499
+ int state = 0;
500
+ *result = rb_protect(resolve_reference, (VALUE)&r, &state);
501
+ if (state) { *result = rb_errinfo(); rb_set_errinfo(Qnil); }
502
+ return state;
503
+ }
504
+ int sn_resolution_error(VALUE error) {
505
+ VALUE schemurai = rb_const_get(rb_cObject, id_schemurai);
506
+ VALUE klass = rb_const_get(schemurai, id_resolution_error);
507
+ return rb_obj_is_kind_of(error, klass);
508
+ }
509
+
510
+ typedef int (*hash_callback)(void *, VALUE, VALUE);
511
+ struct hash_walk { void *context; hash_callback callback; int result; };
512
+ static int hash_visit(VALUE key, VALUE value, VALUE arg) {
513
+ struct hash_walk *walk = (struct hash_walk *)arg;
514
+ walk->result = walk->callback(walk->context, key, value);
515
+ return walk->result == SN_VERDICT_VALID ? ST_CONTINUE : ST_STOP;
516
+ }
517
+ int sn_hash_each(VALUE value, void *context, hash_callback callback) {
518
+ struct hash_walk walk = {context, callback, SN_VERDICT_VALID};
519
+ rb_hash_foreach(value, hash_visit, (VALUE)&walk);
520
+ return walk.result;
521
+ }
522
+ typedef int (*key_view_callback)(void *, const unsigned char *, size_t, VALUE);
523
+ struct key_view_walk { void *context; key_view_callback callback; int result; };
524
+ static int key_view_visit(VALUE key, VALUE value, VALUE arg) {
525
+ struct key_view_walk *walk = (struct key_view_walk *)arg;
526
+ if (!RB_TYPE_P(key, T_STRING) || CLASS_OF(key) != rb_cString) {
527
+ walk->result = SN_VERDICT_UNSUPPORTED;
528
+ return ST_STOP;
529
+ }
530
+ /* The Rust callback borrows these original bytes under the GVL and must
531
+ * neither allocate Ruby objects nor call back into Ruby. */
532
+ walk->result = walk->callback(walk->context,
533
+ (const unsigned char *)RSTRING_PTR(key), RSTRING_LEN(key), value);
534
+ return walk->result == SN_VERDICT_VALID ? ST_CONTINUE : ST_STOP;
535
+ }
536
+ int sn_hash_each_key_view(VALUE value, void *context, key_view_callback callback) {
537
+ struct key_view_walk walk = {context, callback, SN_VERDICT_VALID};
538
+ rb_hash_foreach(value, key_view_visit, (VALUE)&walk);
539
+ return walk.result;
540
+ }
541
+ const unsigned char *sn_string_data(VALUE value) { return (const unsigned char *)RSTRING_PTR(value); }
542
+ size_t sn_string_bytes(VALUE value) { return RSTRING_LEN(value); }
543
+ int sn_string_length_guarded(VALUE value, size_t *length) {
544
+ if (!RB_TYPE_P(value, T_STRING) || CLASS_OF(value) != rb_cString)
545
+ return sn_kind(value);
546
+ *length = string_length(value);
547
+ return SN_TYPE_STRING;
548
+ }
549
+ static void string_raw_view(VALUE value, const unsigned char **data, size_t *length) {
550
+ *data = (const unsigned char *)RSTRING_PTR(value);
551
+ *length = RSTRING_LEN(value);
552
+ }
553
+ void sn_string_raw_view(VALUE value, const unsigned char **data, size_t *length) {
554
+ string_raw_view(value, data, length);
555
+ }
556
+ /* The caller has already guarded an exact String under the GVL. */
557
+ int sn_record_key_view(VALUE value, const unsigned char **data, size_t *length) {
558
+ string_raw_view(value, data, length);
559
+ return RB_ENCODING_GET(value);
560
+ }
561
+ int sn_format_item_view(VALUE value, const unsigned char **data, size_t *length) {
562
+ if (RB_TYPE_P(value, T_STRING) && CLASS_OF(value) == rb_cString) {
563
+ string_raw_view(value, data, length);
564
+ return 0;
565
+ }
566
+ return -sn_kind(value);
567
+ }
568
+ static int string_utf8(VALUE value) {
569
+ /* Public inline alias, with CRuby fallback for extended indices.
570
+ * Callers have already classified this VALUE as a String. */
571
+ int encoding = RB_ENCODING_GET(value);
572
+ return encoding == utf8_index ? SN_ENCODING_UTF8 : (encoding == ascii_index ? SN_ENCODING_ASCII : SN_ENCODING_OTHER);
573
+ }
574
+ int sn_string_utf8(VALUE value) { return string_utf8(value); }
575
+ static int string_view(VALUE value, const unsigned char **data, size_t *length) {
576
+ int encoding = string_utf8(value);
577
+ string_raw_view(value, data, length);
578
+ return encoding;
579
+ }
580
+ int sn_string_view(VALUE value, const unsigned char **data, size_t *length) {
581
+ return string_view(value, data, length);
582
+ }
583
+ static int pattern_view(VALUE value, const unsigned char **data, size_t *length) {
584
+ int encoding = string_view(value, data, length);
585
+ /* Read CRuby's existing ASCII cache without scanning or changing it. */
586
+ if (encoding && RB_ENC_CODERANGE(value) == RUBY_ENC_CODERANGE_7BIT) return SN_ENCODING_SEVEN_BIT;
587
+ return encoding;
588
+ }
589
+ int sn_pattern_view(VALUE value, const unsigned char **data, size_t *length) {
590
+ return pattern_view(value, data, length);
591
+ }
592
+ int sn_pattern_item_view(VALUE value, const unsigned char **data, size_t *length) {
593
+ /* Combine the exact-class guard and byte loan for homogeneous scans.
594
+ * Negative results retain the ordinary classifier for non-string values. */
595
+ if (RB_TYPE_P(value, T_STRING) && CLASS_OF(value) == rb_cString)
596
+ return pattern_view(value, data, length);
597
+ return -sn_kind(value);
598
+ }
599
+ static int json_encoding(VALUE value) {
600
+ int encoding = RB_ENCODING_GET(value);
601
+ return encoding == ascii_index ? SN_ENCODING_ASCII : ((encoding == utf8_index || encoding == binary_index) ? SN_ENCODING_UTF8 : SN_ENCODING_OTHER);
602
+ }
603
+ int sn_json_encoding(VALUE value) { return json_encoding(value); }
604
+ /* Ordinary String guard, original byte loan and JSON encoding metadata in one
605
+ * call. Only String results (0..2) initialize the pointer and length. */
606
+ int sn_json_item_view(VALUE value, const unsigned char **data, size_t *length) {
607
+ if (RB_TYPE_P(value, T_STRING) && CLASS_OF(value) == rb_cString) {
608
+ string_raw_view(value, data, length);
609
+ return json_encoding(value);
610
+ }
611
+ return -sn_kind(value);
612
+ }
613
+
614
+ struct binding { VALUE compiler, resource, name; };
615
+ static VALUE resource_program(VALUE arg) {
616
+ struct binding *b = (struct binding *)arg;
617
+ VALUE node = rb_funcall(b->resource, id_root, 0);
618
+ return rb_funcall(b->compiler, id_compile, 1, node);
619
+ }
620
+ static VALUE anchor_program(VALUE arg) {
621
+ struct binding *b = (struct binding *)arg;
622
+ VALUE graph = rb_ivar_get(b->compiler, id_graph);
623
+ VALUE node = rb_funcall(graph, id_dynamic_anchor, 2, b->resource, b->name);
624
+ return NIL_P(node) ? Qnil : rb_funcall(b->compiler, id_compile, 1, node);
625
+ }
626
+ int sn_resource_program(VALUE compiler, VALUE resource, VALUE *result) {
627
+ struct binding b = {compiler, resource, Qnil};
628
+ int state = 0;
629
+ *result = rb_protect(resource_program, (VALUE)&b, &state);
630
+ if (state) { *result = rb_errinfo(); rb_set_errinfo(Qnil); }
631
+ return state;
632
+ }
633
+ int sn_anchor_program(VALUE compiler, VALUE resource, VALUE name, VALUE *result) {
634
+ struct binding b = {compiler, resource, name};
635
+ int state = 0;
636
+ *result = rb_protect(anchor_program, (VALUE)&b, &state);
637
+ if (state) { *result = rb_errinfo(); rb_set_errinfo(Qnil); }
638
+ return state;
639
+ }