yrby 0.8.0 → 0.9.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,561 @@
1
+ //! Machinery every live handle shares: how a handle finds its branch, how
2
+ //! Ruby indexes map onto yrs's, and how Ruby values cross into yrs and back.
3
+ //!
4
+ //! A handle holds the identity of its branch: a root's name, or the ID of
5
+ //! the item that holds a nested type. It never caches a branch pointer, and
6
+ //! it resolves the identity in every operation's transaction. Identity, not
7
+ //! position: a block someone inserts above an agent's paragraph does not move
8
+ //! the agent's handle onto it, and a handle to a deleted branch resolves to
9
+ //! nothing instead of to whatever took its place.
10
+ //!
11
+ //! Indexes are Ruby characters. Documents count in UTF-16 units, as Yjs does,
12
+ //! and every index crosses through `to_units`/`to_chars`, so an index never
13
+ //! lands inside a character and `String#index` results can be used as is.
14
+ //!
15
+ //! Ruby values are read and built only with the GVL held. Everything that
16
+ //! crosses into a `nogvl` closure is `Send` data with no Ruby in it.
17
+
18
+ use std::panic::{catch_unwind, AssertUnwindSafe};
19
+
20
+ use magnus::{
21
+ prelude::*, r_hash::ForEach, Error, Float, Integer, IntoValue, RArray, RHash, RString, Ruby,
22
+ Value,
23
+ };
24
+ use yrs::branch::{Branch, BranchID, BranchPtr};
25
+ use yrs::types::text::YChange;
26
+ use yrs::{
27
+ Any, Hook, In, IndexScope, MapPrelim, Number, Out, ReadTxn, SharedRef, StickyIndex, Text, Xml,
28
+ XmlOut, XmlTextRef,
29
+ };
30
+
31
+ use crate::{nogvl, yrb_error};
32
+
33
+ /// The origin of every transaction a handle opens. `Doc#edit` returns only
34
+ /// these, so an update applied from elsewhere while it runs stays out.
35
+ pub const ORIGIN: &str = "yrby";
36
+
37
+ /// The largest integer a double represents exactly (2^53).
38
+ const MAX_SAFE_INTEGER: u64 = 1 << 53;
39
+
40
+ /// A key no other observer uses. yrs registers an observer under a key and
41
+ /// removes it by that key, so two observers on one document need different
42
+ /// ones.
43
+ pub fn observer_key() -> yrs::Origin {
44
+ static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
45
+ NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
46
+ .into()
47
+ }
48
+
49
+ /// The branch `id` names, if it exists and has not been deleted.
50
+ pub fn resolve<S: SharedRef, T: ReadTxn>(txn: &T, id: &BranchID) -> Option<S> {
51
+ Hook::<S>::from(id.clone()).get(txn)
52
+ }
53
+
54
+ /// The identity a handle keeps for `shared`.
55
+ pub fn identity<S: SharedRef>(shared: &S) -> BranchID {
56
+ shared.hook().into()
57
+ }
58
+
59
+ /// A value stored in a map or an array, as the handle it takes: its kind
60
+ /// and identity. Read it inside the transaction. The value's branch pointer
61
+ /// can't leave it, since another thread can delete the branch, and its
62
+ /// commit frees it.
63
+ pub enum Child {
64
+ Map(BranchID),
65
+ Array(BranchID),
66
+ Text(BranchID),
67
+ /// A plain value, or nothing at all.
68
+ Other,
69
+ }
70
+
71
+ impl Child {
72
+ pub fn of(value: Option<Out>) -> Child {
73
+ match value {
74
+ Some(Out::YMap(m)) => Child::Map(identity(&m)),
75
+ Some(Out::YArray(a)) => Child::Array(identity(&a)),
76
+ Some(Out::YText(t)) => Child::Text(identity(&t)),
77
+ _ => Child::Other,
78
+ }
79
+ }
80
+ }
81
+
82
+ /// Run `f` without the GVL. Its error, or a panic inside yrs, becomes a
83
+ /// `Y::Error` instead of taking the process down.
84
+ pub fn guarded<F, R>(f: F) -> Result<R, Error>
85
+ where
86
+ F: FnOnce() -> Result<R, String> + Send,
87
+ R: Send,
88
+ {
89
+ nogvl(move || match catch_unwind(AssertUnwindSafe(f)) {
90
+ Ok(result) => result,
91
+ Err(panic) => Err(panic
92
+ .downcast_ref::<String>()
93
+ .cloned()
94
+ .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
95
+ .unwrap_or_else(|| "the document operation failed".to_string())),
96
+ })?
97
+ .map_err(yrb_error)
98
+ }
99
+
100
+ // --- indexes ---
101
+
102
+ /// What `insert(index, ...)` means in a sequence of `len`, as in Ruby: a
103
+ /// negative index counts from the end, so -1 is the end itself. Clamped.
104
+ pub fn insert_index(index: i64, len: u32) -> u32 {
105
+ let len = len as i64;
106
+ let i = if index < 0 { len + index + 1 } else { index };
107
+ i.clamp(0, len) as u32
108
+ }
109
+
110
+ /// The element `index` names in a sequence of `len`, as Ruby's `[]` reads
111
+ /// it: a negative index counts from the end, and an index outside is `None`.
112
+ pub fn element_index(index: i64, len: u32) -> Option<u32> {
113
+ let len = len as i64;
114
+ let i = if index < 0 { len + index } else { index };
115
+ (0..len).contains(&i).then_some(i as u32)
116
+ }
117
+
118
+ /// The span `delete(index, length)` removes, as `String#slice!` reads it, in
119
+ /// characters: `None` when `index` is outside the text.
120
+ pub fn delete_span(index: i64, length: i64, len: u32) -> Option<(u32, u32)> {
121
+ let len = len as i64;
122
+ let start = if index < 0 { len + index } else { index };
123
+ if start < 0 || start > len {
124
+ return None;
125
+ }
126
+ let count = length.clamp(0, len - start);
127
+ Some((start as u32, (start + count) as u32))
128
+ }
129
+
130
+ /// The document index for `insert(index, ...)`. Appending (-1), what a
131
+ /// streaming writer does chunk after chunk, goes straight to the end without
132
+ /// walking the sequence.
133
+ pub fn end_or_units<T: ReadTxn, S: Text>(txn: &T, seq: &S, index: i64) -> u32 {
134
+ if index == -1 {
135
+ seq.len(txn)
136
+ } else {
137
+ to_units(txn, seq, insert_index(index, char_len(txn, seq)))
138
+ }
139
+ }
140
+
141
+ /// A sequence's contents in order: strings, and embeds (a text node's
142
+ /// marker, a nested block), each of which counts as one character.
143
+ fn pieces<T: ReadTxn, S: Text>(txn: &T, seq: &S) -> Vec<Out> {
144
+ seq.diff(txn, YChange::identity)
145
+ .into_iter()
146
+ .map(|d| d.insert)
147
+ .collect()
148
+ }
149
+
150
+ /// How many characters `seq` holds, counting each embed as one.
151
+ pub fn char_len<T: ReadTxn, S: Text>(txn: &T, seq: &S) -> u32 {
152
+ pieces(txn, seq)
153
+ .iter()
154
+ .map(|piece| match piece {
155
+ Out::Any(Any::String(s)) => s.chars().count() as u32,
156
+ _ => 1,
157
+ })
158
+ .sum()
159
+ }
160
+
161
+ /// The document index (UTF-16 units) of character `chars` in `seq`.
162
+ pub fn to_units<T: ReadTxn, S: Text>(txn: &T, seq: &S, chars: u32) -> u32 {
163
+ let mut left = chars;
164
+ let mut units = 0;
165
+ for piece in pieces(txn, seq) {
166
+ if left == 0 {
167
+ break;
168
+ }
169
+ match piece {
170
+ Out::Any(Any::String(s)) => {
171
+ for c in s.chars() {
172
+ if left == 0 {
173
+ break;
174
+ }
175
+ units += c.len_utf16() as u32;
176
+ left -= 1;
177
+ }
178
+ }
179
+ _ => {
180
+ units += 1;
181
+ left -= 1;
182
+ }
183
+ }
184
+ }
185
+ units
186
+ }
187
+
188
+ /// The character index of document index `units` in `seq`.
189
+ pub fn to_chars<T: ReadTxn, S: Text>(txn: &T, seq: &S, units: u32) -> u32 {
190
+ let mut left = units;
191
+ let mut chars = 0;
192
+ for piece in pieces(txn, seq) {
193
+ if left == 0 {
194
+ break;
195
+ }
196
+ match piece {
197
+ Out::Any(Any::String(s)) => {
198
+ for c in s.chars() {
199
+ if left == 0 {
200
+ break;
201
+ }
202
+ left = left.saturating_sub(c.len_utf16() as u32);
203
+ chars += 1;
204
+ }
205
+ }
206
+ _ => {
207
+ left -= 1;
208
+ chars += 1;
209
+ }
210
+ }
211
+ }
212
+ chars
213
+ }
214
+
215
+ // --- blocks ---
216
+
217
+ /// The root `XmlText` named `root`. Editors like Lexical keep their document in
218
+ /// a root of this type. yrs has no root accessor for it, but a root's type is
219
+ /// local metadata: it is repaired on first lookup and never encoded, so we look
220
+ /// it up the way the reader does (as an XML fragment) and address the same
221
+ /// branch as an `XmlText`.
222
+ pub fn root_xml_text<T: ReadTxn>(txn: &T, root: &str) -> Option<XmlTextRef> {
223
+ let fragment = txn.get_xml_fragment(root)?;
224
+ let branch: &yrs::branch::Branch = fragment.as_ref();
225
+ Some(XmlTextRef::from(BranchPtr::from(branch)))
226
+ }
227
+
228
+ /// The root `branch` lives under, found by walking up its XML parents.
229
+ pub fn top(branch: BranchPtr) -> BranchPtr {
230
+ let mut at = branch;
231
+ while let Some(parent) = XmlTextRef::from(at).parent() {
232
+ let branch: &Branch = match &parent {
233
+ XmlOut::Element(e) => e.as_ref(),
234
+ XmlOut::Fragment(f) => f.as_ref(),
235
+ XmlOut::Text(t) => t.as_ref(),
236
+ };
237
+ at = BranchPtr::from(branch);
238
+ }
239
+ at
240
+ }
241
+
242
+ /// The `XmlText`s embedded in `parent`, in order.
243
+ pub fn embedded_xml_texts<T: ReadTxn>(txn: &T, parent: &XmlTextRef) -> Vec<XmlTextRef> {
244
+ pieces(txn, parent)
245
+ .into_iter()
246
+ .filter_map(|piece| match piece {
247
+ Out::YXmlText(x) => Some(x),
248
+ _ => None,
249
+ })
250
+ .collect()
251
+ }
252
+
253
+ /// The document index of the `n`-th `XmlText` embedded in `parent`.
254
+ pub fn embedded_xml_text_index<T: ReadTxn>(txn: &T, parent: &XmlTextRef, n: u32) -> Option<u32> {
255
+ let mut seen = 0u32;
256
+ let mut index = 0u32;
257
+ for piece in pieces(txn, parent) {
258
+ match &piece {
259
+ Out::YXmlText(_) => {
260
+ if seen == n {
261
+ return Some(index);
262
+ }
263
+ seen += 1;
264
+ index += 1;
265
+ }
266
+ Out::Any(Any::String(s)) => index += s.encode_utf16().count() as u32,
267
+ _ => index += 1,
268
+ }
269
+ }
270
+ None
271
+ }
272
+
273
+ /// Whether the last thing in `block` is an embedded `XmlText` (a link, say).
274
+ pub fn ends_with_xml_text<T: ReadTxn>(txn: &T, block: &XmlTextRef) -> bool {
275
+ matches!(pieces(txn, block).last(), Some(Out::YXmlText(_)))
276
+ }
277
+
278
+ /// The strings in `block`, markers skipped. Inline children (a link) join the
279
+ /// line and nested blocks (a list item) start a new one.
280
+ pub fn block_text<T: ReadTxn>(txn: &T, block: &XmlTextRef) -> String {
281
+ let mut out = String::new();
282
+ for piece in pieces(txn, block) {
283
+ match &piece {
284
+ Out::Any(Any::String(s)) => out.push_str(s),
285
+ Out::YXmlText(child) => {
286
+ let inline =
287
+ crate::read::is_inline_lexical_type(&crate::read::lexical_type(txn, child));
288
+ if !inline && !out.is_empty() {
289
+ out.push('\n');
290
+ }
291
+ out.push_str(&block_text(txn, child));
292
+ }
293
+ _ => {}
294
+ }
295
+ }
296
+ out
297
+ }
298
+
299
+ // --- positions ---
300
+
301
+ /// A relative position with no Ruby in it: inside a sequence it names the
302
+ /// item to the right (or left); at the end of a nested type it names the
303
+ /// type; at a root it names the root.
304
+ pub struct Position {
305
+ scope: IndexScope,
306
+ assoc: i32,
307
+ }
308
+
309
+ impl Position {
310
+ pub fn new(sticky: StickyIndex) -> Self {
311
+ Position {
312
+ assoc: sticky.assoc as i32,
313
+ scope: sticky.scope().clone(),
314
+ }
315
+ }
316
+
317
+ /// The `{type, tname, item, assoc}` hash Yjs uses (RelativePosition's
318
+ /// JSON), which editors carry in awareness as `anchorPos`/`focusPos`.
319
+ pub fn to_ruby(&self, ruby: &Ruby) -> Result<RHash, Error> {
320
+ let id = |id: &yrs::block::ID| -> Result<RHash, Error> {
321
+ let h = ruby.hash_new();
322
+ h.aset("client", id.client.get())?;
323
+ h.aset("clock", id.clock)?;
324
+ Ok(h)
325
+ };
326
+ let h = ruby.hash_new();
327
+ h.aset("type", ruby.qnil())?;
328
+ h.aset("tname", ruby.qnil())?;
329
+ h.aset("item", ruby.qnil())?;
330
+ match &self.scope {
331
+ IndexScope::Relative(item) => h.aset("item", id(item)?)?,
332
+ IndexScope::Nested(ty) => h.aset("type", id(ty)?)?,
333
+ IndexScope::Root(name) => h.aset("tname", name.to_string())?,
334
+ }
335
+ h.aset("assoc", self.assoc)?;
336
+ Ok(h)
337
+ }
338
+ }
339
+
340
+ // --- values ---
341
+
342
+ /// A `Send` intermediate: Ruby is read into this with the GVL held, then turned
343
+ /// into yrs input inside `nogvl` (no Ruby calls there).
344
+ pub enum InValue {
345
+ Any(Any),
346
+ Map(Vec<(String, InValue)>),
347
+ }
348
+
349
+ pub fn to_in(v: InValue) -> In {
350
+ match v {
351
+ InValue::Any(a) => In::Any(a),
352
+ InValue::Map(entries) => In::from(to_map_prelim(entries)),
353
+ }
354
+ }
355
+
356
+ /// A nested shared map from Ruby pairs (what an editor's node marker is).
357
+ pub fn to_map_prelim(entries: Vec<(String, InValue)>) -> MapPrelim {
358
+ entries.into_iter().map(|(k, cv)| (k, to_in(cv))).collect()
359
+ }
360
+
361
+ /// Flatten an `InValue` to `Any` (nested maps become `Any::Map` snapshots).
362
+ pub fn invalue_to_any(v: InValue) -> Any {
363
+ match v {
364
+ InValue::Any(a) => a,
365
+ InValue::Map(entries) => {
366
+ let mut hm = std::collections::HashMap::new();
367
+ for (k, cv) in entries {
368
+ hm.insert(k, invalue_to_any(cv));
369
+ }
370
+ Any::Map(std::sync::Arc::new(hm))
371
+ }
372
+ }
373
+ }
374
+
375
+ pub fn key_to_string(v: Value) -> Result<String, Error> {
376
+ if let Some(s) = RString::from_value(v) {
377
+ return s.to_string();
378
+ }
379
+ // Symbols and everything else: use to_s.
380
+ let s: String = v.funcall("to_s", ())?;
381
+ Ok(s)
382
+ }
383
+
384
+ /// Read a Ruby Hash into `Send` pairs with the GVL held.
385
+ pub fn hash_pairs(ruby: &Ruby, hash: RHash) -> Result<Vec<(String, InValue)>, Error> {
386
+ pairs_at(ruby, hash, 0)
387
+ }
388
+
389
+ /// How deep a Hash or Array written to a document may nest, as many levels as
390
+ /// JSON's default `max_nesting`. Converting one that contains itself would
391
+ /// otherwise recurse until the stack overflows.
392
+ pub const MAX_NESTING: usize = 100;
393
+
394
+ fn pairs_at(ruby: &Ruby, hash: RHash, depth: usize) -> Result<Vec<(String, InValue)>, Error> {
395
+ let mut pairs = Vec::new();
396
+ hash.foreach(|k: Value, v: Value| {
397
+ pairs.push((key_to_string(k)?, invalue_at(ruby, v, depth)?));
398
+ Ok(ForEach::Continue)
399
+ })?;
400
+ Ok(pairs)
401
+ }
402
+
403
+ /// Read a Ruby value into an `InValue`. GVL held. Ruby `Hash` becomes a live
404
+ /// nested map; `Array` an embedded array of primitives; primitives their `Any`.
405
+ pub fn ruby_to_invalue(ruby: &Ruby, v: Value) -> Result<InValue, Error> {
406
+ invalue_at(ruby, v, 0)
407
+ }
408
+
409
+ fn invalue_at(ruby: &Ruby, v: Value, depth: usize) -> Result<InValue, Error> {
410
+ if depth > MAX_NESTING {
411
+ return Err(Error::new(
412
+ ruby.exception_arg_error(),
413
+ format!("nesting of {depth} is too deep (at most {MAX_NESTING})"),
414
+ ));
415
+ }
416
+ if v.is_nil() {
417
+ return Ok(InValue::Any(Any::Null));
418
+ }
419
+ if v.equal(ruby.qtrue())? {
420
+ return Ok(InValue::Any(Any::Bool(true)));
421
+ }
422
+ if v.equal(ruby.qfalse())? {
423
+ return Ok(InValue::Any(Any::Bool(false)));
424
+ }
425
+ if let Some(h) = RHash::from_value(v) {
426
+ return Ok(InValue::Map(pairs_at(ruby, h, depth + 1)?));
427
+ }
428
+ if let Some(a) = RArray::from_value(v) {
429
+ let mut items: Vec<Any> = Vec::with_capacity(a.len());
430
+ for item in a.into_iter() {
431
+ items.push(invalue_to_any(invalue_at(ruby, item, depth + 1)?));
432
+ }
433
+ return Ok(InValue::Any(Any::Array(items.into())));
434
+ }
435
+ if let Some(i) = Integer::from_value(v) {
436
+ // yrs encodes an integer as a plain number whenever a double holds it,
437
+ // so JavaScript reads a number, not a BigInt that would break
438
+ // JSON.stringify and the bitwise math editors do on flags like
439
+ // `__format`.
440
+ return Ok(InValue::Any(Any::Number(Number::Int(i.to_i64()?))));
441
+ }
442
+ if let Some(f) = Float::from_value(v) {
443
+ return Ok(InValue::Any(Any::Number(Number::Float(f.to_f64()))));
444
+ }
445
+ if let Some(s) = RString::from_value(v) {
446
+ return Ok(InValue::Any(Any::String(s.to_string()?.into())));
447
+ }
448
+ // Fallback: stringify (covers Symbol and other to_s-able objects).
449
+ Ok(InValue::Any(Any::String(key_to_string(v)?.into())))
450
+ }
451
+
452
+ /// Build a Ruby value from an `Any`. GVL held.
453
+ pub fn any_to_ruby(ruby: &Ruby, a: &Any) -> Value {
454
+ match a {
455
+ Any::Null | Any::Undefined => ruby.qnil().as_value(),
456
+ Any::Bool(b) => (*b).into_value_with(ruby),
457
+ Any::Number(Number::Int(i)) => (*i).into_value_with(ruby),
458
+ // A whole number reads back as an Integer, as JSON.parse would give.
459
+ Any::Number(Number::Float(n)) if n.fract() == 0.0 && n.abs() <= MAX_SAFE_INTEGER as f64 => {
460
+ (*n as i64).into_value_with(ruby)
461
+ }
462
+ Any::Number(Number::Float(n)) => (*n).into_value_with(ruby),
463
+ Any::String(s) => s.as_ref().into_value_with(ruby),
464
+ Any::Buffer(buf) => ruby.str_from_slice(buf).as_value(),
465
+ Any::Array(items) => ruby
466
+ .ary_from_iter(items.iter().map(|it| any_to_ruby(ruby, it)))
467
+ .as_value(),
468
+ Any::Map(m) => {
469
+ let h = ruby.hash_new();
470
+ for (k, v) in m.iter() {
471
+ let _ = h.aset(k.as_str(), any_to_ruby(ruby, v));
472
+ }
473
+ h.as_value()
474
+ }
475
+ }
476
+ }
477
+
478
+ #[cfg(test)]
479
+ mod tests {
480
+ use super::*;
481
+ use yrs::{Doc, GetString, OffsetKind, Options, Transact};
482
+
483
+ fn utf16_doc() -> Doc {
484
+ Doc::with_options(Options {
485
+ offset_kind: OffsetKind::Utf16,
486
+ ..Options::default()
487
+ })
488
+ }
489
+
490
+ #[test]
491
+ fn insert_index_reads_negative_indexes_as_ruby_does() {
492
+ assert_eq!(insert_index(0, 3), 0);
493
+ assert_eq!(insert_index(-1, 3), 3);
494
+ assert_eq!(insert_index(-4, 3), 0);
495
+ assert_eq!(insert_index(99, 3), 3);
496
+ }
497
+
498
+ #[test]
499
+ fn element_index_is_none_outside() {
500
+ assert_eq!(element_index(-1, 3), Some(2));
501
+ assert_eq!(element_index(3, 3), None);
502
+ assert_eq!(element_index(-4, 3), None);
503
+ }
504
+
505
+ #[test]
506
+ fn delete_span_follows_slice_bang() {
507
+ assert_eq!(delete_span(1, 1, 3), Some((1, 2)));
508
+ assert_eq!(delete_span(-1, 5, 3), Some((2, 3)));
509
+ assert_eq!(delete_span(3, 1, 3), Some((3, 3)));
510
+ assert_eq!(delete_span(4, 1, 3), None);
511
+ assert_eq!(delete_span(-4, 1, 3), None);
512
+ }
513
+
514
+ #[test]
515
+ fn characters_map_to_utf16_units_and_back() {
516
+ let doc = utf16_doc();
517
+ let text = doc.get_or_insert_text("t");
518
+ let mut txn = doc.transact_mut();
519
+ text.insert(&mut txn, 0, "a😀é日");
520
+ assert_eq!(char_len(&txn, &text), 4);
521
+ assert_eq!(to_units(&txn, &text, 2), 3);
522
+ assert_eq!(to_units(&txn, &text, 4), 5);
523
+ assert_eq!(to_chars(&txn, &text, 3), 2);
524
+ let from = to_units(&txn, &text, 1);
525
+ text.remove_range(&mut txn, from, 2);
526
+ assert_eq!(text.get_string(&txn), "aé日");
527
+ }
528
+ }
529
+
530
+ /// What the handle tests share: a document set up as `Y::Doc.new` sets one
531
+ /// up, and Ruby values to call handle methods with. Tests that use these run
532
+ /// inside Ruby (`#[ruby_test]`), since the handles call into it.
533
+ #[cfg(test)]
534
+ pub mod test_support {
535
+ use magnus::{IntoValue, RHash, Ruby, Value};
536
+ use yrs::{Any, Doc, Number, OffsetKind, Options};
537
+
538
+ /// A document that counts text as `Y::Doc.new` does, in UTF-16 units.
539
+ pub fn doc() -> Doc {
540
+ Doc::with_options(Options {
541
+ offset_kind: OffsetKind::Utf16,
542
+ ..Options::default()
543
+ })
544
+ }
545
+
546
+ pub fn rb<T: IntoValue>(value: T) -> Value {
547
+ Ruby::get().unwrap().into_value(value)
548
+ }
549
+
550
+ pub fn hash(pairs: &[(&str, Value)]) -> RHash {
551
+ let hash = Ruby::get().unwrap().hash_new();
552
+ for (key, value) in pairs {
553
+ hash.aset(*key, *value).unwrap();
554
+ }
555
+ hash
556
+ }
557
+
558
+ pub fn int(value: i64) -> Any {
559
+ Any::Number(Number::Int(value))
560
+ }
561
+ }