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
|
@@ -10,6 +10,85 @@ pub(super) struct Metadata {
|
|
|
10
10
|
pub dynamic_anchor: Option<Value>,
|
|
11
11
|
pub primary_binding: Option<usize>,
|
|
12
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
|
+
}
|
|
13
92
|
#[derive(Clone, Copy, Debug)]
|
|
14
93
|
enum ReferenceKind {
|
|
15
94
|
Static,
|
|
@@ -31,7 +110,50 @@ pub(super) struct ScopeFrame<'a> {
|
|
|
31
110
|
pub parent: Option<&'a ScopeFrame<'a>>,
|
|
32
111
|
pub primary_binding: Option<usize>,
|
|
33
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
|
+
}
|
|
34
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
|
+
|
|
35
157
|
pub(super) fn compile_scope(
|
|
36
158
|
&mut self,
|
|
37
159
|
program: Value,
|
|
@@ -86,15 +208,14 @@ impl Engine {
|
|
|
86
208
|
&& unsafe { sn_string_equal(fragment, anchor) } != 0 =>
|
|
87
209
|
{
|
|
88
210
|
let name = JsonKey::new(fragment);
|
|
89
|
-
let index = self
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
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
|
+
};
|
|
98
219
|
ReferenceKind::Dynamic(index)
|
|
99
220
|
}
|
|
100
221
|
_ => ReferenceKind::Static,
|
|
@@ -105,35 +226,127 @@ impl Engine {
|
|
|
105
226
|
Ok(Op::Ref(Reference { target, kind }))
|
|
106
227
|
}
|
|
107
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();
|
|
108
234
|
loop {
|
|
235
|
+
let names = self.anchor_names.len();
|
|
236
|
+
if names > 1 {
|
|
237
|
+
return self.bind_indexed_dynamic_anchors();
|
|
238
|
+
}
|
|
109
239
|
let pending: Vec<_> = self
|
|
110
240
|
.resources
|
|
111
241
|
.keys()
|
|
112
|
-
.
|
|
113
|
-
(
|
|
242
|
+
.filter_map(|&resource| {
|
|
243
|
+
let first = resolved.get(&resource).copied().unwrap_or(0);
|
|
244
|
+
(first < names).then_some((resource, first..names))
|
|
114
245
|
})
|
|
115
|
-
.filter(|key| !self.dynamic_bindings.contains_key(key))
|
|
116
246
|
.collect();
|
|
117
247
|
if pending.is_empty() {
|
|
118
|
-
self.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
270
|
}
|
|
123
271
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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));
|
|
132
329
|
}
|
|
133
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();
|
|
134
342
|
return Ok(());
|
|
135
343
|
}
|
|
136
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
|
+
}
|
|
137
350
|
let mut target = 0;
|
|
138
351
|
if unsafe {
|
|
139
352
|
sn_anchor_program(
|
|
@@ -146,13 +359,11 @@ impl Engine {
|
|
|
146
359
|
{
|
|
147
360
|
return Err(target);
|
|
148
361
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
self.dynamic_bindings.insert((resource, name), target);
|
|
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
|
+
}
|
|
156
367
|
}
|
|
157
368
|
}
|
|
158
369
|
}
|
|
@@ -197,8 +408,7 @@ impl Engine {
|
|
|
197
408
|
}
|
|
198
409
|
}
|
|
199
410
|
ReferenceKind::Dynamic(name) => {
|
|
200
|
-
if let Some(
|
|
201
|
-
{
|
|
411
|
+
if let Some(binding) = self.dynamic_bindings.get(&(frame.resource, name)) {
|
|
202
412
|
target = *binding;
|
|
203
413
|
}
|
|
204
414
|
}
|
|
@@ -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
|
+
}
|