@tishlang/tish 2.12.0 → 2.16.13
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 +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -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 +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- 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 +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- 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 +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- 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_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -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_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.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 {
|
|
@@ -860,6 +1062,13 @@ pub fn to_uint32_value(v: &Value) -> u32 {
|
|
|
860
1062
|
}
|
|
861
1063
|
|
|
862
1064
|
/// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
|
|
1065
|
+
///
|
|
1066
|
+
/// Calling a NON-callable (e.g. a method on `null` whose read returned `null`, or any non-function
|
|
1067
|
+
/// value) is a JS `TypeError`. In the native/runtime path this used to `panic!` — an UNCATCHABLE
|
|
1068
|
+
/// process abort (#381 class). It now PARKS a catchable `TypeError` and returns the `null` sentinel;
|
|
1069
|
+
/// the throw surfaces at the caller's next pending-throw checkpoint (statement boundary in emitted
|
|
1070
|
+
/// native code, or the `has_pending_throw()` polls in the shared builtins), so `try { null.foo() }
|
|
1071
|
+
/// catch (e) { … }` recovers instead of aborting — matching the VM/interpreter/node.
|
|
863
1072
|
pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
864
1073
|
match callee {
|
|
865
1074
|
Value::Function(f) => f.call(args),
|
|
@@ -868,15 +1077,19 @@ pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
|
868
1077
|
if let Some(inner) = inner {
|
|
869
1078
|
return value_call(&inner, args);
|
|
870
1079
|
}
|
|
871
|
-
|
|
872
|
-
"
|
|
873
|
-
callee
|
|
874
|
-
);
|
|
1080
|
+
crate::set_pending_throw(crate::not_a_function_error(format!(
|
|
1081
|
+
"{} is not a function",
|
|
1082
|
+
callee.to_display_string()
|
|
1083
|
+
)));
|
|
1084
|
+
Value::Null
|
|
1085
|
+
}
|
|
1086
|
+
_ => {
|
|
1087
|
+
crate::set_pending_throw(crate::not_a_function_error(format!(
|
|
1088
|
+
"{} is not a function",
|
|
1089
|
+
callee.to_display_string()
|
|
1090
|
+
)));
|
|
1091
|
+
Value::Null
|
|
875
1092
|
}
|
|
876
|
-
_ => panic!(
|
|
877
|
-
"Not a function: tried to call {:?} as a function (e.g. method on Null when read failed)",
|
|
878
|
-
callee
|
|
879
|
-
),
|
|
880
1093
|
}
|
|
881
1094
|
}
|
|
882
1095
|
|
|
@@ -967,26 +1180,75 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
|
967
1180
|
return;
|
|
968
1181
|
}
|
|
969
1182
|
|
|
1183
|
+
// Fast path for exact integers within ±2^53 (the f64 "safe integer" range). ECMAScript ToString
|
|
1184
|
+
// renders any such integer in plain decimal (no sign-exponent — magnitude < 1e21 ⇒ `point <= 21`
|
|
1185
|
+
// in the general algorithm below), which `itoa` emits directly, bypassing the `format!("{:e}")`
|
|
1186
|
+
// round-trip + intermediate `String`/`char`-filter/`"0".repeat()`. This is the dominant case for
|
|
1187
|
+
// tally/counter/index loops (e.g. map keys like `"w" + (n % 1000)`). Bit-identical to the general
|
|
1188
|
+
// path for every integer it accepts: each such value is exactly representable in both f64 and i64,
|
|
1189
|
+
// so `value as i64` is exact (no saturation — `±2^53` is well inside i64), and the decimal form is
|
|
1190
|
+
// the same digits the general path would emit. Larger integers (where f64 can't represent
|
|
1191
|
+
// consecutive values) fall through to the general path unchanged.
|
|
1192
|
+
const MAX_SAFE_INT: f64 = 9_007_199_254_740_992.0; // 2^53
|
|
1193
|
+
if value.fract() == 0.0 && value.abs() <= MAX_SAFE_INT {
|
|
1194
|
+
let mut buf = itoa::Buffer::new();
|
|
1195
|
+
out.push_str(buf.format(value as i64));
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
970
1199
|
let negative = value < 0.0;
|
|
971
|
-
// Shortest round-trip digits
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
let
|
|
1200
|
+
// Shortest round-trip digits via ryu, then reassembled into ECMAScript `Number::toString` form.
|
|
1201
|
+
// ryu writes the shortest correctly-rounded digits straight into a stack buffer with none of the
|
|
1202
|
+
// `core::fmt` `{:e}` Formatter machinery the previous version paid per call (profiled as ~40% of
|
|
1203
|
+
// JSON.stringify self-time on numeric payloads). number→string is the JSON / template-literal /
|
|
1204
|
+
// logging / `+=` primitive, so this is a broad win. The shortest round-trip digit *sequence* is
|
|
1205
|
+
// unique, so ryu emits the same digits as the old `{:e}` path → output stays byte-identical.
|
|
1206
|
+
let mut ryu_buf = ryu::Buffer::new();
|
|
1207
|
+
// Finite & non-zero here (NaN / ±∞ / ±0 handled above), so `format_finite` is safe and cheaper.
|
|
1208
|
+
let s = ryu_buf.format_finite(value.abs());
|
|
1209
|
+
|
|
1210
|
+
// Parse ryu's `<int>[.<frac>][e<exp>]` into significant digits + ECMAScript decimal point.
|
|
1211
|
+
// `point` = ECMAScript's `n`: value = digits × 10^(point − k), with `k` = significant-digit count.
|
|
1212
|
+
let (mant, exp) = match s.as_bytes().iter().position(|&c| c == b'e' || c == b'E') {
|
|
1213
|
+
Some(i) => (&s[..i], s[i + 1..].parse::<i32>().expect("ryu exponent")),
|
|
1214
|
+
None => (s, 0i32),
|
|
1215
|
+
};
|
|
1216
|
+
let (int_part, frac_part) = match mant.as_bytes().iter().position(|&c| c == b'.') {
|
|
1217
|
+
Some(i) => (&mant[..i], &mant[i + 1..]),
|
|
1218
|
+
None => (mant, ""),
|
|
1219
|
+
};
|
|
1220
|
+
let frac_len = frac_part.len() as i32;
|
|
1221
|
+
// Raw digit run = int_part ++ frac_part (ryu's mantissa, ≤ ~18 chars for an f64).
|
|
1222
|
+
let mut digits_buf = [0u8; 32];
|
|
1223
|
+
let mut n = 0usize;
|
|
1224
|
+
for &c in int_part.as_bytes().iter().chain(frac_part.as_bytes()) {
|
|
1225
|
+
digits_buf[n] = c;
|
|
1226
|
+
n += 1;
|
|
1227
|
+
}
|
|
1228
|
+
// Strip leading zeros (e.g. ryu "0.1" → raw "01" → "1") …
|
|
1229
|
+
let mut lead = 0usize;
|
|
1230
|
+
while lead + 1 < n && digits_buf[lead] == b'0' {
|
|
1231
|
+
lead += 1;
|
|
1232
|
+
}
|
|
1233
|
+
// … and trailing zeros (defensive — shortest form has none — folded back via `trail`).
|
|
1234
|
+
let mut end = n;
|
|
1235
|
+
while end - 1 > lead && digits_buf[end - 1] == b'0' {
|
|
1236
|
+
end -= 1;
|
|
1237
|
+
}
|
|
1238
|
+
let trail = (n - end) as i32;
|
|
1239
|
+
let digits = std::str::from_utf8(&digits_buf[lead..end]).expect("ascii digits");
|
|
980
1240
|
let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
|
|
981
|
-
let point =
|
|
1241
|
+
let point = k + exp - frac_len + trail;
|
|
982
1242
|
|
|
983
1243
|
if negative {
|
|
984
1244
|
out.push('-');
|
|
985
1245
|
}
|
|
986
1246
|
if k <= point && point <= 21 {
|
|
987
1247
|
// Integer, zero-padded: digits then (point − k) trailing zeros.
|
|
988
|
-
out.push_str(
|
|
989
|
-
|
|
1248
|
+
out.push_str(digits);
|
|
1249
|
+
for _ in 0..(point - k) {
|
|
1250
|
+
out.push('0');
|
|
1251
|
+
}
|
|
990
1252
|
} else if 0 < point && point <= 21 {
|
|
991
1253
|
// Decimal point inside the digit string.
|
|
992
1254
|
out.push_str(&digits[..point as usize]);
|
|
@@ -995,8 +1257,10 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
|
995
1257
|
} else if -6 < point && point <= 0 {
|
|
996
1258
|
// Leading-zero fraction: "0." then (−point) zeros then the digits.
|
|
997
1259
|
out.push_str("0.");
|
|
998
|
-
|
|
999
|
-
|
|
1260
|
+
for _ in 0..(-point) {
|
|
1261
|
+
out.push('0');
|
|
1262
|
+
}
|
|
1263
|
+
out.push_str(digits);
|
|
1000
1264
|
} else {
|
|
1001
1265
|
// Exponential: first digit, optional `.rest`, then `e±E`.
|
|
1002
1266
|
let e = point - 1;
|
|
@@ -1007,13 +1271,22 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
|
1007
1271
|
}
|
|
1008
1272
|
out.push('e');
|
|
1009
1273
|
out.push(if e >= 0 { '+' } else { '-' });
|
|
1010
|
-
|
|
1274
|
+
let mut ib = itoa::Buffer::new();
|
|
1275
|
+
out.push_str(ib.format(e.abs()));
|
|
1011
1276
|
}
|
|
1012
1277
|
}
|
|
1013
1278
|
|
|
1014
1279
|
impl Value {
|
|
1015
1280
|
/// Convert value to display string (for console output).
|
|
1016
1281
|
pub fn to_display_string(&self) -> String {
|
|
1282
|
+
self.to_display_string_guarded(&mut Vec::new())
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/// Cycle-safe recursive form (#381): `ancestors` holds the current path's container-cell pointers,
|
|
1286
|
+
/// so a self-referential array/object (`a.self = a`) renders `[Circular]` instead of recursing
|
|
1287
|
+
/// forever and wedging the thread. Ancestor-only (not all-visited), so a shared DAG node still
|
|
1288
|
+
/// renders in full.
|
|
1289
|
+
fn to_display_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
1017
1290
|
match self {
|
|
1018
1291
|
// Inspect form keeps the sign of negative zero (`console.log(-0)` → `-0`), unlike the
|
|
1019
1292
|
// ECMAScript ToString used by `to_js_string`. See `js_number_to_string`. (#247)
|
|
@@ -1023,8 +1296,17 @@ impl Value {
|
|
|
1023
1296
|
Value::Bool(b) => b.to_string(),
|
|
1024
1297
|
Value::Null => "null".to_string(),
|
|
1025
1298
|
Value::Array(arr) => {
|
|
1026
|
-
let
|
|
1027
|
-
|
|
1299
|
+
let ptr = arr.as_ptr();
|
|
1300
|
+
if ancestors.contains(&ptr) {
|
|
1301
|
+
return "[Circular]".to_string();
|
|
1302
|
+
}
|
|
1303
|
+
ancestors.push(ptr);
|
|
1304
|
+
let inner: Vec<String> = arr
|
|
1305
|
+
.borrow()
|
|
1306
|
+
.iter()
|
|
1307
|
+
.map(|v| v.to_display_string_guarded(ancestors))
|
|
1308
|
+
.collect();
|
|
1309
|
+
ancestors.pop();
|
|
1028
1310
|
format!("[{}]", inner.join(", "))
|
|
1029
1311
|
}
|
|
1030
1312
|
Value::NumberArray(arr) => {
|
|
@@ -1036,12 +1318,18 @@ impl Value {
|
|
|
1036
1318
|
format!("[{}]", inner.join(", "))
|
|
1037
1319
|
}
|
|
1038
1320
|
Value::Object(obj) => {
|
|
1321
|
+
let ptr = obj.as_ptr();
|
|
1322
|
+
if ancestors.contains(&ptr) {
|
|
1323
|
+
return "[Circular]".to_string();
|
|
1324
|
+
}
|
|
1325
|
+
ancestors.push(ptr);
|
|
1039
1326
|
let inner: Vec<String> = obj
|
|
1040
1327
|
.borrow()
|
|
1041
1328
|
.strings
|
|
1042
1329
|
.iter()
|
|
1043
|
-
.map(|(k, v)| format!("{}: {}", k.as_ref(), v.
|
|
1330
|
+
.map(|(k, v)| format!("{}: {}", k.as_ref(), v.to_display_string_guarded(ancestors)))
|
|
1044
1331
|
.collect();
|
|
1332
|
+
ancestors.pop();
|
|
1045
1333
|
format!("{{{}}}", inner.join(", "))
|
|
1046
1334
|
}
|
|
1047
1335
|
Value::Symbol(s) => {
|
|
@@ -1071,16 +1359,32 @@ impl Value {
|
|
|
1071
1359
|
/// renders as `"null"` here (matching `String(null)`); `join` itself maps `null`/`undefined`
|
|
1072
1360
|
/// elements to `""` *before* calling this, per the spec.
|
|
1073
1361
|
pub fn to_js_string(&self) -> String {
|
|
1362
|
+
self.to_js_string_guarded(&mut Vec::new())
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/// Cycle-safe `ToString` (#381): only arrays recurse here (objects render `[object Object]`), so a
|
|
1366
|
+
/// cyclic array via `"" + a` would otherwise hang. A back-reference joins as `""` — matching V8's
|
|
1367
|
+
/// `Array.prototype.join`, where a self-referential element contributes the empty string.
|
|
1368
|
+
fn to_js_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
1074
1369
|
match self {
|
|
1075
|
-
Value::Array(arr) =>
|
|
1076
|
-
.
|
|
1077
|
-
.
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1370
|
+
Value::Array(arr) => {
|
|
1371
|
+
let ptr = arr.as_ptr();
|
|
1372
|
+
if ancestors.contains(&ptr) {
|
|
1373
|
+
return String::new();
|
|
1374
|
+
}
|
|
1375
|
+
ancestors.push(ptr);
|
|
1376
|
+
let s = arr
|
|
1377
|
+
.borrow()
|
|
1378
|
+
.iter()
|
|
1379
|
+
.map(|v| match v {
|
|
1380
|
+
Value::Null => String::new(),
|
|
1381
|
+
other => other.to_js_string_guarded(ancestors),
|
|
1382
|
+
})
|
|
1383
|
+
.collect::<Vec<_>>()
|
|
1384
|
+
.join(",");
|
|
1385
|
+
ancestors.pop();
|
|
1386
|
+
s
|
|
1387
|
+
}
|
|
1084
1388
|
Value::NumberArray(arr) => arr
|
|
1085
1389
|
.borrow()
|
|
1086
1390
|
.iter()
|
|
@@ -1354,6 +1658,46 @@ impl Value {
|
|
|
1354
1658
|
}
|
|
1355
1659
|
}
|
|
1356
1660
|
|
|
1661
|
+
#[cfg(test)]
|
|
1662
|
+
mod value_abstraction_tests {
|
|
1663
|
+
// #201 Stage A — the abstraction must exactly mirror the enum it will replace.
|
|
1664
|
+
use super::{Value, ValueRef, ValueTag};
|
|
1665
|
+
|
|
1666
|
+
#[test]
|
|
1667
|
+
fn named_ctors_report_the_right_tag() {
|
|
1668
|
+
assert_eq!(Value::number(1.5).tag(), ValueTag::Number);
|
|
1669
|
+
assert_eq!(Value::boolean(true).tag(), ValueTag::Bool);
|
|
1670
|
+
assert_eq!(Value::string("hi").tag(), ValueTag::String);
|
|
1671
|
+
assert_eq!(Value::null().tag(), ValueTag::Null);
|
|
1672
|
+
assert_eq!(Value::array(vec![]).tag(), ValueTag::Array);
|
|
1673
|
+
assert_eq!(Value::empty_object().tag(), ValueTag::Object);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
#[test]
|
|
1677
|
+
fn unpack_roundtrips_each_variant() {
|
|
1678
|
+
assert!(matches!(Value::number(3.0).unpack(), ValueRef::Number(n) if n == 3.0));
|
|
1679
|
+
assert!(matches!(Value::boolean(false).unpack(), ValueRef::Bool(false)));
|
|
1680
|
+
assert!(matches!(Value::string("x").unpack(), ValueRef::String(s) if s.as_str() == "x"));
|
|
1681
|
+
assert!(matches!(Value::null().unpack(), ValueRef::Null));
|
|
1682
|
+
assert!(matches!(Value::array(vec![]).unpack(), ValueRef::Array(_)));
|
|
1683
|
+
assert!(matches!(Value::empty_object().unpack(), ValueRef::Object(_)));
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
#[test]
|
|
1687
|
+
fn accessors_extract_or_none() {
|
|
1688
|
+
assert_eq!(Value::number(2.0).as_number(), Some(2.0));
|
|
1689
|
+
assert_eq!(Value::boolean(true).as_bool(), Some(true));
|
|
1690
|
+
assert_eq!(Value::string("abc").as_str(), Some("abc"));
|
|
1691
|
+
assert!(Value::null().is_null());
|
|
1692
|
+
// wrong-variant accessors return None
|
|
1693
|
+
assert_eq!(Value::number(1.0).as_bool(), None);
|
|
1694
|
+
assert_eq!(Value::null().as_str(), None);
|
|
1695
|
+
assert!(!Value::number(0.0).is_null());
|
|
1696
|
+
assert!(Value::array(vec![Value::number(1.0)]).as_array().is_some());
|
|
1697
|
+
assert!(Value::number(1.0).as_array().is_none());
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1357
1701
|
#[cfg(test)]
|
|
1358
1702
|
mod number_to_string_tests {
|
|
1359
1703
|
use super::js_number_to_string;
|
|
@@ -1371,6 +1715,14 @@ mod number_to_string_tests {
|
|
|
1371
1715
|
(0.5, "0.5"),
|
|
1372
1716
|
(-123.456, "-123.456"),
|
|
1373
1717
|
(100000.0, "100000"),
|
|
1718
|
+
(-1000.0, "-1000"),
|
|
1719
|
+
// Integer fast-path boundary (±2^53): the largest safe integer takes the itoa path; the
|
|
1720
|
+
// next decade up (still an exact f64 integer, but > 2^53) takes the general path. Both must
|
|
1721
|
+
// match Node's `String(value)`.
|
|
1722
|
+
(9007199254740992.0, "9007199254740992"), // 2^53
|
|
1723
|
+
(9007199254740991.0, "9007199254740991"), // 2^53 - 1 (Number.MAX_SAFE_INTEGER)
|
|
1724
|
+
(-9007199254740992.0, "-9007199254740992"),
|
|
1725
|
+
(1e16, "10000000000000000"), // > 2^53, exact f64 integer → general path
|
|
1374
1726
|
// Decimal/exponential boundary on the large side: 1e21 flips to exponential.
|
|
1375
1727
|
(1e20, "100000000000000000000"),
|
|
1376
1728
|
(1e21, "1e+21"),
|
|
@@ -1390,6 +1742,15 @@ mod number_to_string_tests {
|
|
|
1390
1742
|
// Shortest round-trip mantissa (not full precision).
|
|
1391
1743
|
(0.1, "0.1"),
|
|
1392
1744
|
(0.1 + 0.2, "0.30000000000000004"),
|
|
1745
|
+
// Shortest-representation ties → ECMAScript rounds the final digit to EVEN (spec: "if
|
|
1746
|
+
// there are two such possible values, choose the one that is even"). These large-magnitude
|
|
1747
|
+
// fractionals each have two equally-short round-tripping decimals; Node/V8 emit the even
|
|
1748
|
+
// one. Verified against Node's `String(value)`. (The prior `{:e}`-based path rounded the
|
|
1749
|
+
// other way — ryu does ties-to-even, matching the spec.)
|
|
1750
|
+
(2181495296738027.3, "2181495296738027.2"),
|
|
1751
|
+
(75251554695404.13, "75251554695404.12"),
|
|
1752
|
+
(256006004960902.63, "256006004960902.62"),
|
|
1753
|
+
(-18546578340962.313, "-18546578340962.312"),
|
|
1393
1754
|
// Non-finite.
|
|
1394
1755
|
(f64::INFINITY, "Infinity"),
|
|
1395
1756
|
(f64::NEG_INFINITY, "-Infinity"),
|
|
@@ -1401,3 +1762,172 @@ mod number_to_string_tests {
|
|
|
1401
1762
|
}
|
|
1402
1763
|
}
|
|
1403
1764
|
|
|
1765
|
+
#[cfg(test)]
|
|
1766
|
+
mod cycle_coercion_tests_381 {
|
|
1767
|
+
//! #381: string-coercing a cyclic array/object must terminate (was a silent thread hang),
|
|
1768
|
+
//! matching Node — `console.log`/inspect renders `[Circular]`, `"" + a` renders the back-ref as
|
|
1769
|
+
//! the empty string via `Array.prototype.join`.
|
|
1770
|
+
use super::Value;
|
|
1771
|
+
use crate::VmRef;
|
|
1772
|
+
|
|
1773
|
+
#[test]
|
|
1774
|
+
fn display_string_cyclic_array_terminates() {
|
|
1775
|
+
let a = Value::Array(VmRef::new(Vec::new()));
|
|
1776
|
+
if let Value::Array(inner) = &a {
|
|
1777
|
+
inner.borrow_mut().push(a.clone()); // a = [a]
|
|
1778
|
+
}
|
|
1779
|
+
assert_eq!(a.to_display_string(), "[[Circular]]");
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
#[test]
|
|
1783
|
+
fn js_string_cyclic_array_terminates() {
|
|
1784
|
+
let a = Value::Array(VmRef::new(Vec::new()));
|
|
1785
|
+
if let Value::Array(inner) = &a {
|
|
1786
|
+
inner.borrow_mut().push(a.clone());
|
|
1787
|
+
}
|
|
1788
|
+
// Node: `let a=[]; a.push(a); "" + a` === "" (the self-referential element joins as "").
|
|
1789
|
+
assert_eq!(a.to_js_string(), "");
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
#[test]
|
|
1793
|
+
fn display_string_cyclic_object_terminates() {
|
|
1794
|
+
let o = Value::object_from_pairs([]);
|
|
1795
|
+
if let Value::Object(inner) = &o {
|
|
1796
|
+
inner.borrow_mut().strings.insert(std::sync::Arc::from("self"), o.clone());
|
|
1797
|
+
}
|
|
1798
|
+
assert_eq!(o.to_display_string(), "{self: [Circular]}");
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
#[test]
|
|
1802
|
+
fn display_string_shared_dag_is_not_circular() {
|
|
1803
|
+
let shared = Value::Array(VmRef::new(vec![Value::Number(1.0)]));
|
|
1804
|
+
let root = Value::Array(VmRef::new(vec![shared.clone(), shared.clone()]));
|
|
1805
|
+
assert_eq!(root.to_display_string(), "[[1], [1]]", "a shared node is a DAG, not a cycle");
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
#[cfg(test)]
|
|
1810
|
+
mod propmap_merge_tests {
|
|
1811
|
+
use super::{PropMap, Value, PROPMAP_INLINE};
|
|
1812
|
+
use std::sync::Arc;
|
|
1813
|
+
|
|
1814
|
+
fn pm(pairs: &[(&str, f64)]) -> PropMap {
|
|
1815
|
+
let mut m = PropMap::default();
|
|
1816
|
+
for (k, v) in pairs {
|
|
1817
|
+
m.insert(Arc::from(*k), Value::Number(*v));
|
|
1818
|
+
}
|
|
1819
|
+
m
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
fn keys(m: &PropMap) -> Vec<String> {
|
|
1823
|
+
m.keys().map(|k| k.to_string()).collect()
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
fn num(m: &PropMap, k: &str) -> Option<f64> {
|
|
1827
|
+
match m.get(k) {
|
|
1828
|
+
Some(Value::Number(n)) => Some(*n),
|
|
1829
|
+
_ => None,
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
#[test]
|
|
1834
|
+
fn merge_appends_new_keys_in_source_order() {
|
|
1835
|
+
let mut dst = pm(&[("a", 1.0), ("b", 2.0)]);
|
|
1836
|
+
dst.merge_from(&pm(&[("c", 3.0), ("d", 4.0)]));
|
|
1837
|
+
assert_eq!(keys(&dst), ["a", "b", "c", "d"]);
|
|
1838
|
+
assert_eq!(num(&dst, "c"), Some(3.0));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
#[test]
|
|
1842
|
+
fn merge_overwrites_existing_keys_without_reordering() {
|
|
1843
|
+
// `{ ...{a,b,c}, b: 20 }` — later write wins, key keeps its original slot.
|
|
1844
|
+
let mut dst = pm(&[("a", 1.0), ("b", 2.0), ("c", 3.0)]);
|
|
1845
|
+
dst.merge_from(&pm(&[("b", 20.0), ("d", 4.0)]));
|
|
1846
|
+
assert_eq!(keys(&dst), ["a", "b", "c", "d"]);
|
|
1847
|
+
assert_eq!(num(&dst, "b"), Some(20.0));
|
|
1848
|
+
assert_eq!(num(&dst, "d"), Some(4.0));
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
#[test]
|
|
1852
|
+
fn merge_empty_source_is_noop() {
|
|
1853
|
+
let mut dst = pm(&[("a", 1.0)]);
|
|
1854
|
+
dst.merge_from(&PropMap::default());
|
|
1855
|
+
assert_eq!(keys(&dst), ["a"]);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
#[test]
|
|
1859
|
+
fn merge_promotes_past_inline_threshold_preserving_order_and_overrides() {
|
|
1860
|
+
// Combined unique key count exceeds the inline cap, forcing the IndexMap promotion path.
|
|
1861
|
+
let mut dst = PropMap::default();
|
|
1862
|
+
for i in 0..PROPMAP_INLINE {
|
|
1863
|
+
dst.insert(Arc::from(format!("k{i}").as_str()), Value::Number(i as f64));
|
|
1864
|
+
}
|
|
1865
|
+
let mut src = PropMap::default();
|
|
1866
|
+
// Overwrite one existing key and add several new ones to cross the threshold.
|
|
1867
|
+
src.insert(Arc::from("k0"), Value::Number(100.0));
|
|
1868
|
+
for i in PROPMAP_INLINE..(PROPMAP_INLINE + 5) {
|
|
1869
|
+
src.insert(Arc::from(format!("k{i}").as_str()), Value::Number(i as f64));
|
|
1870
|
+
}
|
|
1871
|
+
dst.merge_from(&src);
|
|
1872
|
+
assert_eq!(dst.len(), PROPMAP_INLINE + 5);
|
|
1873
|
+
assert_eq!(num(&dst, "k0"), Some(100.0)); // override applied
|
|
1874
|
+
assert_eq!(num(&dst, "k0").is_some(), keys(&dst).first().map(|k| k == "k0").unwrap());
|
|
1875
|
+
// Original keys come first (insertion order), then the new ones.
|
|
1876
|
+
let ks = keys(&dst);
|
|
1877
|
+
assert_eq!(ks[0], "k0");
|
|
1878
|
+
assert_eq!(ks[PROPMAP_INLINE], format!("k{PROPMAP_INLINE}"));
|
|
1879
|
+
// Lookups remain correct after promotion.
|
|
1880
|
+
for i in PROPMAP_INLINE..(PROPMAP_INLINE + 5) {
|
|
1881
|
+
assert_eq!(num(&dst, &format!("k{i}")), Some(i as f64));
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
#[test]
|
|
1886
|
+
fn merge_into_empty_matches_clone() {
|
|
1887
|
+
let src = pm(&[("x", 7.0), ("y", 8.0)]);
|
|
1888
|
+
let mut dst = PropMap::default();
|
|
1889
|
+
dst.merge_from(&src);
|
|
1890
|
+
assert_eq!(keys(&dst), keys(&src));
|
|
1891
|
+
assert_eq!(num(&dst, "x"), Some(7.0));
|
|
1892
|
+
assert_eq!(num(&dst, "y"), Some(8.0));
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
#[cfg(test)]
|
|
1897
|
+
mod value_call_non_function_tests {
|
|
1898
|
+
// Calling a non-callable value PARKS a catchable `TypeError` (#381) instead of `panic!`-ing
|
|
1899
|
+
// (an uncatchable process abort). The throw surfaces at the caller's next pending-throw
|
|
1900
|
+
// checkpoint. These lock the no-panic contract at the primitive level.
|
|
1901
|
+
use super::{value_call, Value};
|
|
1902
|
+
use crate::{has_pending_throw, take_pending_throw};
|
|
1903
|
+
|
|
1904
|
+
fn parked_error_name(v: &Value) -> Option<String> {
|
|
1905
|
+
if let Value::Object(o) = v {
|
|
1906
|
+
if let Some(Value::String(s)) = o.borrow().strings.get("name") {
|
|
1907
|
+
return Some(s.to_string());
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
None
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
#[test]
|
|
1914
|
+
fn calling_null_parks_type_error_and_does_not_panic() {
|
|
1915
|
+
let _ = take_pending_throw(); // clear any stale slot on this thread
|
|
1916
|
+
let r = value_call(&Value::Null, &[]);
|
|
1917
|
+
assert!(matches!(r, Value::Null), "returns the null sentinel, not a panic");
|
|
1918
|
+
assert!(has_pending_throw(), "a throw must be parked");
|
|
1919
|
+
let parked = take_pending_throw().expect("parked throw present");
|
|
1920
|
+
assert_eq!(parked_error_name(&parked).as_deref(), Some("TypeError"));
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
#[test]
|
|
1924
|
+
fn calling_a_number_parks_type_error() {
|
|
1925
|
+
let _ = take_pending_throw();
|
|
1926
|
+
let r = value_call(&Value::Number(5.0), &[]);
|
|
1927
|
+
assert!(matches!(r, Value::Null));
|
|
1928
|
+
assert!(has_pending_throw());
|
|
1929
|
+
let parked = take_pending_throw().expect("parked throw present");
|
|
1930
|
+
assert_eq!(parked_error_name(&parked).as_deref(), Some("TypeError"));
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|