schemurai 2.2.3 → 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 +81 -7
- data/ext/schemurai_native/Cargo.lock +99 -0
- data/ext/schemurai_native/Cargo.toml +21 -0
- data/ext/schemurai_native/build.rs +35 -0
- data/ext/schemurai_native/extconf.rb +63 -0
- data/ext/schemurai_native/glue.c +780 -0
- data/ext/schemurai_native/protocol.def +63 -0
- data/ext/schemurai_native/protocol.h +10 -0
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/content.rs +574 -0
- data/ext/schemurai_native/src/diagnostics.rs +1135 -0
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/format.rs +357 -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 +3139 -0
- 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 +1054 -0
- data/ext/schemurai_native/src/pattern.rs +431 -0
- data/ext/schemurai_native/src/scope.rs +420 -0
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +687 -0
- data/lib/schemurai/backend.rb +3 -1
- 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/diagnostics.rb +52 -0
- data/lib/schemurai/native/evaluator.rb +59 -0
- 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 +81 -33
- data/sig/schemurai.rbs +21 -13
- metadata +36 -6
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
//! Resource/anchor lookup is compiled once. Evaluation only follows native
|
|
2
|
+
//! tables and stack frames, including when speculative branches collect annotations.
|
|
3
|
+
use super::*;
|
|
4
|
+
use tracking::JsonKey;
|
|
5
|
+
#[derive(Clone, Copy, Default)]
|
|
6
|
+
pub(super) struct Metadata {
|
|
7
|
+
pub resource: Value,
|
|
8
|
+
pub enters_scope: bool,
|
|
9
|
+
pub recursive_anchor: bool,
|
|
10
|
+
pub dynamic_anchor: Option<Value>,
|
|
11
|
+
pub primary_binding: Option<usize>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
mod tests {
|
|
16
|
+
use super::*;
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn requires_guard_histories_for_cycles_reached_through_dynamic_overrides() {
|
|
20
|
+
let reference = |target, kind| Op::Ref(Reference { target, kind });
|
|
21
|
+
let mut engine = Engine {
|
|
22
|
+
nodes: vec![
|
|
23
|
+
vec![Op::Boolean(true)],
|
|
24
|
+
vec![reference(0, ReferenceKind::Dynamic(0))],
|
|
25
|
+
vec![reference(1, ReferenceKind::Static)],
|
|
26
|
+
vec![reference(1, ReferenceKind::Static)],
|
|
27
|
+
vec![reference(1, ReferenceKind::Static)],
|
|
28
|
+
],
|
|
29
|
+
anchor_tables: vec![vec![(100, 0), (101, 1)]],
|
|
30
|
+
..Engine::default()
|
|
31
|
+
};
|
|
32
|
+
engine.compile_memoization();
|
|
33
|
+
assert!(engine.memoizable[1]);
|
|
34
|
+
assert!(engine.memo_guarded[1]);
|
|
35
|
+
engine.nodes.pop();
|
|
36
|
+
engine.compile_memoization();
|
|
37
|
+
assert!(!engine.memoizable[1]);
|
|
38
|
+
assert!(engine.memo_guard_sources[1]);
|
|
39
|
+
engine.nodes.push(vec![reference(1, ReferenceKind::Static)]);
|
|
40
|
+
engine.nodes[0] = vec![Op::Unsupported];
|
|
41
|
+
engine.compile_memoization();
|
|
42
|
+
assert!(!engine.memoizable[1]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn removes_scope_keys_only_for_proven_invariant_dynamic_targets() {
|
|
47
|
+
let reference = |target, kind| Op::Ref(Reference { target, kind });
|
|
48
|
+
let mut engine = Engine {
|
|
49
|
+
nodes: vec![
|
|
50
|
+
vec![Op::Boolean(true)],
|
|
51
|
+
vec![reference(0, ReferenceKind::Dynamic(0))],
|
|
52
|
+
vec![reference(0, ReferenceKind::Dynamic(0))],
|
|
53
|
+
vec![Op::All(vec![1, 2])],
|
|
54
|
+
vec![reference(3, ReferenceKind::Static)],
|
|
55
|
+
vec![reference(3, ReferenceKind::Static)],
|
|
56
|
+
vec![reference(0, ReferenceKind::Dynamic(1))],
|
|
57
|
+
vec![reference(0, ReferenceKind::Dynamic(2))],
|
|
58
|
+
vec![reference(0, ReferenceKind::Dynamic(3))],
|
|
59
|
+
vec![reference(0, ReferenceKind::Recursive)],
|
|
60
|
+
],
|
|
61
|
+
anchor_tables: vec![
|
|
62
|
+
vec![(100, 0), (101, 0)],
|
|
63
|
+
vec![(100, 0), (101, 3)],
|
|
64
|
+
vec![(100, 3)],
|
|
65
|
+
vec![],
|
|
66
|
+
],
|
|
67
|
+
..Engine::default()
|
|
68
|
+
};
|
|
69
|
+
engine.compile_memoization();
|
|
70
|
+
assert!(engine.memoizable[3]);
|
|
71
|
+
assert!(engine.memo_contextual[3]);
|
|
72
|
+
engine.simplify_dynamic_references();
|
|
73
|
+
engine.compile_memoization();
|
|
74
|
+
assert!(engine.memoizable[3]);
|
|
75
|
+
assert!(!engine.memo_contextual[3]);
|
|
76
|
+
for node in [1, 2] {
|
|
77
|
+
let Op::Ref(reference) = engine.nodes[node][0] else {
|
|
78
|
+
panic!()
|
|
79
|
+
};
|
|
80
|
+
assert_eq!(reference.static_target(), Some(0));
|
|
81
|
+
}
|
|
82
|
+
// Changing bindings, fallback mismatches, absent bindings and
|
|
83
|
+
// recursive references retain context-sensitive resolution.
|
|
84
|
+
for node in [6, 7, 8, 9] {
|
|
85
|
+
let Op::Ref(reference) = engine.nodes[node][0] else {
|
|
86
|
+
panic!()
|
|
87
|
+
};
|
|
88
|
+
assert_eq!(reference.static_target(), None);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
#[derive(Clone, Copy, Debug)]
|
|
93
|
+
enum ReferenceKind {
|
|
94
|
+
Static,
|
|
95
|
+
Recursive,
|
|
96
|
+
Dynamic(usize),
|
|
97
|
+
}
|
|
98
|
+
#[derive(Clone, Copy, Debug)]
|
|
99
|
+
pub(super) struct Reference {
|
|
100
|
+
target: usize,
|
|
101
|
+
kind: ReferenceKind,
|
|
102
|
+
}
|
|
103
|
+
#[derive(Clone, Copy, Default)]
|
|
104
|
+
pub(super) struct Context<'a> {
|
|
105
|
+
pub active: Option<&'a RefFrame<'a>>,
|
|
106
|
+
pub scope: Option<&'a ScopeFrame<'a>>,
|
|
107
|
+
}
|
|
108
|
+
pub(super) struct ScopeFrame<'a> {
|
|
109
|
+
pub resource: Value,
|
|
110
|
+
pub parent: Option<&'a ScopeFrame<'a>>,
|
|
111
|
+
pub primary_binding: Option<usize>,
|
|
112
|
+
}
|
|
113
|
+
impl Reference {
|
|
114
|
+
pub(super) fn dynamic_target(self) -> Option<(usize, usize)> {
|
|
115
|
+
match self.kind {
|
|
116
|
+
ReferenceKind::Dynamic(name) => Some((name, self.target)),
|
|
117
|
+
_ => None,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
pub(super) fn recursive_target(self) -> Option<usize> {
|
|
121
|
+
matches!(self.kind, ReferenceKind::Recursive).then_some(self.target)
|
|
122
|
+
}
|
|
123
|
+
pub(super) fn static_target(self) -> Option<usize> {
|
|
124
|
+
matches!(self.kind, ReferenceKind::Static).then_some(self.target)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
impl Engine {
|
|
128
|
+
pub(super) fn simplify_dynamic_references(&mut self) {
|
|
129
|
+
// Anchor binding has reached its fixed point, including resources
|
|
130
|
+
// compiled while discovering overrides. A name whose every binding
|
|
131
|
+
// equals the fallback target cannot change under any native scope.
|
|
132
|
+
// Preserve the source opcode for diagnostic paths, while allowing
|
|
133
|
+
// ordinary acyclic static-target memoization for the shared graph.
|
|
134
|
+
let invariant: Vec<_> = self
|
|
135
|
+
.anchor_tables
|
|
136
|
+
.iter()
|
|
137
|
+
.map(|bindings| {
|
|
138
|
+
let &(_, target) = bindings.first()?;
|
|
139
|
+
bindings
|
|
140
|
+
.iter()
|
|
141
|
+
.all(|&(_, candidate)| candidate == target)
|
|
142
|
+
.then_some(target)
|
|
143
|
+
})
|
|
144
|
+
.collect();
|
|
145
|
+
for ops in &mut self.nodes {
|
|
146
|
+
for op in ops {
|
|
147
|
+
if let Op::Ref(reference) = op
|
|
148
|
+
&& let ReferenceKind::Dynamic(name) = reference.kind
|
|
149
|
+
&& invariant[name] == Some(reference.target)
|
|
150
|
+
{
|
|
151
|
+
reference.kind = ReferenceKind::Static;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
pub(super) fn compile_scope(
|
|
158
|
+
&mut self,
|
|
159
|
+
program: Value,
|
|
160
|
+
index: usize,
|
|
161
|
+
flags: i64,
|
|
162
|
+
) -> Result<(), Value> {
|
|
163
|
+
let node = self.field(program, b"node\0")?;
|
|
164
|
+
let resource = self.field(node, b"resource\0")?;
|
|
165
|
+
let recursive = self.field(program, b"recursive_anchor?\0")?;
|
|
166
|
+
let dynamic = self.field(program, b"dynamic_anchor\0")?;
|
|
167
|
+
self.metadata[index] = Metadata {
|
|
168
|
+
resource,
|
|
169
|
+
enters_scope: flags & i64::from(TRACKS_DYNAMIC_SCOPE) != 0,
|
|
170
|
+
recursive_anchor: unsafe { sn_true(recursive) } != 0,
|
|
171
|
+
primary_binding: None,
|
|
172
|
+
dynamic_anchor: (unsafe { sn_same(dynamic, sn_nil()) } == 0).then_some(dynamic),
|
|
173
|
+
};
|
|
174
|
+
if !self.resources.contains_key(&resource) {
|
|
175
|
+
// Reserve the resource before compiling its root: roots can reach
|
|
176
|
+
// the current child through a recursive or dynamic reference.
|
|
177
|
+
self.resources.insert(resource, usize::MAX);
|
|
178
|
+
let mut root = 0;
|
|
179
|
+
if unsafe { sn_resource_program(self.compiler, resource, &mut root) } != 0 {
|
|
180
|
+
return Err(root);
|
|
181
|
+
}
|
|
182
|
+
self.retain(root);
|
|
183
|
+
let root = self.compile(root)?;
|
|
184
|
+
self.resources.insert(resource, root);
|
|
185
|
+
}
|
|
186
|
+
Ok(())
|
|
187
|
+
}
|
|
188
|
+
pub(super) fn compile_reference(
|
|
189
|
+
&mut self,
|
|
190
|
+
operand: Value,
|
|
191
|
+
opcode: i32,
|
|
192
|
+
target: usize,
|
|
193
|
+
) -> Result<Op, Value> {
|
|
194
|
+
let fragment = self.field(operand, b"fragment\0")?;
|
|
195
|
+
let kind = if opcode == OP_RECURSIVE_REF
|
|
196
|
+
&& unsafe { sn_same(fragment, sn_nil()) } == 0
|
|
197
|
+
&& unsafe { sn_string_bytes(fragment) } == 0
|
|
198
|
+
&& self.metadata[target].recursive_anchor
|
|
199
|
+
{
|
|
200
|
+
ReferenceKind::Recursive
|
|
201
|
+
} else if opcode == OP_DYNAMIC_REF {
|
|
202
|
+
let anchor = self.metadata[target].dynamic_anchor;
|
|
203
|
+
match anchor {
|
|
204
|
+
Some(anchor)
|
|
205
|
+
if unsafe { sn_same(fragment, sn_nil()) } == 0
|
|
206
|
+
&& unsafe { sn_string_bytes(fragment) } != 0
|
|
207
|
+
&& unsafe { *sn_string_data(fragment) } != b'/'
|
|
208
|
+
&& unsafe { sn_string_equal(fragment, anchor) } != 0 =>
|
|
209
|
+
{
|
|
210
|
+
let name = JsonKey::new(fragment);
|
|
211
|
+
let index = if let Some(&index) = self.anchor_ids.get(&name) {
|
|
212
|
+
index
|
|
213
|
+
} else {
|
|
214
|
+
let index = self.anchor_names.len();
|
|
215
|
+
self.anchor_names.push(name);
|
|
216
|
+
self.anchor_ids.insert(name, index);
|
|
217
|
+
index
|
|
218
|
+
};
|
|
219
|
+
ReferenceKind::Dynamic(index)
|
|
220
|
+
}
|
|
221
|
+
_ => ReferenceKind::Static,
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
ReferenceKind::Static
|
|
225
|
+
};
|
|
226
|
+
Ok(Op::Ref(Reference { target, kind }))
|
|
227
|
+
}
|
|
228
|
+
pub(super) fn bind_dynamic_anchors(&mut self) -> Result<(), Value> {
|
|
229
|
+
// Names append during compilation. A resolved prefix per resource
|
|
230
|
+
// records negative lookups without retaining their Cartesian product.
|
|
231
|
+
// Snapshot only pending rows; compiling a binding can add resources
|
|
232
|
+
// and names, which the next pass must resolve too.
|
|
233
|
+
let mut resolved = HashMap::new();
|
|
234
|
+
loop {
|
|
235
|
+
let names = self.anchor_names.len();
|
|
236
|
+
if names > 1 {
|
|
237
|
+
return self.bind_indexed_dynamic_anchors();
|
|
238
|
+
}
|
|
239
|
+
let pending: Vec<_> = self
|
|
240
|
+
.resources
|
|
241
|
+
.keys()
|
|
242
|
+
.filter_map(|&resource| {
|
|
243
|
+
let first = resolved.get(&resource).copied().unwrap_or(0);
|
|
244
|
+
(first < names).then_some((resource, first..names))
|
|
245
|
+
})
|
|
246
|
+
.collect();
|
|
247
|
+
if pending.is_empty() {
|
|
248
|
+
self.finish_dynamic_bindings();
|
|
249
|
+
return Ok(());
|
|
250
|
+
}
|
|
251
|
+
for (resource, names) in pending {
|
|
252
|
+
let end = names.end;
|
|
253
|
+
for name in names {
|
|
254
|
+
let mut target = 0;
|
|
255
|
+
if unsafe {
|
|
256
|
+
sn_anchor_program(
|
|
257
|
+
self.compiler,
|
|
258
|
+
resource,
|
|
259
|
+
self.anchor_names[name].0,
|
|
260
|
+
&mut target,
|
|
261
|
+
)
|
|
262
|
+
} != 0
|
|
263
|
+
{
|
|
264
|
+
return Err(target);
|
|
265
|
+
}
|
|
266
|
+
if unsafe { sn_same(target, sn_nil()) } == 0 {
|
|
267
|
+
self.retain(target);
|
|
268
|
+
let target = self.compile(target)?;
|
|
269
|
+
self.dynamic_bindings.insert((resource, name), target);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
resolved.insert(resource, end);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
fn finish_dynamic_bindings(&mut self) {
|
|
277
|
+
self.anchor_tables = vec![vec![]; self.anchor_names.len()];
|
|
278
|
+
for (&(resource, name), &target) in &self.dynamic_bindings {
|
|
279
|
+
self.anchor_tables[name].push((resource, target));
|
|
280
|
+
}
|
|
281
|
+
if self.anchor_names.len() == 1 {
|
|
282
|
+
for metadata in &mut self.metadata {
|
|
283
|
+
// Use the existing resource/name index instead of an
|
|
284
|
+
// O(binding count) scan for every compiled node.
|
|
285
|
+
metadata.primary_binding =
|
|
286
|
+
self.dynamic_bindings.get(&(metadata.resource, 0)).copied();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Compiler-only name indexes cannot be reused at runtime. Drop
|
|
290
|
+
// their allocations and temporary GC roots after the fixed point.
|
|
291
|
+
drop(std::mem::take(&mut self.anchor_ids));
|
|
292
|
+
drop(std::mem::take(
|
|
293
|
+
&mut unsafe { &mut *self.roots }.binding_names,
|
|
294
|
+
));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fn bind_indexed_dynamic_anchors(&mut self) -> Result<(), Value> {
|
|
298
|
+
// Join registered names with resources that actually bind them. Resource
|
|
299
|
+
// tables append names, including when external indexing augments an
|
|
300
|
+
// existing resource. An indexed prefix admits only newly added names.
|
|
301
|
+
let mut available: HashMap<JsonKey, Vec<Value>, BuildHasherDefault<BucketHasher>> =
|
|
302
|
+
HashMap::default();
|
|
303
|
+
let mut resolved_names = 0;
|
|
304
|
+
let mut indexed = HashMap::new();
|
|
305
|
+
loop {
|
|
306
|
+
let mut pending = vec![];
|
|
307
|
+
for &resource in self.resources.keys() {
|
|
308
|
+
let first = indexed.get(&resource).copied().unwrap_or(0);
|
|
309
|
+
if unsafe { sn_anchor_count(resource) } == first {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let mut names = 0;
|
|
313
|
+
if unsafe { sn_anchor_names(self.compiler, resource, &mut names) } != 0 {
|
|
314
|
+
return Err(names);
|
|
315
|
+
}
|
|
316
|
+
// Copy handles before any Ruby callback. The snapshot Array
|
|
317
|
+
// is never borrowed across allocation, and temporary roots
|
|
318
|
+
// pin its names while the reverse index remains live.
|
|
319
|
+
let count = unsafe { sn_len(names) };
|
|
320
|
+
for i in first..count {
|
|
321
|
+
let name = unsafe { sn_entry(names, i) };
|
|
322
|
+
unsafe { &mut *self.roots }.binding_names.push(name);
|
|
323
|
+
let name = JsonKey::new(name);
|
|
324
|
+
available.entry(name).or_default().push(resource);
|
|
325
|
+
if let Some(&id) = self.anchor_ids.get(&name)
|
|
326
|
+
&& id < resolved_names
|
|
327
|
+
{
|
|
328
|
+
pending.push((resource, id));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
indexed.insert(resource, count);
|
|
332
|
+
}
|
|
333
|
+
let names = self.anchor_names.len();
|
|
334
|
+
for id in resolved_names..names {
|
|
335
|
+
if let Some(resources) = available.get(&self.anchor_names[id]) {
|
|
336
|
+
pending.extend(resources.iter().map(|&resource| (resource, id)));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
resolved_names = names;
|
|
340
|
+
if pending.is_empty() {
|
|
341
|
+
self.finish_dynamic_bindings();
|
|
342
|
+
return Ok(());
|
|
343
|
+
}
|
|
344
|
+
for (resource, name) in pending {
|
|
345
|
+
// Switching from single-name discovery can revisit a positive
|
|
346
|
+
// pair already compiled. It must not invoke compilation twice.
|
|
347
|
+
if self.dynamic_bindings.contains_key(&(resource, name)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
let mut target = 0;
|
|
351
|
+
if unsafe {
|
|
352
|
+
sn_anchor_program(
|
|
353
|
+
self.compiler,
|
|
354
|
+
resource,
|
|
355
|
+
self.anchor_names[name].0,
|
|
356
|
+
&mut target,
|
|
357
|
+
)
|
|
358
|
+
} != 0
|
|
359
|
+
{
|
|
360
|
+
return Err(target);
|
|
361
|
+
}
|
|
362
|
+
if unsafe { sn_same(target, sn_nil()) } == 0 {
|
|
363
|
+
self.retain(target);
|
|
364
|
+
let target = self.compile(target)?;
|
|
365
|
+
self.dynamic_bindings.insert((resource, name), target);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
pub(super) fn enter_scope<'a>(
|
|
371
|
+
&self,
|
|
372
|
+
node: usize,
|
|
373
|
+
active: Context<'a>,
|
|
374
|
+
frame: &'a ScopeFrame<'a>,
|
|
375
|
+
) -> Context<'a> {
|
|
376
|
+
if self.metadata[node].enters_scope
|
|
377
|
+
&& active
|
|
378
|
+
.scope
|
|
379
|
+
.is_none_or(|scope| scope.resource != frame.resource)
|
|
380
|
+
{
|
|
381
|
+
Context {
|
|
382
|
+
scope: Some(frame),
|
|
383
|
+
..active
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
active
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
pub(super) fn reference_target(&self, reference: Reference, context: Context<'_>) -> usize {
|
|
390
|
+
if matches!(reference.kind, ReferenceKind::Dynamic(0)) && self.anchor_tables.len() == 1 {
|
|
391
|
+
return context
|
|
392
|
+
.scope
|
|
393
|
+
.and_then(|frame| frame.primary_binding)
|
|
394
|
+
.unwrap_or(reference.target);
|
|
395
|
+
}
|
|
396
|
+
let mut target = reference.target;
|
|
397
|
+
let mut current = context.scope;
|
|
398
|
+
// Linked frames walk inner to outer. Keep replacing a match so the
|
|
399
|
+
// outermost matching resource wins, just as the VM's scope.each does.
|
|
400
|
+
while let Some(frame) = current {
|
|
401
|
+
match reference.kind {
|
|
402
|
+
ReferenceKind::Static => break,
|
|
403
|
+
ReferenceKind::Recursive => {
|
|
404
|
+
if let Some(&root) = self.resources.get(&frame.resource)
|
|
405
|
+
&& self.metadata[root].recursive_anchor
|
|
406
|
+
{
|
|
407
|
+
target = root;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
ReferenceKind::Dynamic(name) => {
|
|
411
|
+
if let Some(binding) = self.dynamic_bindings.get(&(frame.resource, name)) {
|
|
412
|
+
target = *binding;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
current = frame.parent;
|
|
417
|
+
}
|
|
418
|
+
target
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! One comparison/hash owns its cache. Ruby values cannot mutate or move
|
|
2
|
+
//! during these GVL-held traversals, which never call back into Ruby.
|
|
3
|
+
use std::collections::{HashMap, VecDeque};
|
|
4
|
+
use std::hash::Hash;
|
|
5
|
+
|
|
6
|
+
const MAX_ENTRIES: usize = 4096;
|
|
7
|
+
|
|
8
|
+
pub(super) struct Cache<K, T> {
|
|
9
|
+
start_depth: usize,
|
|
10
|
+
shallow_width: usize,
|
|
11
|
+
results: HashMap<K, T>,
|
|
12
|
+
order: VecDeque<K>,
|
|
13
|
+
}
|
|
14
|
+
impl<K, T> Cache<K, T> {
|
|
15
|
+
// A batch can share wide containers below distinct element roots. Keep
|
|
16
|
+
// small shallow trees cheap; four expanded levels are still a constant
|
|
17
|
+
// bound before depth-based reuse handles thin shared graphs.
|
|
18
|
+
pub(super) fn batch(depth: usize) -> Self {
|
|
19
|
+
let mut cache = Self::new(depth);
|
|
20
|
+
cache.shallow_width = 16;
|
|
21
|
+
cache
|
|
22
|
+
}
|
|
23
|
+
pub(super) fn new(depth: usize) -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
start_depth: depth.saturating_add(4),
|
|
26
|
+
shallow_width: usize::MAX,
|
|
27
|
+
results: HashMap::new(),
|
|
28
|
+
order: VecDeque::new(),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Shallow ordinary records/trees avoid table bookkeeping. At most four
|
|
32
|
+
// levels are expanded before reuse begins, independently of graph depth.
|
|
33
|
+
pub(super) fn eligible(&self, depth: usize) -> bool {
|
|
34
|
+
depth >= self.start_depth
|
|
35
|
+
}
|
|
36
|
+
pub(super) fn eligible_container(&self, depth: usize, width: usize) -> bool {
|
|
37
|
+
self.eligible(depth) || width >= self.shallow_width
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
impl<K: Copy + Eq + Hash, T: Copy> Cache<K, T> {
|
|
41
|
+
pub(super) fn get(&self, key: K) -> Option<T> {
|
|
42
|
+
self.results.get(&key).copied()
|
|
43
|
+
}
|
|
44
|
+
pub(super) fn record(&mut self, key: K, result: T) {
|
|
45
|
+
if self.results.contains_key(&key) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if self.results.len() == MAX_ENTRIES {
|
|
49
|
+
self.results.remove(&self.order.pop_front().unwrap());
|
|
50
|
+
}
|
|
51
|
+
self.results.insert(key, result);
|
|
52
|
+
self.order.push_back(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[cfg(test)]
|
|
57
|
+
mod tests {
|
|
58
|
+
use super::*;
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn batches_admit_wide_containers_without_charging_small_shallow_trees() {
|
|
62
|
+
let cache = Cache::<usize, usize>::batch(7);
|
|
63
|
+
assert!(!cache.eligible_container(7, 2));
|
|
64
|
+
assert!(!cache.eligible_container(10, 15));
|
|
65
|
+
assert!(cache.eligible_container(8, 16));
|
|
66
|
+
assert!(cache.eligible_container(11, 0));
|
|
67
|
+
assert!(!Cache::<usize, usize>::new(7).eligible_container(8, 1024));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn separates_depth_and_admits_later_shared_values() {
|
|
72
|
+
let mut cache = Cache::new(0);
|
|
73
|
+
for value in 0..MAX_ENTRIES * 3 {
|
|
74
|
+
cache.record((value, 0), Some(value));
|
|
75
|
+
cache.record((value, 0), None);
|
|
76
|
+
}
|
|
77
|
+
assert_eq!(cache.results.len(), MAX_ENTRIES);
|
|
78
|
+
assert_eq!(cache.order.len(), MAX_ENTRIES);
|
|
79
|
+
assert_eq!(cache.get((0, 0)), None);
|
|
80
|
+
assert_eq!(
|
|
81
|
+
cache.get((MAX_ENTRIES * 3 - 1, 0)),
|
|
82
|
+
Some(Some(MAX_ENTRIES * 3 - 1))
|
|
83
|
+
);
|
|
84
|
+
assert_eq!(cache.get((MAX_ENTRIES * 3 - 1, 1)), None);
|
|
85
|
+
}
|
|
86
|
+
}
|