@tishlang/tish-lint 2.12.0 → 2.35.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.
- package/Cargo.toml +3 -0
- package/bin/tish-lint +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
|
@@ -383,6 +383,177 @@ pub enum Value {
|
|
|
383
383
|
#[cfg(target_pointer_width = "64")]
|
|
384
384
|
const _: () = assert!(std::mem::size_of::<Value>() == 24);
|
|
385
385
|
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
387
|
+
// #201 Stage A — representation abstraction (behavior-preserving, zero perf effect).
|
|
388
|
+
//
|
|
389
|
+
// `Value` is an enum today. To eventually swap it for a NaN-boxed `struct Value(u64)`
|
|
390
|
+
// (#201 Stage C) WITHOUT thousands of simultaneous edits, call sites should go through
|
|
391
|
+
// this abstraction instead of matching the enum directly:
|
|
392
|
+
// * construct via the named constructors (`Value::number`, `::boolean`, `::string`, …),
|
|
393
|
+
// * inspect via `v.unpack()` (a borrowed view) or the `as_*` / `tag` accessors.
|
|
394
|
+
// Once sites are migrated, the representation changes behind these method bodies and the
|
|
395
|
+
// call sites are untouched. Stage B (the 24→16 size-shrink) is intentionally SKIPPED — it
|
|
396
|
+
// was tried and regressed dispatch ~8-10% (see the size-guard note above); only Stage C's
|
|
397
|
+
// branch-free tag test can pay off. This layer is pure enabling: no behavior, no perf.
|
|
398
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
/// Cheap type discriminant for a [`Value`]. `match v.tag()` will lower to a branch-free
|
|
401
|
+
/// tag test once `Value` is NaN-boxed (#201). Variant order/values are not part of any ABI.
|
|
402
|
+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
403
|
+
pub enum ValueTag {
|
|
404
|
+
Number,
|
|
405
|
+
String,
|
|
406
|
+
Bool,
|
|
407
|
+
Null,
|
|
408
|
+
Array,
|
|
409
|
+
NumberArray,
|
|
410
|
+
Object,
|
|
411
|
+
Symbol,
|
|
412
|
+
Function,
|
|
413
|
+
#[cfg(feature = "regex")]
|
|
414
|
+
RegExp,
|
|
415
|
+
Promise,
|
|
416
|
+
Opaque,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/// Borrowed view of a [`Value`]'s payload, mirroring the enum variants. Lets call sites
|
|
420
|
+
/// write `match v.unpack() { ValueRef::Number(n) => … }` instead of matching the enum
|
|
421
|
+
/// directly, so the underlying representation can change (#201) without touching the match
|
|
422
|
+
/// sites. Zero-cost — each arm borrows the existing payload in place.
|
|
423
|
+
pub enum ValueRef<'a> {
|
|
424
|
+
Number(f64),
|
|
425
|
+
String(&'a arcstr::ArcStr),
|
|
426
|
+
Bool(bool),
|
|
427
|
+
Null,
|
|
428
|
+
Array(&'a VmRef<Vec<Value>>),
|
|
429
|
+
NumberArray(&'a VmRef<Vec<f64>>),
|
|
430
|
+
Object(&'a VmRef<ObjectData>),
|
|
431
|
+
Symbol(&'a Arc<TishSymbol>),
|
|
432
|
+
Function(&'a NativeFn),
|
|
433
|
+
#[cfg(feature = "regex")]
|
|
434
|
+
RegExp(&'a VmRef<TishRegExp>),
|
|
435
|
+
Promise(&'a Arc<dyn TishPromise>),
|
|
436
|
+
Opaque(&'a Arc<dyn TishOpaque>),
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
impl Value {
|
|
440
|
+
/// Named constructor for a number (#201 abstraction). Prefer over `Value::Number(n)`.
|
|
441
|
+
#[inline]
|
|
442
|
+
pub fn number(n: f64) -> Self {
|
|
443
|
+
Value::Number(n)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Named constructor for a boolean (#201 abstraction). Prefer over `Value::Bool(b)`.
|
|
447
|
+
#[inline]
|
|
448
|
+
pub fn boolean(b: bool) -> Self {
|
|
449
|
+
Value::Bool(b)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// Named constructor for a string (#201 abstraction). Accepts anything that converts
|
|
453
|
+
/// into the interned `ArcStr` (`&str`, `String`, `ArcStr`).
|
|
454
|
+
#[inline]
|
|
455
|
+
pub fn string(s: impl Into<arcstr::ArcStr>) -> Self {
|
|
456
|
+
Value::String(s.into())
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Named constructor for null (#201 abstraction). Prefer over `Value::Null`.
|
|
460
|
+
#[inline]
|
|
461
|
+
pub fn null() -> Self {
|
|
462
|
+
Value::Null
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/// Borrowed view of the payload — the migration target for `match v { … }`.
|
|
466
|
+
#[inline]
|
|
467
|
+
pub fn unpack(&self) -> ValueRef<'_> {
|
|
468
|
+
match self {
|
|
469
|
+
Value::Number(n) => ValueRef::Number(*n),
|
|
470
|
+
Value::String(s) => ValueRef::String(s),
|
|
471
|
+
Value::Bool(b) => ValueRef::Bool(*b),
|
|
472
|
+
Value::Null => ValueRef::Null,
|
|
473
|
+
Value::Array(a) => ValueRef::Array(a),
|
|
474
|
+
Value::NumberArray(a) => ValueRef::NumberArray(a),
|
|
475
|
+
Value::Object(o) => ValueRef::Object(o),
|
|
476
|
+
Value::Symbol(s) => ValueRef::Symbol(s),
|
|
477
|
+
Value::Function(f) => ValueRef::Function(f),
|
|
478
|
+
#[cfg(feature = "regex")]
|
|
479
|
+
Value::RegExp(r) => ValueRef::RegExp(r),
|
|
480
|
+
Value::Promise(p) => ValueRef::Promise(p),
|
|
481
|
+
Value::Opaque(o) => ValueRef::Opaque(o),
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/// The value's type discriminant (#201 abstraction) — for cheap type checks.
|
|
486
|
+
#[inline]
|
|
487
|
+
pub fn tag(&self) -> ValueTag {
|
|
488
|
+
match self {
|
|
489
|
+
Value::Number(_) => ValueTag::Number,
|
|
490
|
+
Value::String(_) => ValueTag::String,
|
|
491
|
+
Value::Bool(_) => ValueTag::Bool,
|
|
492
|
+
Value::Null => ValueTag::Null,
|
|
493
|
+
Value::Array(_) => ValueTag::Array,
|
|
494
|
+
Value::NumberArray(_) => ValueTag::NumberArray,
|
|
495
|
+
Value::Object(_) => ValueTag::Object,
|
|
496
|
+
Value::Symbol(_) => ValueTag::Symbol,
|
|
497
|
+
Value::Function(_) => ValueTag::Function,
|
|
498
|
+
#[cfg(feature = "regex")]
|
|
499
|
+
Value::RegExp(_) => ValueTag::RegExp,
|
|
500
|
+
Value::Promise(_) => ValueTag::Promise,
|
|
501
|
+
Value::Opaque(_) => ValueTag::Opaque,
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/// Extract the boolean payload, if this is a `Bool`.
|
|
506
|
+
#[inline]
|
|
507
|
+
pub fn as_bool(&self) -> Option<bool> {
|
|
508
|
+
match self {
|
|
509
|
+
Value::Bool(b) => Some(*b),
|
|
510
|
+
_ => None,
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// Borrow the string payload as `&str`, if this is a `String`.
|
|
515
|
+
#[inline]
|
|
516
|
+
pub fn as_str(&self) -> Option<&str> {
|
|
517
|
+
match self {
|
|
518
|
+
Value::String(s) => Some(s.as_str()),
|
|
519
|
+
_ => None,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/// Borrow the object payload, if this is an `Object`.
|
|
524
|
+
#[inline]
|
|
525
|
+
pub fn as_object(&self) -> Option<&VmRef<ObjectData>> {
|
|
526
|
+
match self {
|
|
527
|
+
Value::Object(o) => Some(o),
|
|
528
|
+
_ => None,
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/// Borrow the array payload, if this is an `Array`.
|
|
533
|
+
#[inline]
|
|
534
|
+
pub fn as_array(&self) -> Option<&VmRef<Vec<Value>>> {
|
|
535
|
+
match self {
|
|
536
|
+
Value::Array(a) => Some(a),
|
|
537
|
+
_ => None,
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/// Borrow the packed-number-array payload, if this is a `NumberArray`.
|
|
542
|
+
#[inline]
|
|
543
|
+
pub fn as_number_array(&self) -> Option<&VmRef<Vec<f64>>> {
|
|
544
|
+
match self {
|
|
545
|
+
Value::NumberArray(a) => Some(a),
|
|
546
|
+
_ => None,
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/// True if this is `Null`.
|
|
551
|
+
#[inline]
|
|
552
|
+
pub fn is_null(&self) -> bool {
|
|
553
|
+
matches!(self, Value::Null)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
386
557
|
/// Number of properties kept inline (no heap hashmap) before promoting to a map.
|
|
387
558
|
const PROPMAP_INLINE: usize = 8;
|
|
388
559
|
|
|
@@ -591,6 +762,37 @@ impl PropMap {
|
|
|
591
762
|
m.reserve(additional);
|
|
592
763
|
}
|
|
593
764
|
}
|
|
765
|
+
|
|
766
|
+
/// Merge every entry of `other` into `self`, in `other`'s insertion order.
|
|
767
|
+
///
|
|
768
|
+
/// Existing keys are overwritten (last write wins), so this preserves JS object-spread
|
|
769
|
+
/// semantics: `{ ...a, ...b }` lets `b` override `a`. The work is pre-sized — if the merge
|
|
770
|
+
/// would push the result past the inline threshold we promote to an `IndexMap` once, with the
|
|
771
|
+
/// final capacity, instead of growing/rehashing one key at a time. The native codegen calls
|
|
772
|
+
/// this for every `{ ...src }` spread, replacing the old "rebuild via AHashMap" path.
|
|
773
|
+
pub fn merge_from(&mut self, other: &PropMap) {
|
|
774
|
+
let incoming = other.len();
|
|
775
|
+
if incoming == 0 {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
// If the combined worst-case size escapes inline storage, promote once up front so the
|
|
779
|
+
// per-key inserts below never reallocate or re-promote.
|
|
780
|
+
if self.map.is_none() && self.inline.len() + incoming > PROPMAP_INLINE {
|
|
781
|
+
let mut m: IndexMap<Arc<str>, Value, ahash::RandomState> = IndexMap::with_capacity_and_hasher(
|
|
782
|
+
self.inline.len() + incoming,
|
|
783
|
+
ahash::RandomState::default(),
|
|
784
|
+
);
|
|
785
|
+
for (k, v) in self.inline.drain(..) {
|
|
786
|
+
m.insert(k, v);
|
|
787
|
+
}
|
|
788
|
+
self.map = Some(Box::new(m));
|
|
789
|
+
} else if let Some(m) = &mut self.map {
|
|
790
|
+
m.reserve(incoming);
|
|
791
|
+
}
|
|
792
|
+
for (k, v) in other.iter() {
|
|
793
|
+
self.insert(Arc::clone(k), v.clone());
|
|
794
|
+
}
|
|
795
|
+
}
|
|
594
796
|
}
|
|
595
797
|
|
|
596
798
|
impl FromIterator<(Arc<str>, Value)> for PropMap {
|
|
@@ -780,6 +982,12 @@ pub fn object_has(obj: &Value, key: &Value) -> bool {
|
|
|
780
982
|
/// A missing/absent `done` is treated as truthy so a malformed object can't spin forever;
|
|
781
983
|
/// the native iterators always set `done`, so well-formed iteration is exact.
|
|
782
984
|
pub fn drain_iterator(obj: &Value) -> Option<Vec<Value>> {
|
|
985
|
+
// A string iterates as its characters, each a single-char string (`[...s]`, `for (c of s)`,
|
|
986
|
+
// call-spread `f(...s)`, `Array.from(s)`). Every caller of this helper is a spread/for-of
|
|
987
|
+
// context, so treating a string as iterable here is always correct. (#440)
|
|
988
|
+
if let Value::String(s) = obj {
|
|
989
|
+
return Some(s.chars().map(|c| Value::String(c.to_string().into())).collect());
|
|
990
|
+
}
|
|
783
991
|
if !matches!(obj, Value::Object(_)) {
|
|
784
992
|
return None;
|
|
785
993
|
}
|
|
@@ -860,6 +1068,13 @@ pub fn to_uint32_value(v: &Value) -> u32 {
|
|
|
860
1068
|
}
|
|
861
1069
|
|
|
862
1070
|
/// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
|
|
1071
|
+
///
|
|
1072
|
+
/// Calling a NON-callable (e.g. a method on `null` whose read returned `null`, or any non-function
|
|
1073
|
+
/// value) is a JS `TypeError`. In the native/runtime path this used to `panic!` — an UNCATCHABLE
|
|
1074
|
+
/// process abort (#381 class). It now PARKS a catchable `TypeError` and returns the `null` sentinel;
|
|
1075
|
+
/// the throw surfaces at the caller's next pending-throw checkpoint (statement boundary in emitted
|
|
1076
|
+
/// native code, or the `has_pending_throw()` polls in the shared builtins), so `try { null.foo() }
|
|
1077
|
+
/// catch (e) { … }` recovers instead of aborting — matching the VM/interpreter/node.
|
|
863
1078
|
pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
864
1079
|
match callee {
|
|
865
1080
|
Value::Function(f) => f.call(args),
|
|
@@ -868,15 +1083,19 @@ pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
|
868
1083
|
if let Some(inner) = inner {
|
|
869
1084
|
return value_call(&inner, args);
|
|
870
1085
|
}
|
|
871
|
-
|
|
872
|
-
"
|
|
873
|
-
callee
|
|
874
|
-
);
|
|
1086
|
+
crate::set_pending_throw(crate::not_a_function_error(format!(
|
|
1087
|
+
"{} is not a function",
|
|
1088
|
+
callee.to_display_string()
|
|
1089
|
+
)));
|
|
1090
|
+
Value::Null
|
|
1091
|
+
}
|
|
1092
|
+
_ => {
|
|
1093
|
+
crate::set_pending_throw(crate::not_a_function_error(format!(
|
|
1094
|
+
"{} is not a function",
|
|
1095
|
+
callee.to_display_string()
|
|
1096
|
+
)));
|
|
1097
|
+
Value::Null
|
|
875
1098
|
}
|
|
876
|
-
_ => panic!(
|
|
877
|
-
"Not a function: tried to call {:?} as a function (e.g. method on Null when read failed)",
|
|
878
|
-
callee
|
|
879
|
-
),
|
|
880
1099
|
}
|
|
881
1100
|
}
|
|
882
1101
|
|
|
@@ -967,26 +1186,75 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
|
967
1186
|
return;
|
|
968
1187
|
}
|
|
969
1188
|
|
|
1189
|
+
// Fast path for exact integers within ±2^53 (the f64 "safe integer" range). ECMAScript ToString
|
|
1190
|
+
// renders any such integer in plain decimal (no sign-exponent — magnitude < 1e21 ⇒ `point <= 21`
|
|
1191
|
+
// in the general algorithm below), which `itoa` emits directly, bypassing the `format!("{:e}")`
|
|
1192
|
+
// round-trip + intermediate `String`/`char`-filter/`"0".repeat()`. This is the dominant case for
|
|
1193
|
+
// tally/counter/index loops (e.g. map keys like `"w" + (n % 1000)`). Bit-identical to the general
|
|
1194
|
+
// path for every integer it accepts: each such value is exactly representable in both f64 and i64,
|
|
1195
|
+
// so `value as i64` is exact (no saturation — `±2^53` is well inside i64), and the decimal form is
|
|
1196
|
+
// the same digits the general path would emit. Larger integers (where f64 can't represent
|
|
1197
|
+
// consecutive values) fall through to the general path unchanged.
|
|
1198
|
+
const MAX_SAFE_INT: f64 = 9_007_199_254_740_992.0; // 2^53
|
|
1199
|
+
if value.fract() == 0.0 && value.abs() <= MAX_SAFE_INT {
|
|
1200
|
+
let mut buf = itoa::Buffer::new();
|
|
1201
|
+
out.push_str(buf.format(value as i64));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
970
1205
|
let negative = value < 0.0;
|
|
971
|
-
// Shortest round-trip digits
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
let
|
|
1206
|
+
// Shortest round-trip digits via ryu, then reassembled into ECMAScript `Number::toString` form.
|
|
1207
|
+
// ryu writes the shortest correctly-rounded digits straight into a stack buffer with none of the
|
|
1208
|
+
// `core::fmt` `{:e}` Formatter machinery the previous version paid per call (profiled as ~40% of
|
|
1209
|
+
// JSON.stringify self-time on numeric payloads). number→string is the JSON / template-literal /
|
|
1210
|
+
// logging / `+=` primitive, so this is a broad win. The shortest round-trip digit *sequence* is
|
|
1211
|
+
// unique, so ryu emits the same digits as the old `{:e}` path → output stays byte-identical.
|
|
1212
|
+
let mut ryu_buf = ryu::Buffer::new();
|
|
1213
|
+
// Finite & non-zero here (NaN / ±∞ / ±0 handled above), so `format_finite` is safe and cheaper.
|
|
1214
|
+
let s = ryu_buf.format_finite(value.abs());
|
|
1215
|
+
|
|
1216
|
+
// Parse ryu's `<int>[.<frac>][e<exp>]` into significant digits + ECMAScript decimal point.
|
|
1217
|
+
// `point` = ECMAScript's `n`: value = digits × 10^(point − k), with `k` = significant-digit count.
|
|
1218
|
+
let (mant, exp) = match s.as_bytes().iter().position(|&c| c == b'e' || c == b'E') {
|
|
1219
|
+
Some(i) => (&s[..i], s[i + 1..].parse::<i32>().expect("ryu exponent")),
|
|
1220
|
+
None => (s, 0i32),
|
|
1221
|
+
};
|
|
1222
|
+
let (int_part, frac_part) = match mant.as_bytes().iter().position(|&c| c == b'.') {
|
|
1223
|
+
Some(i) => (&mant[..i], &mant[i + 1..]),
|
|
1224
|
+
None => (mant, ""),
|
|
1225
|
+
};
|
|
1226
|
+
let frac_len = frac_part.len() as i32;
|
|
1227
|
+
// Raw digit run = int_part ++ frac_part (ryu's mantissa, ≤ ~18 chars for an f64).
|
|
1228
|
+
let mut digits_buf = [0u8; 32];
|
|
1229
|
+
let mut n = 0usize;
|
|
1230
|
+
for &c in int_part.as_bytes().iter().chain(frac_part.as_bytes()) {
|
|
1231
|
+
digits_buf[n] = c;
|
|
1232
|
+
n += 1;
|
|
1233
|
+
}
|
|
1234
|
+
// Strip leading zeros (e.g. ryu "0.1" → raw "01" → "1") …
|
|
1235
|
+
let mut lead = 0usize;
|
|
1236
|
+
while lead + 1 < n && digits_buf[lead] == b'0' {
|
|
1237
|
+
lead += 1;
|
|
1238
|
+
}
|
|
1239
|
+
// … and trailing zeros (defensive — shortest form has none — folded back via `trail`).
|
|
1240
|
+
let mut end = n;
|
|
1241
|
+
while end - 1 > lead && digits_buf[end - 1] == b'0' {
|
|
1242
|
+
end -= 1;
|
|
1243
|
+
}
|
|
1244
|
+
let trail = (n - end) as i32;
|
|
1245
|
+
let digits = std::str::from_utf8(&digits_buf[lead..end]).expect("ascii digits");
|
|
980
1246
|
let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
|
|
981
|
-
let point =
|
|
1247
|
+
let point = k + exp - frac_len + trail;
|
|
982
1248
|
|
|
983
1249
|
if negative {
|
|
984
1250
|
out.push('-');
|
|
985
1251
|
}
|
|
986
1252
|
if k <= point && point <= 21 {
|
|
987
1253
|
// Integer, zero-padded: digits then (point − k) trailing zeros.
|
|
988
|
-
out.push_str(
|
|
989
|
-
|
|
1254
|
+
out.push_str(digits);
|
|
1255
|
+
for _ in 0..(point - k) {
|
|
1256
|
+
out.push('0');
|
|
1257
|
+
}
|
|
990
1258
|
} else if 0 < point && point <= 21 {
|
|
991
1259
|
// Decimal point inside the digit string.
|
|
992
1260
|
out.push_str(&digits[..point as usize]);
|
|
@@ -995,8 +1263,10 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
|
995
1263
|
} else if -6 < point && point <= 0 {
|
|
996
1264
|
// Leading-zero fraction: "0." then (−point) zeros then the digits.
|
|
997
1265
|
out.push_str("0.");
|
|
998
|
-
|
|
999
|
-
|
|
1266
|
+
for _ in 0..(-point) {
|
|
1267
|
+
out.push('0');
|
|
1268
|
+
}
|
|
1269
|
+
out.push_str(digits);
|
|
1000
1270
|
} else {
|
|
1001
1271
|
// Exponential: first digit, optional `.rest`, then `e±E`.
|
|
1002
1272
|
let e = point - 1;
|
|
@@ -1007,13 +1277,22 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
|
1007
1277
|
}
|
|
1008
1278
|
out.push('e');
|
|
1009
1279
|
out.push(if e >= 0 { '+' } else { '-' });
|
|
1010
|
-
|
|
1280
|
+
let mut ib = itoa::Buffer::new();
|
|
1281
|
+
out.push_str(ib.format(e.abs()));
|
|
1011
1282
|
}
|
|
1012
1283
|
}
|
|
1013
1284
|
|
|
1014
1285
|
impl Value {
|
|
1015
1286
|
/// Convert value to display string (for console output).
|
|
1016
1287
|
pub fn to_display_string(&self) -> String {
|
|
1288
|
+
self.to_display_string_guarded(&mut Vec::new())
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/// Cycle-safe recursive form (#381): `ancestors` holds the current path's container-cell pointers,
|
|
1292
|
+
/// so a self-referential array/object (`a.self = a`) renders `[Circular]` instead of recursing
|
|
1293
|
+
/// forever and wedging the thread. Ancestor-only (not all-visited), so a shared DAG node still
|
|
1294
|
+
/// renders in full.
|
|
1295
|
+
fn to_display_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
1017
1296
|
match self {
|
|
1018
1297
|
// Inspect form keeps the sign of negative zero (`console.log(-0)` → `-0`), unlike the
|
|
1019
1298
|
// ECMAScript ToString used by `to_js_string`. See `js_number_to_string`. (#247)
|
|
@@ -1023,8 +1302,17 @@ impl Value {
|
|
|
1023
1302
|
Value::Bool(b) => b.to_string(),
|
|
1024
1303
|
Value::Null => "null".to_string(),
|
|
1025
1304
|
Value::Array(arr) => {
|
|
1026
|
-
let
|
|
1027
|
-
|
|
1305
|
+
let ptr = arr.as_ptr();
|
|
1306
|
+
if ancestors.contains(&ptr) {
|
|
1307
|
+
return "[Circular]".to_string();
|
|
1308
|
+
}
|
|
1309
|
+
ancestors.push(ptr);
|
|
1310
|
+
let inner: Vec<String> = arr
|
|
1311
|
+
.borrow()
|
|
1312
|
+
.iter()
|
|
1313
|
+
.map(|v| v.to_display_string_guarded(ancestors))
|
|
1314
|
+
.collect();
|
|
1315
|
+
ancestors.pop();
|
|
1028
1316
|
format!("[{}]", inner.join(", "))
|
|
1029
1317
|
}
|
|
1030
1318
|
Value::NumberArray(arr) => {
|
|
@@ -1036,12 +1324,18 @@ impl Value {
|
|
|
1036
1324
|
format!("[{}]", inner.join(", "))
|
|
1037
1325
|
}
|
|
1038
1326
|
Value::Object(obj) => {
|
|
1327
|
+
let ptr = obj.as_ptr();
|
|
1328
|
+
if ancestors.contains(&ptr) {
|
|
1329
|
+
return "[Circular]".to_string();
|
|
1330
|
+
}
|
|
1331
|
+
ancestors.push(ptr);
|
|
1039
1332
|
let inner: Vec<String> = obj
|
|
1040
1333
|
.borrow()
|
|
1041
1334
|
.strings
|
|
1042
1335
|
.iter()
|
|
1043
|
-
.map(|(k, v)| format!("{}: {}", k.as_ref(), v.
|
|
1336
|
+
.map(|(k, v)| format!("{}: {}", k.as_ref(), v.to_display_string_guarded(ancestors)))
|
|
1044
1337
|
.collect();
|
|
1338
|
+
ancestors.pop();
|
|
1045
1339
|
format!("{{{}}}", inner.join(", "))
|
|
1046
1340
|
}
|
|
1047
1341
|
Value::Symbol(s) => {
|
|
@@ -1071,16 +1365,32 @@ impl Value {
|
|
|
1071
1365
|
/// renders as `"null"` here (matching `String(null)`); `join` itself maps `null`/`undefined`
|
|
1072
1366
|
/// elements to `""` *before* calling this, per the spec.
|
|
1073
1367
|
pub fn to_js_string(&self) -> String {
|
|
1368
|
+
self.to_js_string_guarded(&mut Vec::new())
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/// Cycle-safe `ToString` (#381): only arrays recurse here (objects render `[object Object]`), so a
|
|
1372
|
+
/// cyclic array via `"" + a` would otherwise hang. A back-reference joins as `""` — matching V8's
|
|
1373
|
+
/// `Array.prototype.join`, where a self-referential element contributes the empty string.
|
|
1374
|
+
fn to_js_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
1074
1375
|
match self {
|
|
1075
|
-
Value::Array(arr) =>
|
|
1076
|
-
.
|
|
1077
|
-
.
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1376
|
+
Value::Array(arr) => {
|
|
1377
|
+
let ptr = arr.as_ptr();
|
|
1378
|
+
if ancestors.contains(&ptr) {
|
|
1379
|
+
return String::new();
|
|
1380
|
+
}
|
|
1381
|
+
ancestors.push(ptr);
|
|
1382
|
+
let s = arr
|
|
1383
|
+
.borrow()
|
|
1384
|
+
.iter()
|
|
1385
|
+
.map(|v| match v {
|
|
1386
|
+
Value::Null => String::new(),
|
|
1387
|
+
other => other.to_js_string_guarded(ancestors),
|
|
1388
|
+
})
|
|
1389
|
+
.collect::<Vec<_>>()
|
|
1390
|
+
.join(",");
|
|
1391
|
+
ancestors.pop();
|
|
1392
|
+
s
|
|
1393
|
+
}
|
|
1084
1394
|
Value::NumberArray(arr) => arr
|
|
1085
1395
|
.borrow()
|
|
1086
1396
|
.iter()
|
|
@@ -1354,6 +1664,46 @@ impl Value {
|
|
|
1354
1664
|
}
|
|
1355
1665
|
}
|
|
1356
1666
|
|
|
1667
|
+
#[cfg(test)]
|
|
1668
|
+
mod value_abstraction_tests {
|
|
1669
|
+
// #201 Stage A — the abstraction must exactly mirror the enum it will replace.
|
|
1670
|
+
use super::{Value, ValueRef, ValueTag};
|
|
1671
|
+
|
|
1672
|
+
#[test]
|
|
1673
|
+
fn named_ctors_report_the_right_tag() {
|
|
1674
|
+
assert_eq!(Value::number(1.5).tag(), ValueTag::Number);
|
|
1675
|
+
assert_eq!(Value::boolean(true).tag(), ValueTag::Bool);
|
|
1676
|
+
assert_eq!(Value::string("hi").tag(), ValueTag::String);
|
|
1677
|
+
assert_eq!(Value::null().tag(), ValueTag::Null);
|
|
1678
|
+
assert_eq!(Value::array(vec![]).tag(), ValueTag::Array);
|
|
1679
|
+
assert_eq!(Value::empty_object().tag(), ValueTag::Object);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
#[test]
|
|
1683
|
+
fn unpack_roundtrips_each_variant() {
|
|
1684
|
+
assert!(matches!(Value::number(3.0).unpack(), ValueRef::Number(n) if n == 3.0));
|
|
1685
|
+
assert!(matches!(Value::boolean(false).unpack(), ValueRef::Bool(false)));
|
|
1686
|
+
assert!(matches!(Value::string("x").unpack(), ValueRef::String(s) if s.as_str() == "x"));
|
|
1687
|
+
assert!(matches!(Value::null().unpack(), ValueRef::Null));
|
|
1688
|
+
assert!(matches!(Value::array(vec![]).unpack(), ValueRef::Array(_)));
|
|
1689
|
+
assert!(matches!(Value::empty_object().unpack(), ValueRef::Object(_)));
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
#[test]
|
|
1693
|
+
fn accessors_extract_or_none() {
|
|
1694
|
+
assert_eq!(Value::number(2.0).as_number(), Some(2.0));
|
|
1695
|
+
assert_eq!(Value::boolean(true).as_bool(), Some(true));
|
|
1696
|
+
assert_eq!(Value::string("abc").as_str(), Some("abc"));
|
|
1697
|
+
assert!(Value::null().is_null());
|
|
1698
|
+
// wrong-variant accessors return None
|
|
1699
|
+
assert_eq!(Value::number(1.0).as_bool(), None);
|
|
1700
|
+
assert_eq!(Value::null().as_str(), None);
|
|
1701
|
+
assert!(!Value::number(0.0).is_null());
|
|
1702
|
+
assert!(Value::array(vec![Value::number(1.0)]).as_array().is_some());
|
|
1703
|
+
assert!(Value::number(1.0).as_array().is_none());
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1357
1707
|
#[cfg(test)]
|
|
1358
1708
|
mod number_to_string_tests {
|
|
1359
1709
|
use super::js_number_to_string;
|
|
@@ -1371,6 +1721,14 @@ mod number_to_string_tests {
|
|
|
1371
1721
|
(0.5, "0.5"),
|
|
1372
1722
|
(-123.456, "-123.456"),
|
|
1373
1723
|
(100000.0, "100000"),
|
|
1724
|
+
(-1000.0, "-1000"),
|
|
1725
|
+
// Integer fast-path boundary (±2^53): the largest safe integer takes the itoa path; the
|
|
1726
|
+
// next decade up (still an exact f64 integer, but > 2^53) takes the general path. Both must
|
|
1727
|
+
// match Node's `String(value)`.
|
|
1728
|
+
(9007199254740992.0, "9007199254740992"), // 2^53
|
|
1729
|
+
(9007199254740991.0, "9007199254740991"), // 2^53 - 1 (Number.MAX_SAFE_INTEGER)
|
|
1730
|
+
(-9007199254740992.0, "-9007199254740992"),
|
|
1731
|
+
(1e16, "10000000000000000"), // > 2^53, exact f64 integer → general path
|
|
1374
1732
|
// Decimal/exponential boundary on the large side: 1e21 flips to exponential.
|
|
1375
1733
|
(1e20, "100000000000000000000"),
|
|
1376
1734
|
(1e21, "1e+21"),
|
|
@@ -1390,6 +1748,15 @@ mod number_to_string_tests {
|
|
|
1390
1748
|
// Shortest round-trip mantissa (not full precision).
|
|
1391
1749
|
(0.1, "0.1"),
|
|
1392
1750
|
(0.1 + 0.2, "0.30000000000000004"),
|
|
1751
|
+
// Shortest-representation ties → ECMAScript rounds the final digit to EVEN (spec: "if
|
|
1752
|
+
// there are two such possible values, choose the one that is even"). These large-magnitude
|
|
1753
|
+
// fractionals each have two equally-short round-tripping decimals; Node/V8 emit the even
|
|
1754
|
+
// one. Verified against Node's `String(value)`. (The prior `{:e}`-based path rounded the
|
|
1755
|
+
// other way — ryu does ties-to-even, matching the spec.)
|
|
1756
|
+
(2181495296738027.3, "2181495296738027.2"),
|
|
1757
|
+
(75251554695404.13, "75251554695404.12"),
|
|
1758
|
+
(256006004960902.63, "256006004960902.62"),
|
|
1759
|
+
(-18546578340962.313, "-18546578340962.312"),
|
|
1393
1760
|
// Non-finite.
|
|
1394
1761
|
(f64::INFINITY, "Infinity"),
|
|
1395
1762
|
(f64::NEG_INFINITY, "-Infinity"),
|
|
@@ -1401,3 +1768,172 @@ mod number_to_string_tests {
|
|
|
1401
1768
|
}
|
|
1402
1769
|
}
|
|
1403
1770
|
|
|
1771
|
+
#[cfg(test)]
|
|
1772
|
+
mod cycle_coercion_tests_381 {
|
|
1773
|
+
//! #381: string-coercing a cyclic array/object must terminate (was a silent thread hang),
|
|
1774
|
+
//! matching Node — `console.log`/inspect renders `[Circular]`, `"" + a` renders the back-ref as
|
|
1775
|
+
//! the empty string via `Array.prototype.join`.
|
|
1776
|
+
use super::Value;
|
|
1777
|
+
use crate::VmRef;
|
|
1778
|
+
|
|
1779
|
+
#[test]
|
|
1780
|
+
fn display_string_cyclic_array_terminates() {
|
|
1781
|
+
let a = Value::Array(VmRef::new(Vec::new()));
|
|
1782
|
+
if let Value::Array(inner) = &a {
|
|
1783
|
+
inner.borrow_mut().push(a.clone()); // a = [a]
|
|
1784
|
+
}
|
|
1785
|
+
assert_eq!(a.to_display_string(), "[[Circular]]");
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
#[test]
|
|
1789
|
+
fn js_string_cyclic_array_terminates() {
|
|
1790
|
+
let a = Value::Array(VmRef::new(Vec::new()));
|
|
1791
|
+
if let Value::Array(inner) = &a {
|
|
1792
|
+
inner.borrow_mut().push(a.clone());
|
|
1793
|
+
}
|
|
1794
|
+
// Node: `let a=[]; a.push(a); "" + a` === "" (the self-referential element joins as "").
|
|
1795
|
+
assert_eq!(a.to_js_string(), "");
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
#[test]
|
|
1799
|
+
fn display_string_cyclic_object_terminates() {
|
|
1800
|
+
let o = Value::object_from_pairs([]);
|
|
1801
|
+
if let Value::Object(inner) = &o {
|
|
1802
|
+
inner.borrow_mut().strings.insert(std::sync::Arc::from("self"), o.clone());
|
|
1803
|
+
}
|
|
1804
|
+
assert_eq!(o.to_display_string(), "{self: [Circular]}");
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
#[test]
|
|
1808
|
+
fn display_string_shared_dag_is_not_circular() {
|
|
1809
|
+
let shared = Value::Array(VmRef::new(vec![Value::Number(1.0)]));
|
|
1810
|
+
let root = Value::Array(VmRef::new(vec![shared.clone(), shared.clone()]));
|
|
1811
|
+
assert_eq!(root.to_display_string(), "[[1], [1]]", "a shared node is a DAG, not a cycle");
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
#[cfg(test)]
|
|
1816
|
+
mod propmap_merge_tests {
|
|
1817
|
+
use super::{PropMap, Value, PROPMAP_INLINE};
|
|
1818
|
+
use std::sync::Arc;
|
|
1819
|
+
|
|
1820
|
+
fn pm(pairs: &[(&str, f64)]) -> PropMap {
|
|
1821
|
+
let mut m = PropMap::default();
|
|
1822
|
+
for (k, v) in pairs {
|
|
1823
|
+
m.insert(Arc::from(*k), Value::Number(*v));
|
|
1824
|
+
}
|
|
1825
|
+
m
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
fn keys(m: &PropMap) -> Vec<String> {
|
|
1829
|
+
m.keys().map(|k| k.to_string()).collect()
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
fn num(m: &PropMap, k: &str) -> Option<f64> {
|
|
1833
|
+
match m.get(k) {
|
|
1834
|
+
Some(Value::Number(n)) => Some(*n),
|
|
1835
|
+
_ => None,
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
#[test]
|
|
1840
|
+
fn merge_appends_new_keys_in_source_order() {
|
|
1841
|
+
let mut dst = pm(&[("a", 1.0), ("b", 2.0)]);
|
|
1842
|
+
dst.merge_from(&pm(&[("c", 3.0), ("d", 4.0)]));
|
|
1843
|
+
assert_eq!(keys(&dst), ["a", "b", "c", "d"]);
|
|
1844
|
+
assert_eq!(num(&dst, "c"), Some(3.0));
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
#[test]
|
|
1848
|
+
fn merge_overwrites_existing_keys_without_reordering() {
|
|
1849
|
+
// `{ ...{a,b,c}, b: 20 }` — later write wins, key keeps its original slot.
|
|
1850
|
+
let mut dst = pm(&[("a", 1.0), ("b", 2.0), ("c", 3.0)]);
|
|
1851
|
+
dst.merge_from(&pm(&[("b", 20.0), ("d", 4.0)]));
|
|
1852
|
+
assert_eq!(keys(&dst), ["a", "b", "c", "d"]);
|
|
1853
|
+
assert_eq!(num(&dst, "b"), Some(20.0));
|
|
1854
|
+
assert_eq!(num(&dst, "d"), Some(4.0));
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
#[test]
|
|
1858
|
+
fn merge_empty_source_is_noop() {
|
|
1859
|
+
let mut dst = pm(&[("a", 1.0)]);
|
|
1860
|
+
dst.merge_from(&PropMap::default());
|
|
1861
|
+
assert_eq!(keys(&dst), ["a"]);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
#[test]
|
|
1865
|
+
fn merge_promotes_past_inline_threshold_preserving_order_and_overrides() {
|
|
1866
|
+
// Combined unique key count exceeds the inline cap, forcing the IndexMap promotion path.
|
|
1867
|
+
let mut dst = PropMap::default();
|
|
1868
|
+
for i in 0..PROPMAP_INLINE {
|
|
1869
|
+
dst.insert(Arc::from(format!("k{i}").as_str()), Value::Number(i as f64));
|
|
1870
|
+
}
|
|
1871
|
+
let mut src = PropMap::default();
|
|
1872
|
+
// Overwrite one existing key and add several new ones to cross the threshold.
|
|
1873
|
+
src.insert(Arc::from("k0"), Value::Number(100.0));
|
|
1874
|
+
for i in PROPMAP_INLINE..(PROPMAP_INLINE + 5) {
|
|
1875
|
+
src.insert(Arc::from(format!("k{i}").as_str()), Value::Number(i as f64));
|
|
1876
|
+
}
|
|
1877
|
+
dst.merge_from(&src);
|
|
1878
|
+
assert_eq!(dst.len(), PROPMAP_INLINE + 5);
|
|
1879
|
+
assert_eq!(num(&dst, "k0"), Some(100.0)); // override applied
|
|
1880
|
+
assert_eq!(num(&dst, "k0").is_some(), keys(&dst).first().map(|k| k == "k0").unwrap());
|
|
1881
|
+
// Original keys come first (insertion order), then the new ones.
|
|
1882
|
+
let ks = keys(&dst);
|
|
1883
|
+
assert_eq!(ks[0], "k0");
|
|
1884
|
+
assert_eq!(ks[PROPMAP_INLINE], format!("k{PROPMAP_INLINE}"));
|
|
1885
|
+
// Lookups remain correct after promotion.
|
|
1886
|
+
for i in PROPMAP_INLINE..(PROPMAP_INLINE + 5) {
|
|
1887
|
+
assert_eq!(num(&dst, &format!("k{i}")), Some(i as f64));
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
#[test]
|
|
1892
|
+
fn merge_into_empty_matches_clone() {
|
|
1893
|
+
let src = pm(&[("x", 7.0), ("y", 8.0)]);
|
|
1894
|
+
let mut dst = PropMap::default();
|
|
1895
|
+
dst.merge_from(&src);
|
|
1896
|
+
assert_eq!(keys(&dst), keys(&src));
|
|
1897
|
+
assert_eq!(num(&dst, "x"), Some(7.0));
|
|
1898
|
+
assert_eq!(num(&dst, "y"), Some(8.0));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
#[cfg(test)]
|
|
1903
|
+
mod value_call_non_function_tests {
|
|
1904
|
+
// Calling a non-callable value PARKS a catchable `TypeError` (#381) instead of `panic!`-ing
|
|
1905
|
+
// (an uncatchable process abort). The throw surfaces at the caller's next pending-throw
|
|
1906
|
+
// checkpoint. These lock the no-panic contract at the primitive level.
|
|
1907
|
+
use super::{value_call, Value};
|
|
1908
|
+
use crate::{has_pending_throw, take_pending_throw};
|
|
1909
|
+
|
|
1910
|
+
fn parked_error_name(v: &Value) -> Option<String> {
|
|
1911
|
+
if let Value::Object(o) = v {
|
|
1912
|
+
if let Some(Value::String(s)) = o.borrow().strings.get("name") {
|
|
1913
|
+
return Some(s.to_string());
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
None
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
#[test]
|
|
1920
|
+
fn calling_null_parks_type_error_and_does_not_panic() {
|
|
1921
|
+
let _ = take_pending_throw(); // clear any stale slot on this thread
|
|
1922
|
+
let r = value_call(&Value::Null, &[]);
|
|
1923
|
+
assert!(matches!(r, Value::Null), "returns the null sentinel, not a panic");
|
|
1924
|
+
assert!(has_pending_throw(), "a throw must be parked");
|
|
1925
|
+
let parked = take_pending_throw().expect("parked throw present");
|
|
1926
|
+
assert_eq!(parked_error_name(&parked).as_deref(), Some("TypeError"));
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
#[test]
|
|
1930
|
+
fn calling_a_number_parks_type_error() {
|
|
1931
|
+
let _ = take_pending_throw();
|
|
1932
|
+
let r = value_call(&Value::Number(5.0), &[]);
|
|
1933
|
+
assert!(matches!(r, Value::Null));
|
|
1934
|
+
assert!(has_pending_throw());
|
|
1935
|
+
let parked = take_pending_throw().expect("parked throw present");
|
|
1936
|
+
assert_eq!(parked_error_name(&parked).as_deref(), Some("TypeError"));
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|