@tishlang/tish 2.2.7 → 2.8.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/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- package/crates/tish_ast/src/ast.rs +6 -2
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +1 -1
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +4 -4
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +17 -0
- package/crates/tish_compile_js/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +45 -1
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +6 -0
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +856 -140
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +15 -26
- package/crates/tish_resolve/src/lib.rs +188 -18
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- 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
|
@@ -834,6 +834,31 @@ pub fn to_uint32(x: f64) -> u32 {
|
|
|
834
834
|
}
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
/// `ToNumber` for the native boxed runtime: a `Value::Number` passes through, every other variant
|
|
838
|
+
/// coerces to `NaN`. This is the same `as_number().unwrap_or(NaN)` convention used by the
|
|
839
|
+
/// interpreter (`binop_number` / `to_int32`), the VM (`eval_binop`), and `ops::add`/`sub`/`mul`/`div`
|
|
840
|
+
/// — so the native backend stays bit-for-bit in lock-step with them at runtime (a string/bool/null
|
|
841
|
+
/// operand is `NaN`, hence `0` for bitwise). The compiler constant-folds literal cases like
|
|
842
|
+
/// `"5" | 0 === 5` separately, so this only governs runtime (non-constant) operands.
|
|
843
|
+
#[inline]
|
|
844
|
+
pub fn to_number_value(v: &Value) -> f64 {
|
|
845
|
+
v.as_number().unwrap_or(f64::NAN)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/// `ToInt32` of an arbitrary [`Value`] (coerce via [`to_number_value`], then [`to_int32`]). Designed
|
|
849
|
+
/// to compose in generated code: unlike a `let Value::Number(a) = &(..) else { panic!() }` block,
|
|
850
|
+
/// it binds no name, so nested bitwise/shift operands can never shadow each other.
|
|
851
|
+
#[inline]
|
|
852
|
+
pub fn to_int32_value(v: &Value) -> i32 {
|
|
853
|
+
to_int32(to_number_value(v))
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/// `ToUint32` companion to [`to_int32_value`].
|
|
857
|
+
#[inline]
|
|
858
|
+
pub fn to_uint32_value(v: &Value) -> u32 {
|
|
859
|
+
to_uint32(to_number_value(v))
|
|
860
|
+
}
|
|
861
|
+
|
|
837
862
|
/// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
|
|
838
863
|
pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
839
864
|
match callee {
|
|
@@ -910,20 +935,36 @@ impl std::fmt::Debug for Value {
|
|
|
910
935
|
/// We take the shortest round-tripping digits from Rust's `{:e}` (a Ryū/Grisu-class shortest
|
|
911
936
|
/// formatter, matching V8's digit choice) and lay them out per the ECMAScript rule: plain
|
|
912
937
|
/// decimal when the point position `n` is in `(-6, 21]`, otherwise `d[.ddd]e±E` with `E = n-1`
|
|
913
|
-
/// (sign always shown, no leading zeros in the exponent).
|
|
914
|
-
/// `
|
|
938
|
+
/// (sign always shown, no leading zeros in the exponent). This is the ECMAScript `Number::toString`
|
|
939
|
+
/// used by `.toString()`, `String(n)`, `+` concatenation, and template literals, so `-0` renders as
|
|
940
|
+
/// `"0"` (the sign is dropped — `(-0).toString() === "0"`). The *inspect* form (`console.log` of a
|
|
941
|
+
/// bare number / array element) keeps `"-0"`; that distinction lives in [`Value::to_display_string`].
|
|
915
942
|
pub fn js_number_to_string(value: f64) -> String {
|
|
943
|
+
let mut out = String::new();
|
|
944
|
+
js_number_to_string_into(&mut out, value);
|
|
945
|
+
out
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/// Append the ECMAScript `Number::toString` of `value` to `out` — identical result to
|
|
949
|
+
/// [`js_number_to_string`] but with no intermediate `String` allocation. The JSON.stringify
|
|
950
|
+
/// hot path appends millions of numbers into a single buffer, so the alloc-free form matters.
|
|
951
|
+
pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
916
952
|
if value.is_nan() {
|
|
917
|
-
|
|
953
|
+
out.push_str("NaN");
|
|
954
|
+
return;
|
|
918
955
|
}
|
|
919
956
|
if value == f64::INFINITY {
|
|
920
|
-
|
|
957
|
+
out.push_str("Infinity");
|
|
958
|
+
return;
|
|
921
959
|
}
|
|
922
960
|
if value == f64::NEG_INFINITY {
|
|
923
|
-
|
|
961
|
+
out.push_str("-Infinity");
|
|
962
|
+
return;
|
|
924
963
|
}
|
|
925
964
|
if value == 0.0 {
|
|
926
|
-
|
|
965
|
+
// ECMAScript `Number::toString`: both `+0` and `-0` stringify to `"0"`.
|
|
966
|
+
out.push('0');
|
|
967
|
+
return;
|
|
927
968
|
}
|
|
928
969
|
|
|
929
970
|
let negative = value < 0.0;
|
|
@@ -939,7 +980,6 @@ pub fn js_number_to_string(value: f64) -> String {
|
|
|
939
980
|
let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
|
|
940
981
|
let point = exp + 1; // ECMAScript's `n`: value = digits × 10^(point − k)
|
|
941
982
|
|
|
942
|
-
let mut out = String::new();
|
|
943
983
|
if negative {
|
|
944
984
|
out.push('-');
|
|
945
985
|
}
|
|
@@ -969,13 +1009,15 @@ pub fn js_number_to_string(value: f64) -> String {
|
|
|
969
1009
|
out.push(if e >= 0 { '+' } else { '-' });
|
|
970
1010
|
out.push_str(&e.abs().to_string());
|
|
971
1011
|
}
|
|
972
|
-
out
|
|
973
1012
|
}
|
|
974
1013
|
|
|
975
1014
|
impl Value {
|
|
976
1015
|
/// Convert value to display string (for console output).
|
|
977
1016
|
pub fn to_display_string(&self) -> String {
|
|
978
1017
|
match self {
|
|
1018
|
+
// Inspect form keeps the sign of negative zero (`console.log(-0)` → `-0`), unlike the
|
|
1019
|
+
// ECMAScript ToString used by `to_js_string`. See `js_number_to_string`. (#247)
|
|
1020
|
+
Value::Number(n) if *n == 0.0 && n.is_sign_negative() => "-0".to_string(),
|
|
979
1021
|
Value::Number(n) => js_number_to_string(*n),
|
|
980
1022
|
Value::String(s) => s.to_string(),
|
|
981
1023
|
Value::Bool(b) => b.to_string(),
|
|
@@ -1046,6 +1088,9 @@ impl Value {
|
|
|
1046
1088
|
.collect::<Vec<_>>()
|
|
1047
1089
|
.join(","),
|
|
1048
1090
|
Value::Object(_) => "[object Object]".to_string(),
|
|
1091
|
+
// ECMAScript ToString of a number (drops `-0`'s sign), distinct from the inspect form
|
|
1092
|
+
// that `to_display_string` would give for `-0`. (#247)
|
|
1093
|
+
Value::Number(n) => js_number_to_string(*n),
|
|
1049
1094
|
// Primitives (and the remaining cases) coincide with the display form.
|
|
1050
1095
|
_ => self.to_display_string(),
|
|
1051
1096
|
}
|
|
@@ -1148,12 +1193,18 @@ impl Value {
|
|
|
1148
1193
|
// -------------------------------------------------------------------------
|
|
1149
1194
|
|
|
1150
1195
|
/// Whether packed f64 arrays are enabled this run. Default: **off** (`TISH_PACKED_ARRAYS=1`
|
|
1151
|
-
/// opts in).
|
|
1196
|
+
/// opts in). Read once per process and cached — the VM calls this once per executed array
|
|
1197
|
+
/// literal, and a `std::env::var` there is a libc env lock plus a `String` allocation per
|
|
1198
|
+
/// `NewArray` for a flag that never changes after startup (#166). Set the variable before the
|
|
1199
|
+
/// process starts (as the CI sweep does); mid-process toggling is not observed by design.
|
|
1152
1200
|
/// The flag is intentionally backwards from the slot/JIT flags (those were default-on) to
|
|
1153
1201
|
/// keep the default binary behaviour byte-identical while we validate coverage.
|
|
1154
1202
|
#[inline]
|
|
1155
1203
|
pub fn packed_arrays_enabled() -> bool {
|
|
1156
|
-
std::
|
|
1204
|
+
static PACKED_ARRAYS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
|
1205
|
+
*PACKED_ARRAYS.get_or_init(|| {
|
|
1206
|
+
std::env::var("TISH_PACKED_ARRAYS").map(|v| v == "1").unwrap_or(false)
|
|
1207
|
+
})
|
|
1157
1208
|
}
|
|
1158
1209
|
|
|
1159
1210
|
/// Wrap a `Vec<f64>` as a `Value::NumberArray`. Only call when `packed_arrays_enabled()`.
|
|
@@ -1310,9 +1361,11 @@ mod number_to_string_tests {
|
|
|
1310
1361
|
#[test]
|
|
1311
1362
|
fn matches_javascript_number_tostring() {
|
|
1312
1363
|
// (value, expected) — every `expected` is what Node's `String(value)` produces.
|
|
1364
|
+
// `String(-0) === "0"`: ToString drops the sign of negative zero (the inspect form keeps
|
|
1365
|
+
// it, but that path is `Value::to_display_string`, not this function). (#247)
|
|
1313
1366
|
let cases: &[(f64, &str)] = &[
|
|
1314
1367
|
(0.0, "0"),
|
|
1315
|
-
(-0.0, "
|
|
1368
|
+
(-0.0, "0"),
|
|
1316
1369
|
(123.0, "123"),
|
|
1317
1370
|
(123.456, "123.456"),
|
|
1318
1371
|
(0.5, "0.5"),
|
|
@@ -144,6 +144,11 @@ impl Evaluator {
|
|
|
144
144
|
math.insert("round".into(), Value::Native(natives::math_round));
|
|
145
145
|
math.insert("random".into(), Value::Native(natives::math_random));
|
|
146
146
|
math.insert("pow".into(), Value::Native(natives::math_pow));
|
|
147
|
+
math.insert("hypot".into(), Value::Native(natives::math_hypot));
|
|
148
|
+
math.insert("asin".into(), Value::Native(natives::math_asin));
|
|
149
|
+
math.insert("acos".into(), Value::Native(natives::math_acos));
|
|
150
|
+
math.insert("atan".into(), Value::Native(natives::math_atan));
|
|
151
|
+
math.insert("atan2".into(), Value::Native(natives::math_atan2));
|
|
147
152
|
math.insert("sin".into(), Value::Native(natives::math_sin));
|
|
148
153
|
math.insert("cos".into(), Value::Native(natives::math_cos));
|
|
149
154
|
math.insert("tan".into(), Value::Native(natives::math_tan));
|
|
@@ -180,6 +185,26 @@ impl Evaluator {
|
|
|
180
185
|
true,
|
|
181
186
|
);
|
|
182
187
|
|
|
188
|
+
// Bare `process` global (node-compatible), mirroring the VM. `process.argv` reads the
|
|
189
|
+
// configurable argv so `tish run <file> [args...]` reaches the script. #88
|
|
190
|
+
#[cfg(feature = "process")]
|
|
191
|
+
{
|
|
192
|
+
let mut process_obj = PropMap::default();
|
|
193
|
+
process_obj.insert("exit".into(), Value::Native(natives::process_exit));
|
|
194
|
+
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
195
|
+
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
196
|
+
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
197
|
+
.into_iter()
|
|
198
|
+
.map(|s| Value::String(s.into()))
|
|
199
|
+
.collect();
|
|
200
|
+
process_obj.insert("argv".into(), Value::Array(Rc::new(RefCell::new(argv))));
|
|
201
|
+
let env_obj: PropMap = std::env::vars()
|
|
202
|
+
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
|
|
203
|
+
.collect();
|
|
204
|
+
process_obj.insert("env".into(), Value::object(env_obj));
|
|
205
|
+
s.set("process".into(), Value::object(process_obj), true);
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
let mut object = PropMap::with_capacity(5);
|
|
184
209
|
object.insert("keys".into(), Value::Native(Self::object_keys));
|
|
185
210
|
object.insert("values".into(), Value::Native(Self::object_values));
|
|
@@ -1032,8 +1057,10 @@ impl Evaluator {
|
|
|
1032
1057
|
exports.insert("exit".into(), Value::Native(natives::process_exit));
|
|
1033
1058
|
exports.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1034
1059
|
exports.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1035
|
-
let argv: Vec<Value> =
|
|
1036
|
-
|
|
1060
|
+
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
1061
|
+
.into_iter()
|
|
1062
|
+
.map(|s| Value::String(s.into()))
|
|
1063
|
+
.collect();
|
|
1037
1064
|
exports.insert(
|
|
1038
1065
|
"argv".into(),
|
|
1039
1066
|
Value::Array(Rc::new(RefCell::new(argv.clone()))),
|
|
@@ -1106,11 +1133,35 @@ impl Evaluator {
|
|
|
1106
1133
|
op,
|
|
1107
1134
|
right,
|
|
1108
1135
|
..
|
|
1109
|
-
} => {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1136
|
+
} => match op {
|
|
1137
|
+
// Short-circuit + value-returning && / || (JS, #240): evaluate the left; if it
|
|
1138
|
+
// already decides the result, return IT without evaluating (and running the side
|
|
1139
|
+
// effects of) the right. The result is always an operand value, never a coerced
|
|
1140
|
+
// boolean. The generic path below eagerly evaluated both operands and `eval_binop`
|
|
1141
|
+
// coerced And/Or to `Bool`, so `five() && 7` was `true` and `false && f()` still
|
|
1142
|
+
// ran `f()`.
|
|
1143
|
+
BinOp::And => {
|
|
1144
|
+
let l = self.eval_expr(left)?;
|
|
1145
|
+
if l.is_truthy() {
|
|
1146
|
+
self.eval_expr(right)
|
|
1147
|
+
} else {
|
|
1148
|
+
Ok(l)
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
BinOp::Or => {
|
|
1152
|
+
let l = self.eval_expr(left)?;
|
|
1153
|
+
if l.is_truthy() {
|
|
1154
|
+
Ok(l)
|
|
1155
|
+
} else {
|
|
1156
|
+
self.eval_expr(right)
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
_ => {
|
|
1160
|
+
let l = self.eval_expr(left)?;
|
|
1161
|
+
let r = self.eval_expr(right)?;
|
|
1162
|
+
self.eval_binop(&l, *op, &r).map_err(EvalError::Error)
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1114
1165
|
Expr::Unary { op, operand, .. } => {
|
|
1115
1166
|
let o = self.eval_expr(operand)?;
|
|
1116
1167
|
self.eval_unary(*op, &o).map_err(EvalError::Error)
|
|
@@ -1173,7 +1224,11 @@ impl Evaluator {
|
|
|
1173
1224
|
_ => 0,
|
|
1174
1225
|
};
|
|
1175
1226
|
for v in arr_borrow.iter().skip(start) {
|
|
1176
|
-
|
|
1227
|
+
// SameValueZero: NaN matches NaN (JS Array.includes, unlike
|
|
1228
|
+
// indexOf). #247
|
|
1229
|
+
if v.strict_eq(&search)
|
|
1230
|
+
|| matches!((v, &search), (Value::Number(a), Value::Number(b)) if a.is_nan() && b.is_nan())
|
|
1231
|
+
{
|
|
1177
1232
|
return Ok(Value::Bool(true));
|
|
1178
1233
|
}
|
|
1179
1234
|
}
|
|
@@ -1496,6 +1551,54 @@ impl Evaluator {
|
|
|
1496
1551
|
}
|
|
1497
1552
|
return Ok(Value::Number(-1.0));
|
|
1498
1553
|
}
|
|
1554
|
+
"findLast" => {
|
|
1555
|
+
// Like find, from the end (#247). Callback gets (value, original index).
|
|
1556
|
+
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1557
|
+
let arr_borrow = arr.borrow();
|
|
1558
|
+
let scoped = self.create_callback_scope(&callback);
|
|
1559
|
+
for i in (0..arr_borrow.len()).rev() {
|
|
1560
|
+
let v = arr_borrow[i].clone();
|
|
1561
|
+
let args = [v.clone(), Value::Number(i as f64)];
|
|
1562
|
+
let found = match &scoped {
|
|
1563
|
+
Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
|
|
1564
|
+
None => self.call_func(&callback, &args)?,
|
|
1565
|
+
};
|
|
1566
|
+
if found.is_truthy() {
|
|
1567
|
+
return Ok(v);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return Ok(Value::Null);
|
|
1571
|
+
}
|
|
1572
|
+
"findLastIndex" => {
|
|
1573
|
+
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1574
|
+
let arr_borrow = arr.borrow();
|
|
1575
|
+
let scoped = self.create_callback_scope(&callback);
|
|
1576
|
+
for i in (0..arr_borrow.len()).rev() {
|
|
1577
|
+
let args = [arr_borrow[i].clone(), Value::Number(i as f64)];
|
|
1578
|
+
let found = match &scoped {
|
|
1579
|
+
Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
|
|
1580
|
+
None => self.call_func(&callback, &args)?,
|
|
1581
|
+
};
|
|
1582
|
+
if found.is_truthy() {
|
|
1583
|
+
return Ok(Value::Number(i as f64));
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return Ok(Value::Number(-1.0));
|
|
1587
|
+
}
|
|
1588
|
+
"at" => {
|
|
1589
|
+
// Negative index counts from the end; out of range → null (#247).
|
|
1590
|
+
let i = match arg_vals.first() {
|
|
1591
|
+
Some(Value::Number(n)) => *n as i64,
|
|
1592
|
+
_ => 0,
|
|
1593
|
+
};
|
|
1594
|
+
let arr_borrow = arr.borrow();
|
|
1595
|
+
let len = arr_borrow.len() as i64;
|
|
1596
|
+
let idx = if i < 0 { len + i } else { i };
|
|
1597
|
+
if idx >= 0 && idx < len {
|
|
1598
|
+
return Ok(arr_borrow[idx as usize].clone());
|
|
1599
|
+
}
|
|
1600
|
+
return Ok(Value::Null);
|
|
1601
|
+
}
|
|
1499
1602
|
"forEach" => {
|
|
1500
1603
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1501
1604
|
let arr_borrow = arr.borrow();
|
|
@@ -1788,6 +1891,20 @@ impl Evaluator {
|
|
|
1788
1891
|
.map(|c| Value::String(c.to_string().into()))
|
|
1789
1892
|
.unwrap_or(Value::String("".into())));
|
|
1790
1893
|
}
|
|
1894
|
+
"at" => {
|
|
1895
|
+
// Negative index from end; out of range → null (#247).
|
|
1896
|
+
let i = match arg_vals.first() {
|
|
1897
|
+
Some(Value::Number(n)) => *n as i64,
|
|
1898
|
+
_ => 0,
|
|
1899
|
+
};
|
|
1900
|
+
let chars: Vec<char> = s.chars().collect();
|
|
1901
|
+
let len = chars.len() as i64;
|
|
1902
|
+
let idx = if i < 0 { len + i } else { i };
|
|
1903
|
+
if idx >= 0 && idx < len {
|
|
1904
|
+
return Ok(Value::String(chars[idx as usize].to_string().into()));
|
|
1905
|
+
}
|
|
1906
|
+
return Ok(Value::Null);
|
|
1907
|
+
}
|
|
1791
1908
|
"charCodeAt" => {
|
|
1792
1909
|
let idx = match arg_vals.first() {
|
|
1793
1910
|
Some(Value::Number(n)) => *n as usize,
|
|
@@ -1868,7 +1985,8 @@ impl Evaluator {
|
|
|
1868
1985
|
})
|
|
1869
1986
|
.unwrap_or(0)
|
|
1870
1987
|
.clamp(0, 20); // ECMA-262: 0–20
|
|
1871
|
-
|
|
1988
|
+
// Shared half-away-from-zero rounding so interp matches vm/native/node (#247).
|
|
1989
|
+
let formatted = tishlang_builtins::number::to_fixed_str(*n, digits as usize);
|
|
1872
1990
|
return Ok(Value::String(formatted.into()));
|
|
1873
1991
|
}
|
|
1874
1992
|
if method_name.as_ref() == "toString" {
|
|
@@ -2011,7 +2129,7 @@ impl Evaluator {
|
|
|
2011
2129
|
let mut data = EvalObjectData::default();
|
|
2012
2130
|
for prop in props {
|
|
2013
2131
|
match prop {
|
|
2014
|
-
tishlang_ast::ObjectProp::KeyValue(k, v) => {
|
|
2132
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
|
|
2015
2133
|
data
|
|
2016
2134
|
.strings
|
|
2017
2135
|
.insert(Arc::clone(k), self.eval_expr(v)?);
|
|
@@ -3546,188 +3664,16 @@ impl Evaluator {
|
|
|
3546
3664
|
}
|
|
3547
3665
|
|
|
3548
3666
|
fn json_parse(s: &str) -> Value {
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
fn json_parse_str(s: &str) -> Result<Value, ()> {
|
|
3560
|
-
let s = s.trim();
|
|
3561
|
-
if s.is_empty() {
|
|
3562
|
-
return Err(());
|
|
3563
|
-
}
|
|
3564
|
-
if s == "null" {
|
|
3565
|
-
return Ok(Value::Null);
|
|
3566
|
-
}
|
|
3567
|
-
if s == "true" {
|
|
3568
|
-
return Ok(Value::Bool(true));
|
|
3569
|
-
}
|
|
3570
|
-
if s == "false" {
|
|
3571
|
-
return Ok(Value::Bool(false));
|
|
3572
|
-
}
|
|
3573
|
-
if s.starts_with('"') {
|
|
3574
|
-
return Self::json_parse_string_full(s);
|
|
3575
|
-
}
|
|
3576
|
-
if s.starts_with('[') {
|
|
3577
|
-
return Self::json_parse_array(s);
|
|
3578
|
-
}
|
|
3579
|
-
if s.starts_with('{') {
|
|
3580
|
-
return Self::json_parse_object(s);
|
|
3581
|
-
}
|
|
3582
|
-
if let Ok(n) = s.parse::<f64>() {
|
|
3583
|
-
return Ok(Value::Number(n));
|
|
3584
|
-
}
|
|
3585
|
-
Err(())
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
|
-
fn json_parse_string(s: &str) -> Result<(Value, &str), ()> {
|
|
3589
|
-
let s = &s[1..];
|
|
3590
|
-
let mut out = String::new();
|
|
3591
|
-
let mut i = 0;
|
|
3592
|
-
let chars: Vec<char> = s.chars().collect();
|
|
3593
|
-
while i < chars.len() {
|
|
3594
|
-
if chars[i] == '"' {
|
|
3595
|
-
let rest_start = s.chars().take(i + 1).map(|c| c.len_utf8()).sum::<usize>();
|
|
3596
|
-
return Ok((Value::String(out.into()), &s[rest_start..]));
|
|
3597
|
-
}
|
|
3598
|
-
if chars[i] == '\\' {
|
|
3599
|
-
i += 1;
|
|
3600
|
-
if i >= chars.len() {
|
|
3601
|
-
return Err(());
|
|
3602
|
-
}
|
|
3603
|
-
match chars[i] {
|
|
3604
|
-
'"' => out.push('"'),
|
|
3605
|
-
'\\' => out.push('\\'),
|
|
3606
|
-
'n' => out.push('\n'),
|
|
3607
|
-
'r' => out.push('\r'),
|
|
3608
|
-
't' => out.push('\t'),
|
|
3609
|
-
_ => return Err(()),
|
|
3610
|
-
}
|
|
3611
|
-
} else {
|
|
3612
|
-
out.push(chars[i]);
|
|
3613
|
-
}
|
|
3614
|
-
i += 1;
|
|
3615
|
-
}
|
|
3616
|
-
Err(())
|
|
3617
|
-
}
|
|
3618
|
-
|
|
3619
|
-
fn json_parse_string_full(s: &str) -> Result<Value, ()> {
|
|
3620
|
-
Self::json_parse_string(s).map(|(v, _)| v)
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
fn json_parse_array(s: &str) -> Result<Value, ()> {
|
|
3624
|
-
let s = s[1..].trim_start();
|
|
3625
|
-
if s.starts_with(']') {
|
|
3626
|
-
return Ok(Value::Array(Rc::new(RefCell::new(vec![]))));
|
|
3627
|
-
}
|
|
3628
|
-
let mut vals = Vec::new();
|
|
3629
|
-
let mut rest = s;
|
|
3630
|
-
loop {
|
|
3631
|
-
let (v, next) = Self::json_parse_one(rest)?;
|
|
3632
|
-
vals.push(v);
|
|
3633
|
-
rest = next.trim_start();
|
|
3634
|
-
if rest.starts_with(']') {
|
|
3635
|
-
break;
|
|
3636
|
-
}
|
|
3637
|
-
if !rest.starts_with(',') {
|
|
3638
|
-
return Err(());
|
|
3639
|
-
}
|
|
3640
|
-
rest = rest[1..].trim_start();
|
|
3641
|
-
}
|
|
3642
|
-
Ok(Value::Array(Rc::new(RefCell::new(vals))))
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
fn json_parse_object(s: &str) -> Result<Value, ()> {
|
|
3646
|
-
let s = s[1..].trim_start();
|
|
3647
|
-
if s.starts_with('}') {
|
|
3648
|
-
return Ok(Value::object(PropMap::default()));
|
|
3649
|
-
}
|
|
3650
|
-
let mut map = PropMap::default();
|
|
3651
|
-
let mut rest = s;
|
|
3652
|
-
loop {
|
|
3653
|
-
if !rest.starts_with('"') {
|
|
3654
|
-
return Err(());
|
|
3655
|
-
}
|
|
3656
|
-
let (key_val, next) = Self::json_parse_string(rest)?;
|
|
3657
|
-
let key = match &key_val {
|
|
3658
|
-
Value::String(k) => Arc::clone(k),
|
|
3659
|
-
_ => return Err(()),
|
|
3660
|
-
};
|
|
3661
|
-
rest = next.trim_start();
|
|
3662
|
-
if !rest.starts_with(':') {
|
|
3663
|
-
return Err(());
|
|
3664
|
-
}
|
|
3665
|
-
rest = rest[1..].trim_start();
|
|
3666
|
-
let (val, next) = Self::json_parse_one(rest)?;
|
|
3667
|
-
map.insert(key, val);
|
|
3668
|
-
rest = next.trim_start();
|
|
3669
|
-
if rest.starts_with('}') {
|
|
3670
|
-
break;
|
|
3671
|
-
}
|
|
3672
|
-
if !rest.starts_with(',') {
|
|
3673
|
-
return Err(());
|
|
3674
|
-
}
|
|
3675
|
-
rest = rest[1..].trim_start();
|
|
3676
|
-
}
|
|
3677
|
-
Ok(Value::object(map))
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
fn json_parse_one(s: &str) -> Result<(Value, &str), ()> {
|
|
3681
|
-
let s = s.trim();
|
|
3682
|
-
if s.is_empty() {
|
|
3683
|
-
return Err(());
|
|
3684
|
-
}
|
|
3685
|
-
if s.starts_with('"') {
|
|
3686
|
-
let (v, rest) = Self::json_parse_string(s)?;
|
|
3687
|
-
Ok((v, rest))
|
|
3688
|
-
} else if s.starts_with('[') {
|
|
3689
|
-
let mut depth = 0;
|
|
3690
|
-
for (i, c) in s.char_indices() {
|
|
3691
|
-
if c == '[' {
|
|
3692
|
-
depth += 1;
|
|
3693
|
-
} else if c == ']' {
|
|
3694
|
-
depth -= 1;
|
|
3695
|
-
if depth == 0 {
|
|
3696
|
-
let v = Self::json_parse_array(&s[..=i])?;
|
|
3697
|
-
return Ok((v, &s[i + c.len_utf8()..]));
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
Err(())
|
|
3702
|
-
} else if s.starts_with('{') {
|
|
3703
|
-
let mut depth = 0;
|
|
3704
|
-
for (i, c) in s.char_indices() {
|
|
3705
|
-
if c == '{' {
|
|
3706
|
-
depth += 1;
|
|
3707
|
-
} else if c == '}' {
|
|
3708
|
-
depth -= 1;
|
|
3709
|
-
if depth == 0 {
|
|
3710
|
-
let v = Self::json_parse_object(&s[..=i])?;
|
|
3711
|
-
return Ok((v, &s[i + c.len_utf8()..]));
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
Err(())
|
|
3716
|
-
} else if let Some(rest) = s.strip_prefix("null") {
|
|
3717
|
-
Ok((Value::Null, rest))
|
|
3718
|
-
} else if let Some(rest) = s.strip_prefix("true") {
|
|
3719
|
-
Ok((Value::Bool(true), rest))
|
|
3720
|
-
} else if let Some(rest) = s.strip_prefix("false") {
|
|
3721
|
-
Ok((Value::Bool(false), rest))
|
|
3722
|
-
} else {
|
|
3723
|
-
let end = s
|
|
3724
|
-
.find(|c: char| {
|
|
3725
|
-
!c.is_ascii_digit() && c != '-' && c != '+' && c != '.' && c != 'e' && c != 'E'
|
|
3726
|
-
})
|
|
3727
|
-
.unwrap_or(s.len());
|
|
3728
|
-
let num_str = &s[..end];
|
|
3729
|
-
let n: f64 = num_str.parse().map_err(|_| ())?;
|
|
3730
|
-
Ok((Value::Number(n), &s[end..]))
|
|
3667
|
+
// Delegate to the shared, spec-correct parser in `tishlang_core` and convert into
|
|
3668
|
+
// interpreter Values — one source of truth with the VM/native/cranelift/wasi backends.
|
|
3669
|
+
// The previous hand-rolled parser depth-counted `{}`/`[]` brackets WITHOUT skipping string
|
|
3670
|
+
// contents, so a nested value whose string held `}`/`]`/`{`/`[` (e.g. `{"a":{"s":"}"}}`)
|
|
3671
|
+
// mis-sliced and the whole parse failed to `null` where Node/VM succeed; it also re-scanned
|
|
3672
|
+
// each nested value O(n^2). The core parser is string-aware and builds insertion-ordered
|
|
3673
|
+
// PropMaps. Invalid input still yields `null` (unchanged interpreter behavior).
|
|
3674
|
+
match tishlang_core::json_parse(s) {
|
|
3675
|
+
Ok(core) => crate::value_convert::core_to_eval(core),
|
|
3676
|
+
Err(_) => Value::Null,
|
|
3731
3677
|
}
|
|
3732
3678
|
}
|
|
3733
3679
|
|
|
@@ -3736,11 +3682,12 @@ impl Evaluator {
|
|
|
3736
3682
|
Value::Null => "null".to_string(),
|
|
3737
3683
|
Value::Bool(b) => b.to_string(),
|
|
3738
3684
|
Value::Number(n) => {
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3685
|
+
// Format numbers exactly like the VM/native path (shared helper) so JSON output
|
|
3686
|
+
// matches across backends and Node — Rust's `{}` Display diverges (e.g. `1e21`,
|
|
3687
|
+
// `1e-7`, `-0`). #180.
|
|
3688
|
+
let mut s = String::new();
|
|
3689
|
+
tishlang_core::write_json_number(&mut s, *n);
|
|
3690
|
+
s
|
|
3744
3691
|
}
|
|
3745
3692
|
Value::String(s) => format!(
|
|
3746
3693
|
"\"{}\"",
|
|
@@ -56,26 +56,14 @@ fn get_log_level() -> u8 {
|
|
|
56
56
|
|
|
57
57
|
pub fn parse_int(args: &[Value]) -> Result<Value, String> {
|
|
58
58
|
let s = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let n = if (2..=36).contains(&radix) {
|
|
68
|
-
let prefix: String = s
|
|
69
|
-
.chars()
|
|
70
|
-
.take_while(|c| *c == '-' || *c == '+' || c.is_digit(radix as u32))
|
|
71
|
-
.collect();
|
|
72
|
-
i64::from_str_radix(&prefix, radix as u32)
|
|
73
|
-
.ok()
|
|
74
|
-
.map(|n| n as f64)
|
|
75
|
-
} else {
|
|
76
|
-
None
|
|
77
|
-
};
|
|
78
|
-
Ok(Value::Number(n.unwrap_or(f64::NAN)))
|
|
59
|
+
let radix = args.get(1).and_then(|v| match v {
|
|
60
|
+
Value::Number(n) => Some(*n as i32),
|
|
61
|
+
_ => None,
|
|
62
|
+
});
|
|
63
|
+
// Shared semantics (0x stripping, sign, auto-radix) — see js_parse_int (#247).
|
|
64
|
+
Ok(Value::Number(tishlang_builtins::globals::js_parse_int(
|
|
65
|
+
&s, radix,
|
|
66
|
+
)))
|
|
79
67
|
}
|
|
80
68
|
|
|
81
69
|
pub fn parse_float(args: &[Value]) -> Result<Value, String> {
|
|
@@ -168,32 +156,44 @@ pub fn math_sqrt(args: &[Value]) -> Result<Value, String> {
|
|
|
168
156
|
))
|
|
169
157
|
}
|
|
170
158
|
|
|
159
|
+
// The JS-specific min/max/round semantics live in the shared `tishlang_builtins::math` f64 helpers
|
|
160
|
+
// (#247) — the interpreter uses a different `Value` type, so it extracts its own args and calls those
|
|
161
|
+
// helpers, keeping interp/vm/native bit-identical without duplicating the rules.
|
|
171
162
|
pub fn math_min(args: &[Value]) -> Result<Value, String> {
|
|
172
|
-
let nums: Vec<f64> = args
|
|
173
|
-
|
|
174
|
-
.filter_map(|v| match v {
|
|
175
|
-
Value::Number(n) => Some(*n),
|
|
176
|
-
_ => None,
|
|
177
|
-
})
|
|
178
|
-
.collect();
|
|
179
|
-
let n = nums.into_iter().fold(f64::INFINITY, f64::min);
|
|
180
|
-
Ok(Value::Number(if n == f64::INFINITY { f64::NAN } else { n }))
|
|
163
|
+
let nums: Vec<f64> = args.iter().map(get_num).collect();
|
|
164
|
+
Ok(Value::Number(tishlang_builtins::math::min_f64(&nums)))
|
|
181
165
|
}
|
|
182
166
|
|
|
183
167
|
pub fn math_max(args: &[Value]) -> Result<Value, String> {
|
|
184
|
-
let nums: Vec<f64> = args
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
168
|
+
let nums: Vec<f64> = args.iter().map(get_num).collect();
|
|
169
|
+
Ok(Value::Number(tishlang_builtins::math::max_f64(&nums)))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// hypot/asin/acos/atan/atan2 existed on the vm's Math but not the interpreter's — a direct interp≠vm
|
|
173
|
+
// gap (#247). These are 1:1 with Rust f64 methods (same as the builtins), so compute directly; hypot
|
|
174
|
+
// defaults missing args to 0.0 to match `tishlang_builtins::math::hypot`.
|
|
175
|
+
pub fn math_hypot(args: &[Value]) -> Result<Value, String> {
|
|
176
|
+
let x = args.first().map(get_num).unwrap_or(0.0);
|
|
177
|
+
let y = args.get(1).map(get_num).unwrap_or(0.0);
|
|
178
|
+
Ok(Value::Number(x.hypot(y)))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pub fn math_asin(args: &[Value]) -> Result<Value, String> {
|
|
182
|
+
Ok(Value::Number(get_num(args.first().unwrap_or(&Value::Null)).asin()))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pub fn math_acos(args: &[Value]) -> Result<Value, String> {
|
|
186
|
+
Ok(Value::Number(get_num(args.first().unwrap_or(&Value::Null)).acos()))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pub fn math_atan(args: &[Value]) -> Result<Value, String> {
|
|
190
|
+
Ok(Value::Number(get_num(args.first().unwrap_or(&Value::Null)).atan()))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
pub fn math_atan2(args: &[Value]) -> Result<Value, String> {
|
|
194
|
+
let y = get_num(args.first().unwrap_or(&Value::Null));
|
|
195
|
+
let x = get_num(args.get(1).unwrap_or(&Value::Null));
|
|
196
|
+
Ok(Value::Number(y.atan2(x)))
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
pub fn math_floor(args: &[Value]) -> Result<Value, String> {
|
|
@@ -209,9 +209,9 @@ pub fn math_ceil(args: &[Value]) -> Result<Value, String> {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
pub fn math_round(args: &[Value]) -> Result<Value, String> {
|
|
212
|
-
Ok(Value::Number(
|
|
213
|
-
|
|
214
|
-
))
|
|
212
|
+
Ok(Value::Number(tishlang_builtins::math::round_f64(get_num(
|
|
213
|
+
args.first().unwrap_or(&Value::Null),
|
|
214
|
+
))))
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
pub fn math_random(_args: &[Value]) -> Result<Value, String> {
|