schemurai 2.3.0 → 3.0.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 +4 -4
- data/README.md +67 -4
- data/ext/schemurai_native/Cargo.lock +1 -1
- data/ext/schemurai_native/Cargo.toml +1 -1
- data/ext/schemurai_native/glue.c +155 -14
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/diagnostics.rs +395 -225
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/graph.rs +50 -0
- data/ext/schemurai_native/src/instance_graph.rs +84 -0
- data/ext/schemurai_native/src/lib.rs +947 -184
- data/ext/schemurai_native/src/memo.rs +1149 -0
- data/ext/schemurai_native/src/memo_scope.rs +240 -0
- data/ext/schemurai_native/src/number.rs +7 -8
- data/ext/schemurai_native/src/pattern.rs +157 -14
- data/ext/schemurai_native/src/scope.rs +243 -33
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +245 -90
- data/lib/schemurai/backend.rb +1 -0
- data/lib/schemurai/evaluator.rb +45 -3
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +100 -0
- data/lib/schemurai/native/evaluator.rb +32 -5
- data/lib/schemurai/schema_domain.rb +12 -7
- data/lib/schemurai/schema_graph.rb +10 -1
- data/lib/schemurai/unique_items.rb +2 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +7 -3
- data/lib/schemurai/vm/evaluator.rb +50 -3
- data/lib/schemurai/vm/limited_evaluator.rb +97 -0
- data/lib/schemurai.rb +74 -27
- data/sig/schemurai.rbs +20 -12
- metadata +16 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fde4b5a2e6ccf979c4948fd13476c8250acf053346de28c4d62b32ed0c92e4f
|
|
4
|
+
data.tar.gz: 7e3a5fb2dfa17195b1a45cde045f7256a1c5f3687f19f8d2c7b821980ffe2ab1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42c0e94ce30932d09c9f1617592338f2386076eca4d176f1e1a9461557ea723abf512f94d7c68fb52a392021e9b45cba4ab329ee3669754b6d0a996f0319a07f
|
|
7
|
+
data.tar.gz: 68b8c4e585cd40e11d4d7fbae8d9345f52a4bfca437704b86999eab79f219cf52e276b623faf0271cd49c4044c596f544399be77ba8a8fd36946f20524708cc2
|
data/README.md
CHANGED
|
@@ -185,9 +185,12 @@ validator = registry.validator_for("https://example.test/value")
|
|
|
185
185
|
`ResolutionError` if a reference cannot be resolved. After it returns,
|
|
186
186
|
`validator_for` is read-only and may be called concurrently, while `compile` is
|
|
187
187
|
no longer available. A `Validator` contains per-validation mutable state and
|
|
188
|
-
must not be shared between threads or Ractors. With the VM
|
|
189
|
-
compiles and shares its bytecode before this transition
|
|
190
|
-
|
|
188
|
+
must not be shared between threads or Ractors. With the VM and native backends,
|
|
189
|
+
the registry compiles and shares its bytecode before this transition. The native
|
|
190
|
+
backend also prepares shared native instruction tables for all four combinations
|
|
191
|
+
of `content` and `format`; `validator_for` does not recompile them in each Ractor.
|
|
192
|
+
Native regex search caches and VM fallback evaluation state belong to each
|
|
193
|
+
validator. Preparing these tables adds time and memory to `make_shareable`.
|
|
191
194
|
|
|
192
195
|
### Meta-schema validation
|
|
193
196
|
|
|
@@ -218,10 +221,70 @@ regardless of the registry option and reuse meta-schema graph data within that
|
|
|
218
221
|
registry. The stateful validator used for a check is temporary; no global
|
|
219
222
|
validator is retained.
|
|
220
223
|
|
|
224
|
+
### Opt-in validation limits (experimental)
|
|
225
|
+
|
|
226
|
+
`max_errors:` and `timeout:` are optional kwargs. Both default to `nil`
|
|
227
|
+
(unlimited) and apply independently to each validation call. `max_errors` must
|
|
228
|
+
be a nonnegative Integer; `timeout` must be finite nonnegative Integer or Float
|
|
229
|
+
seconds. No settings object is needed.
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
validator = Schemurai.compile(schema, max_errors: 100, timeout: 0.1)
|
|
233
|
+
begin
|
|
234
|
+
result = validator.validate(order)
|
|
235
|
+
rescue Schemurai::LimitExceeded => error
|
|
236
|
+
# Evaluation is incomplete; no partial result is returned.
|
|
237
|
+
warn "Validation stopped at #{error.limit}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
validator.validate(order, max_errors: 10) # retains the compiled timeout
|
|
241
|
+
validator.validate(order, timeout: nil) # retains the compiled error cap
|
|
242
|
+
validator.validate(order, max_errors: nil, timeout: nil) # disables both
|
|
243
|
+
validator.valid?(order) # applies the timeout, ignores max_errors
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`SchemaRegistry#compile`, `#validator_for`, the convenience `validate` / `valid?`
|
|
247
|
+
methods and explicit meta-schema checks also accept these kwargs. Overrides
|
|
248
|
+
replace only the specified option. Settings belong to each compiled validator;
|
|
249
|
+
a shareable registry retains no mutable invocation counters.
|
|
250
|
+
|
|
251
|
+
The error cap checks before constructing the next emitted diagnostic. Exactly
|
|
252
|
+
`max_errors` errors may be returned; attempting another raises `LimitExceeded`
|
|
253
|
+
with `limit == :max_errors`, `maximum` and `attempted` counts. Suppressed
|
|
254
|
+
applicator trials do not consume the cap. Boolean checks ignore the error cap
|
|
255
|
+
and use the ordinary evaluator when no timeout is set.
|
|
256
|
+
|
|
257
|
+
The timeout uses a monotonic elapsed-time deadline, including diagnostic
|
|
258
|
+
rendering and native-to-VM fallback. It checks at entry, before returning and
|
|
259
|
+
every 256 checkpoints in evaluation, container scans and structural traversal.
|
|
260
|
+
Ruby string/content operations also check before and after expensive calls.
|
|
261
|
+
Zero seconds stops even a trivial call. Expiry raises `LimitExceeded` with
|
|
262
|
+
`limit == :timeout`; `maximum` and `attempted` are configured and elapsed seconds.
|
|
263
|
+
Native returns an exhaustion status and releases Rust resources before Ruby
|
|
264
|
+
raises. A limit hit is terminal and does not trigger fallback.
|
|
265
|
+
|
|
266
|
+
This implementation never uses Ruby `Timeout`, timer threads, signals or
|
|
267
|
+
asynchronous exception injection. It is cooperative: one regexp search, scalar
|
|
268
|
+
operation, JSON decode, allocation or custom format callback may overrun until
|
|
269
|
+
the next checkpoint. GC and descheduling count toward elapsed time; the polling
|
|
270
|
+
interval bounds skipped checkpoints, not elapsed overrun. Compilation,
|
|
271
|
+
registration and automatic compile-time meta-schema validation are outside the
|
|
272
|
+
timeout. Explicit meta-schema checks can set their own timeout.
|
|
273
|
+
|
|
274
|
+
Use the error cap to bound diagnostic count and the timeout to bound prolonged
|
|
275
|
+
traversal. They protect different resources: a deadline does not bound the
|
|
276
|
+
number of diagnostics, and an error cap cannot stop a long successful check.
|
|
277
|
+
Input byte size and nesting depth belong at the parsing boundary. An error cap
|
|
278
|
+
does not bound the size of an individual message. Preempting individual scalar
|
|
279
|
+
operations requires operation-specific limits or a supervised worker process.
|
|
280
|
+
The backends retain their respective Ruby, VM and native execution paths and
|
|
281
|
+
native memoization. Timed native bulk pattern/format/content and Fixnum contains
|
|
282
|
+
scans use per-item evaluation so successful scans can poll.
|
|
283
|
+
|
|
221
284
|
### Backend selection
|
|
222
285
|
|
|
223
286
|
`Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
|
|
224
|
-
the
|
|
287
|
+
the selected backend. Pass `backend: :ruby` to force the Ruby oracle or
|
|
225
288
|
`backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
|
|
226
289
|
Pass `backend: :native` to use the experimental Rust VM extension. Building the
|
|
227
290
|
native extension requires a C toolchain, Ruby development headers, and Rust
|
data/ext/schemurai_native/glue.c
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include <ruby.h>
|
|
2
2
|
#include <ruby/encoding.h>
|
|
3
|
+
#include <ruby/ractor.h>
|
|
3
4
|
#include <ruby/st.h>
|
|
4
5
|
#include <stdint.h>
|
|
5
6
|
#include <stddef.h>
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
#include "protocol.h"
|
|
8
9
|
|
|
9
10
|
static ID id_resolve, id_schemurai, id_resolution_error;
|
|
10
|
-
static ID id_root, id_compile, id_graph, id_dynamic_anchor;
|
|
11
|
+
static ID id_root, id_compile, id_graph, id_dynamic_anchor, id_dynamic_anchor_names, id_dynamic_anchor_count;
|
|
11
12
|
|
|
12
13
|
/* The C layer adapts CRuby APIs to Rust's opaque VALUEs. */
|
|
13
14
|
#ifdef HAVE_RB_HASH_STLIKE_FOREACH
|
|
@@ -17,8 +18,15 @@ extern int rb_hash_stlike_foreach(VALUE, st_foreach_callback_func *, st_data_t);
|
|
|
17
18
|
#endif
|
|
18
19
|
typedef struct { void *engine; const void *roots; VALUE program; int compiling; } holder;
|
|
19
20
|
extern void *sn_new(void);
|
|
20
|
-
extern int sn_compile(void *, VALUE, VALUE, int, int, VALUE *);
|
|
21
|
-
extern int sn_valid(const void *, VALUE);
|
|
21
|
+
extern int sn_compile(void *, VALUE, VALUE, int, int, int, VALUE *);
|
|
22
|
+
extern int sn_valid(const void *, const void *, size_t, VALUE);
|
|
23
|
+
extern size_t sn_node_index(const void *, VALUE);
|
|
24
|
+
extern size_t sn_root_index(const void *);
|
|
25
|
+
extern size_t sn_root_count(const void *);
|
|
26
|
+
extern VALUE sn_root_at(const void *, size_t);
|
|
27
|
+
extern void *sn_caches_new(void);
|
|
28
|
+
extern void sn_caches_free(void *);
|
|
29
|
+
extern size_t sn_caches_size(const void *);
|
|
22
30
|
extern void sn_free(void *);
|
|
23
31
|
extern const void *sn_roots(const void *);
|
|
24
32
|
extern void sn_roots_mark(const void *);
|
|
@@ -43,7 +51,7 @@ static size_t size(const void *ptr) {
|
|
|
43
51
|
}
|
|
44
52
|
static const rb_data_type_t engine_type = {
|
|
45
53
|
"Schemurai::Native::Program", {mark, release, size, NULL, {NULL}},
|
|
46
|
-
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
|
|
54
|
+
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE
|
|
47
55
|
};
|
|
48
56
|
static VALUE allocate(VALUE klass) {
|
|
49
57
|
holder *h;
|
|
@@ -55,6 +63,7 @@ static VALUE allocate(VALUE klass) {
|
|
|
55
63
|
return obj;
|
|
56
64
|
}
|
|
57
65
|
static VALUE initialize(VALUE self, VALUE program, VALUE compiler, VALUE format, VALUE content) {
|
|
66
|
+
rb_check_frozen(self);
|
|
58
67
|
holder *h;
|
|
59
68
|
TypedData_Get_Struct(self, holder, &engine_type, h);
|
|
60
69
|
if (h->engine) rb_raise(rb_eRuntimeError, "native program is already initialized");
|
|
@@ -63,7 +72,8 @@ static VALUE initialize(VALUE self, VALUE program, VALUE compiler, VALUE format,
|
|
|
63
72
|
h->roots = sn_roots(h->engine);
|
|
64
73
|
h->compiling = 1;
|
|
65
74
|
VALUE error = Qnil;
|
|
66
|
-
int status = sn_compile(h->engine, program, compiler, RTEST(format), RTEST(content),
|
|
75
|
+
int status = sn_compile(h->engine, program, compiler, RTEST(format), RTEST(content),
|
|
76
|
+
RB_TYPE_P(program, T_ARRAY), &error);
|
|
67
77
|
h->compiling = 0;
|
|
68
78
|
if (status < 0) {
|
|
69
79
|
sn_free(h->engine);
|
|
@@ -79,8 +89,92 @@ static VALUE valid(VALUE self, VALUE instance) {
|
|
|
79
89
|
TypedData_Get_Struct(self, holder, &engine_type, h);
|
|
80
90
|
if (!h->engine) rb_raise(rb_eRuntimeError, "uninitialized native program");
|
|
81
91
|
if (h->compiling) rb_raise(rb_eRuntimeError, "native program is still compiling");
|
|
82
|
-
int status = sn_valid(h->engine, instance);
|
|
92
|
+
int status = sn_valid(h->engine, NULL, sn_root_index(h->engine), instance);
|
|
93
|
+
RB_GC_GUARD(instance);
|
|
94
|
+
if (status < 0) rb_raise(rb_eRuntimeError, "native evaluator panicked");
|
|
95
|
+
return status == SN_VERDICT_UNSUPPORTED ? Qnil : (status ? Qtrue : Qfalse);
|
|
96
|
+
}
|
|
97
|
+
/* Search caches and root selection belong to the owning validator. The Ruby
|
|
98
|
+
* owner keeps the shared engine and all its GC roots alive. */
|
|
99
|
+
typedef struct { VALUE owner; void *caches; size_t root; } runner_holder;
|
|
100
|
+
static void runner_mark(void *ptr) { rb_gc_mark(((runner_holder *)ptr)->owner); }
|
|
101
|
+
static void runner_release(void *ptr) {
|
|
102
|
+
runner_holder *h = ptr;
|
|
103
|
+
if (h->caches) sn_caches_free(h->caches);
|
|
104
|
+
xfree(h);
|
|
105
|
+
}
|
|
106
|
+
static size_t runner_size(const void *ptr) {
|
|
107
|
+
const runner_holder *h = ptr;
|
|
108
|
+
return sizeof(*h) + (h->caches ? sn_caches_size(h->caches) : 0);
|
|
109
|
+
}
|
|
110
|
+
static const rb_data_type_t runner_type = {
|
|
111
|
+
"Schemurai::Native::Runner", {runner_mark, runner_release, runner_size, NULL, {NULL}},
|
|
112
|
+
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
|
|
113
|
+
};
|
|
114
|
+
static holder *ready_program(VALUE self) {
|
|
115
|
+
holder *h;
|
|
116
|
+
TypedData_Get_Struct(self, holder, &engine_type, h);
|
|
117
|
+
if (!h->engine) rb_raise(rb_eRuntimeError, "uninitialized native program");
|
|
118
|
+
if (h->compiling) rb_raise(rb_eRuntimeError, "native program is still compiling");
|
|
119
|
+
return h;
|
|
120
|
+
}
|
|
121
|
+
static VALUE make_shareable(VALUE self) {
|
|
122
|
+
holder *h = ready_program(self);
|
|
123
|
+
/* TypedData's hidden references must be shareable too. Freeze temporary
|
|
124
|
+
* compilation arrays as well as the VM programs/graph they retain. */
|
|
125
|
+
size_t count = sn_root_count(h->roots);
|
|
126
|
+
for (size_t i = 0; i < count; i++) rb_ractor_make_shareable(sn_root_at(h->roots, i));
|
|
127
|
+
rb_ractor_make_shareable(h->program);
|
|
128
|
+
return rb_ractor_make_shareable(self);
|
|
129
|
+
}
|
|
130
|
+
static VALUE runner(VALUE self, VALUE root) {
|
|
131
|
+
holder *h = ready_program(self);
|
|
132
|
+
size_t index = sn_node_index(h->engine, root);
|
|
133
|
+
if (index == SIZE_MAX) rb_raise(rb_eArgError, "root was not compiled in this native program");
|
|
134
|
+
VALUE klass = rb_const_get(rb_obj_class(self), rb_intern_const("Runner"));
|
|
135
|
+
runner_holder *r;
|
|
136
|
+
VALUE object = TypedData_Make_Struct(klass, runner_holder, &runner_type, r);
|
|
137
|
+
r->owner = self;
|
|
138
|
+
r->root = index;
|
|
139
|
+
r->caches = sn_caches_new();
|
|
140
|
+
RB_GC_GUARD(self);
|
|
141
|
+
return object;
|
|
142
|
+
}
|
|
143
|
+
typedef struct { size_t max_errors; uint32_t enabled; double timeout; } native_limits;
|
|
144
|
+
typedef struct { uint32_t kind; size_t attempted; } native_outcome;
|
|
145
|
+
extern int sn_valid_with_limits(const void *, const void *, size_t, VALUE, const native_limits *, native_outcome *);
|
|
146
|
+
static native_limits read_limits(VALUE budget) {
|
|
147
|
+
native_limits limits = {0};
|
|
148
|
+
VALUE cap = rb_funcall(budget, rb_intern("native_max_errors"), 0);
|
|
149
|
+
if (!NIL_P(cap)) { limits.max_errors = NUM2SIZET(cap); limits.enabled |= 1; }
|
|
150
|
+
VALUE duration = rb_funcall(budget, rb_intern("native_duration"), 0);
|
|
151
|
+
if (!NIL_P(duration)) { limits.timeout = NUM2DBL(duration); limits.enabled |= 2; }
|
|
152
|
+
RB_GC_GUARD(cap);
|
|
153
|
+
return limits;
|
|
154
|
+
}
|
|
155
|
+
static void finish_budget(VALUE budget, const native_outcome *outcome) {
|
|
156
|
+
if (outcome->kind) rb_funcall(budget, rb_intern("raise_native_limit"), 2, UINT2NUM(outcome->kind), SIZET2NUM(outcome->attempted));
|
|
157
|
+
}
|
|
158
|
+
static VALUE runner_valid_with_budget(VALUE self, VALUE instance, VALUE budget) {
|
|
159
|
+
runner_holder *r;
|
|
160
|
+
TypedData_Get_Struct(self, runner_holder, &runner_type, r);
|
|
161
|
+
holder *h = ready_program(r->owner);
|
|
162
|
+
native_limits limits = read_limits(budget);
|
|
163
|
+
native_outcome outcome = {0};
|
|
164
|
+
int status = sn_valid_with_limits(h->engine, r->caches, r->root, instance, &limits, &outcome);
|
|
165
|
+
if (status < 0) rb_raise(rb_eRuntimeError, "native evaluator panicked");
|
|
166
|
+
finish_budget(budget, &outcome);
|
|
167
|
+
RB_GC_GUARD(instance);
|
|
168
|
+
RB_GC_GUARD(self);
|
|
169
|
+
return status == SN_VERDICT_UNSUPPORTED ? Qnil : (status ? Qtrue : Qfalse);
|
|
170
|
+
}
|
|
171
|
+
static VALUE runner_valid(VALUE self, VALUE instance) {
|
|
172
|
+
runner_holder *r;
|
|
173
|
+
TypedData_Get_Struct(self, runner_holder, &runner_type, r);
|
|
174
|
+
holder *h = ready_program(r->owner);
|
|
175
|
+
int status = sn_valid(h->engine, r->caches, r->root, instance);
|
|
83
176
|
RB_GC_GUARD(instance);
|
|
177
|
+
RB_GC_GUARD(self);
|
|
84
178
|
if (status < 0) rb_raise(rb_eRuntimeError, "native evaluator panicked");
|
|
85
179
|
return status == SN_VERDICT_UNSUPPORTED ? Qnil : (status ? Qtrue : Qfalse);
|
|
86
180
|
}
|
|
@@ -265,7 +359,6 @@ int sn_wide_integer_compare(VALUE value, VALUE bound) {
|
|
|
265
359
|
if (FIXNUM_P(value)) return rb_big_sign(bound) ? -1 : 1;
|
|
266
360
|
return FIX2INT(rb_big_cmp(value, bound));
|
|
267
361
|
}
|
|
268
|
-
size_t sn_integer_bytes(VALUE value) { return rb_absint_size(value, NULL); }
|
|
269
362
|
int sn_integer1024(VALUE value, uint64_t *words) {
|
|
270
363
|
return rb_integer_pack(value, words, 16, sizeof(uint64_t), 0,
|
|
271
364
|
INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER);
|
|
@@ -388,7 +481,8 @@ typedef struct {
|
|
|
388
481
|
size_t detail;
|
|
389
482
|
} diagnostic_record;
|
|
390
483
|
typedef struct { void *report; } report_holder;
|
|
391
|
-
extern void *sn_diagnose(const void *, VALUE, int *);
|
|
484
|
+
extern void *sn_diagnose(const void *, const void *, size_t, VALUE, int *);
|
|
485
|
+
extern void *sn_diagnose_with_limits(const void *, const void *, size_t, VALUE, int *, const native_limits *, native_outcome *);
|
|
392
486
|
extern size_t sn_report_len(const void *);
|
|
393
487
|
extern size_t sn_report_size(const void *);
|
|
394
488
|
extern void sn_report_record(const void *, size_t, diagnostic_record *);
|
|
@@ -411,21 +505,25 @@ static const rb_data_type_t report_type = {
|
|
|
411
505
|
"Schemurai::Native::Diagnostics", {report_mark, report_release, report_size, NULL, {NULL}},
|
|
412
506
|
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
|
|
413
507
|
};
|
|
414
|
-
static VALUE
|
|
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");
|
|
508
|
+
static VALUE collect_diagnostics(VALUE self, VALUE instance, holder *engine, const void *caches, size_t root, VALUE budget) {
|
|
419
509
|
report_holder *h;
|
|
420
510
|
VALUE owner = TypedData_Make_Struct(rb_cObject, report_holder, &report_type, h);
|
|
421
511
|
h->report = NULL;
|
|
422
512
|
int status = 0;
|
|
423
|
-
|
|
513
|
+
native_outcome outcome = {0};
|
|
514
|
+
if (NIL_P(budget)) {
|
|
515
|
+
h->report = sn_diagnose(engine->engine, caches, root, instance, &status);
|
|
516
|
+
} else {
|
|
517
|
+
native_limits limits = read_limits(budget);
|
|
518
|
+
h->report = sn_diagnose_with_limits(engine->engine, caches, root, instance, &status, &limits, &outcome);
|
|
519
|
+
}
|
|
424
520
|
if (status < 0) rb_raise(rb_eRuntimeError, "native diagnostics panicked");
|
|
521
|
+
if (!NIL_P(budget)) finish_budget(budget, &outcome);
|
|
425
522
|
if (status == SN_VERDICT_UNSUPPORTED) return Qnil;
|
|
426
523
|
size_t count = sn_report_len(h->report);
|
|
427
524
|
VALUE output = rb_ary_new_capa(count);
|
|
428
525
|
for (size_t index = 0; index < count; index++) {
|
|
526
|
+
if (!NIL_P(budget) && index % 256 == 0) rb_funcall(budget, rb_intern("check_deadline!"), 0);
|
|
429
527
|
diagnostic_record r;
|
|
430
528
|
sn_report_record(h->report, index, &r);
|
|
431
529
|
VALUE row = rb_ary_new_capa(7);
|
|
@@ -443,6 +541,22 @@ static VALUE diagnostics(VALUE self, VALUE instance) {
|
|
|
443
541
|
RB_GC_GUARD(self);
|
|
444
542
|
return output;
|
|
445
543
|
}
|
|
544
|
+
static VALUE diagnostics(VALUE self, VALUE instance) {
|
|
545
|
+
holder *h = ready_program(self);
|
|
546
|
+
return collect_diagnostics(self, instance, h, NULL, sn_root_index(h->engine), Qnil);
|
|
547
|
+
}
|
|
548
|
+
static VALUE runner_diagnostics(VALUE self, VALUE instance) {
|
|
549
|
+
runner_holder *r;
|
|
550
|
+
TypedData_Get_Struct(self, runner_holder, &runner_type, r);
|
|
551
|
+
holder *h = ready_program(r->owner);
|
|
552
|
+
return collect_diagnostics(self, instance, h, r->caches, r->root, Qnil);
|
|
553
|
+
}
|
|
554
|
+
static VALUE runner_diagnostics_with_budget(VALUE self, VALUE instance, VALUE budget) {
|
|
555
|
+
runner_holder *r;
|
|
556
|
+
TypedData_Get_Struct(self, runner_holder, &runner_type, r);
|
|
557
|
+
holder *h = ready_program(r->owner);
|
|
558
|
+
return collect_diagnostics(self, instance, h, r->caches, r->root, budget);
|
|
559
|
+
}
|
|
446
560
|
void Init_schemurai_native(void) {
|
|
447
561
|
/* Cache fixed IDs explicitly, independent of compiler-specific macros. */
|
|
448
562
|
id_resolve = rb_intern_const("resolve");
|
|
@@ -452,6 +566,8 @@ void Init_schemurai_native(void) {
|
|
|
452
566
|
id_compile = rb_intern_const("compile");
|
|
453
567
|
id_graph = rb_intern_const("@graph");
|
|
454
568
|
id_dynamic_anchor = rb_intern_const("dynamic_anchor");
|
|
569
|
+
id_dynamic_anchor_names = rb_intern_const("dynamic_anchor_names");
|
|
570
|
+
id_dynamic_anchor_count = rb_intern_const("@dynamic_anchor_count");
|
|
455
571
|
utf8_index = rb_utf8_encindex();
|
|
456
572
|
ascii_index = rb_usascii_encindex();
|
|
457
573
|
binary_index = rb_ascii8bit_encindex();
|
|
@@ -471,6 +587,14 @@ void Init_schemurai_native(void) {
|
|
|
471
587
|
rb_define_method(klass, "initialize", initialize, 4);
|
|
472
588
|
rb_define_method(klass, "valid?", valid, 1);
|
|
473
589
|
rb_define_method(klass, "diagnostics", diagnostics, 1);
|
|
590
|
+
rb_define_method(klass, "make_shareable", make_shareable, 0);
|
|
591
|
+
rb_define_method(klass, "runner", runner, 1);
|
|
592
|
+
VALUE runner_class = rb_define_class_under(klass, "Runner", rb_cObject);
|
|
593
|
+
rb_undef_alloc_func(runner_class);
|
|
594
|
+
rb_define_method(runner_class, "valid?", runner_valid, 1);
|
|
595
|
+
rb_define_method(runner_class, "valid_with_budget", runner_valid_with_budget, 2);
|
|
596
|
+
rb_define_method(runner_class, "diagnostics", runner_diagnostics, 1);
|
|
597
|
+
rb_define_method(runner_class, "diagnostics_with_budget", runner_diagnostics_with_budget, 2);
|
|
474
598
|
}
|
|
475
599
|
|
|
476
600
|
extern int sn_object_entry(const void *, VALUE, VALUE);
|
|
@@ -623,6 +747,23 @@ static VALUE anchor_program(VALUE arg) {
|
|
|
623
747
|
VALUE node = rb_funcall(graph, id_dynamic_anchor, 2, b->resource, b->name);
|
|
624
748
|
return NIL_P(node) ? Qnil : rb_funcall(b->compiler, id_compile, 1, node);
|
|
625
749
|
}
|
|
750
|
+
/* Anchor counts fit immediate integers: the table cannot exceed memory.
|
|
751
|
+
* Reading metadata invokes no Ruby method or allocation. */
|
|
752
|
+
size_t sn_anchor_count(VALUE resource) {
|
|
753
|
+
return (size_t)FIX2LONG(rb_ivar_get(resource, id_dynamic_anchor_count));
|
|
754
|
+
}
|
|
755
|
+
static VALUE anchor_names(VALUE arg) {
|
|
756
|
+
struct binding *b = (struct binding *)arg;
|
|
757
|
+
VALUE graph = rb_ivar_get(b->compiler, id_graph);
|
|
758
|
+
return rb_funcall(graph, id_dynamic_anchor_names, 1, b->resource);
|
|
759
|
+
}
|
|
760
|
+
int sn_anchor_names(VALUE compiler, VALUE resource, VALUE *result) {
|
|
761
|
+
struct binding b = {compiler, resource, Qnil};
|
|
762
|
+
int state = 0;
|
|
763
|
+
*result = rb_protect(anchor_names, (VALUE)&b, &state);
|
|
764
|
+
if (state) { *result = rb_errinfo(); rb_set_errinfo(Qnil); }
|
|
765
|
+
return state;
|
|
766
|
+
}
|
|
626
767
|
int sn_resource_program(VALUE compiler, VALUE resource, VALUE *result) {
|
|
627
768
|
struct binding b = {compiler, resource, Qnil};
|
|
628
769
|
int state = 0;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//! Invocation-local native limits. Exhaustion is terminal, not VM delegation.
|
|
2
|
+
use std::cell::Cell;
|
|
3
|
+
use std::time::{Duration, Instant};
|
|
4
|
+
|
|
5
|
+
#[repr(C)]
|
|
6
|
+
#[derive(Clone, Copy, Default)]
|
|
7
|
+
pub struct Limits {
|
|
8
|
+
pub max_errors: usize,
|
|
9
|
+
pub enabled: u32,
|
|
10
|
+
pub timeout: f64,
|
|
11
|
+
}
|
|
12
|
+
#[repr(C)]
|
|
13
|
+
#[derive(Default)]
|
|
14
|
+
pub struct Outcome {
|
|
15
|
+
pub kind: u32,
|
|
16
|
+
pub attempted: usize,
|
|
17
|
+
}
|
|
18
|
+
#[derive(Default)]
|
|
19
|
+
pub(super) struct Budget {
|
|
20
|
+
pub limits: Limits,
|
|
21
|
+
pub hit: Cell<(u32, usize)>,
|
|
22
|
+
deadline: Option<Instant>,
|
|
23
|
+
checks: Cell<usize>,
|
|
24
|
+
}
|
|
25
|
+
impl Budget {
|
|
26
|
+
pub fn new(limits: Limits) -> Self {
|
|
27
|
+
Self {
|
|
28
|
+
limits,
|
|
29
|
+
deadline: if limits.enabled & 2 != 0 {
|
|
30
|
+
Duration::try_from_secs_f64(limits.timeout.max(0.0))
|
|
31
|
+
.ok()
|
|
32
|
+
.and_then(|duration| Instant::now().checked_add(duration))
|
|
33
|
+
} else {
|
|
34
|
+
None
|
|
35
|
+
},
|
|
36
|
+
..Self::default()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
pub fn timed(&self) -> bool {
|
|
40
|
+
self.limits.enabled & 2 != 0
|
|
41
|
+
}
|
|
42
|
+
pub fn check_deadline(&self) -> bool {
|
|
43
|
+
if self.hit.get().0 != 0 {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if self
|
|
47
|
+
.deadline
|
|
48
|
+
.is_some_and(|deadline| Instant::now() >= deadline)
|
|
49
|
+
{
|
|
50
|
+
self.hit.set((2, 0));
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
true
|
|
54
|
+
}
|
|
55
|
+
pub fn checkpoint(&self) -> bool {
|
|
56
|
+
if !self.timed() {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
let checks = self.checks.get().wrapping_add(1);
|
|
60
|
+
self.checks.set(checks);
|
|
61
|
+
if checks & 255 == 0 {
|
|
62
|
+
self.check_deadline()
|
|
63
|
+
} else {
|
|
64
|
+
self.hit.get().0 == 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
mod tests {
|
|
71
|
+
use super::*;
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn deadline_is_sampled_and_exhaustion_is_sticky() {
|
|
75
|
+
let mut budget = Budget::new(Limits {
|
|
76
|
+
timeout: 10.0,
|
|
77
|
+
enabled: 2,
|
|
78
|
+
..Limits::default()
|
|
79
|
+
});
|
|
80
|
+
assert!(budget.check_deadline());
|
|
81
|
+
budget.deadline = Some(Instant::now() - Duration::from_secs(1));
|
|
82
|
+
for _ in 0..255 {
|
|
83
|
+
assert!(budget.checkpoint());
|
|
84
|
+
}
|
|
85
|
+
assert!(!budget.checkpoint());
|
|
86
|
+
assert_eq!(budget.hit.get(), (2, 0));
|
|
87
|
+
assert!(!budget.checkpoint());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn huge_finite_deadlines_do_not_panic() {
|
|
92
|
+
assert!(
|
|
93
|
+
Budget::new(Limits {
|
|
94
|
+
timeout: f64::MAX,
|
|
95
|
+
enabled: 2,
|
|
96
|
+
..Limits::default()
|
|
97
|
+
})
|
|
98
|
+
.check_deadline()
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|