@tishlang/tish-lsp 2.39.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +4 -1
- package/bin/tish-lsp +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
- package/crates/tish/tests/regr_typed_externs.rs +59 -0
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +65 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +2154 -225
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +195 -11
- package/crates/tish_compile/src/schemes.rs +333 -0
- package/crates/tish_compile/src/types.rs +205 -34
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
- package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
- package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
- package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
- package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
- package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
- package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
- package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
- package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
- package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
- package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +23 -1
- package/crates/tish_core/src/json.rs +42 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +305 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_lsp/Cargo.toml +3 -1
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/lib.rs +55 -9
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +804 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +583 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
//! and the tree-walk interpreter all route through here so every backend produces
|
|
6
6
|
//! byte-identical output — see `tish/docs/full-backend-parity-plan.md` (Workstream A).
|
|
7
7
|
|
|
8
|
+
#[cfg(feature = "portable")]
|
|
9
|
+
#[allow(unused_imports)]
|
|
10
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
11
|
+
#[cfg(feature = "portable")]
|
|
12
|
+
use tishlang_core::FloatExt;
|
|
13
|
+
|
|
8
14
|
use tishlang_core::Value;
|
|
9
15
|
|
|
10
16
|
/// `Number.prototype.toFixed(digits)` — ECMA-262 §21.1.3.3.
|
|
@@ -25,8 +31,16 @@ pub fn to_fixed(n: &Value, digits: &Value) -> Value {
|
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/// f64-domain `Number.prototype.toFixed` so the tree-walk interpreter (distinct `Value`) shares the
|
|
28
|
-
/// exact behavior.
|
|
29
|
-
/// from zero
|
|
34
|
+
/// exact behavior. tish's rule (#438): round the number's *true* decimal value to `digits` places,
|
|
35
|
+
/// ties away from zero — matching V8/node's observable `toFixed`.
|
|
36
|
+
///
|
|
37
|
+
/// The obvious `(num * 10^digits).round()` is wrong: float scaling nudges values that are actually
|
|
38
|
+
/// just below a tie up over it, so `(2.675).toFixed(2)` came out `2.68` where node gives `2.67`
|
|
39
|
+
/// (2.675 is stored as 2.67499999…). Instead we render the double's exact decimal expansion to well
|
|
40
|
+
/// beyond `digits` — f64 fractions terminate, so a generous precision captures the real digits — then
|
|
41
|
+
/// round that digit string: if the first dropped digit is ≥ 5, increment with decimal carry; else
|
|
42
|
+
/// truncate. On the magnitude, "first dropped ≥ 5" is round-half-up = round-half-away-from-zero, and
|
|
43
|
+
/// because the expansion is exact, non-ties round the way the true value dictates. #438
|
|
30
44
|
pub fn to_fixed_str(num: f64, digits: usize) -> String {
|
|
31
45
|
if num.is_nan() {
|
|
32
46
|
return "NaN".to_string();
|
|
@@ -38,9 +52,61 @@ pub fn to_fixed_str(num: f64, digits: usize) -> String {
|
|
|
38
52
|
if num.abs() >= 1e21 {
|
|
39
53
|
return format!("{}", num);
|
|
40
54
|
}
|
|
41
|
-
|
|
42
|
-
let
|
|
43
|
-
|
|
55
|
+
|
|
56
|
+
let negative = num.is_sign_negative() && num != 0.0;
|
|
57
|
+
let magnitude = num.abs();
|
|
58
|
+
|
|
59
|
+
// Exact-enough decimal expansion. The digit just past `digits` then reflects the double's TRUE
|
|
60
|
+
// value; +25 guard digits sit far beyond any position we inspect, so the half-to-even rounding
|
|
61
|
+
// Rust applies at the cutoff can't perturb our decision (it would take 25 consecutive 9s to carry
|
|
62
|
+
// back that far, and such a value already rounds up under the exact rule anyway).
|
|
63
|
+
let guard = digits + 25;
|
|
64
|
+
let rendered = format!("{:.*}", guard, magnitude);
|
|
65
|
+
let dot = rendered.find('.').expect("fixed formatting always has a decimal point");
|
|
66
|
+
let int_str = &rendered[..dot];
|
|
67
|
+
let frac = rendered[dot + 1..].as_bytes();
|
|
68
|
+
|
|
69
|
+
// Kept digits: the whole integer part, then `digits` fractional digits.
|
|
70
|
+
let mut ds: Vec<u8> = int_str
|
|
71
|
+
.bytes()
|
|
72
|
+
.chain(frac[..digits].iter().copied())
|
|
73
|
+
.map(|b| b - b'0')
|
|
74
|
+
.collect();
|
|
75
|
+
|
|
76
|
+
// Round half-away-from-zero on the magnitude: increment iff the first dropped digit is >= 5.
|
|
77
|
+
if frac[digits] - b'0' >= 5 {
|
|
78
|
+
let mut i = ds.len();
|
|
79
|
+
loop {
|
|
80
|
+
if i == 0 {
|
|
81
|
+
ds.insert(0, 1);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
i -= 1;
|
|
85
|
+
if ds[i] == 9 {
|
|
86
|
+
ds[i] = 0;
|
|
87
|
+
} else {
|
|
88
|
+
ds[i] += 1;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Split back into integer (all but the last `digits`) and fractional digits.
|
|
95
|
+
let int_len = ds.len() - digits;
|
|
96
|
+
let mut out = String::with_capacity(ds.len() + 2);
|
|
97
|
+
if negative {
|
|
98
|
+
out.push('-');
|
|
99
|
+
}
|
|
100
|
+
for &d in &ds[..int_len] {
|
|
101
|
+
out.push((d + b'0') as char);
|
|
102
|
+
}
|
|
103
|
+
if digits > 0 {
|
|
104
|
+
out.push('.');
|
|
105
|
+
for &d in &ds[int_len..] {
|
|
106
|
+
out.push((d + b'0') as char);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
out
|
|
44
110
|
}
|
|
45
111
|
|
|
46
112
|
/// `Number.prototype.toExponential(fractionDigits?)` — ECMA-262 §21.1.3.2. Exponential notation with
|
|
@@ -270,7 +336,7 @@ pub fn number_to_string_radix(num: f64, radix: i64) -> Option<String> {
|
|
|
270
336
|
if negative {
|
|
271
337
|
out.push('-');
|
|
272
338
|
}
|
|
273
|
-
out.push_str(
|
|
339
|
+
out.push_str(core::str::from_utf8(&int_digits).unwrap());
|
|
274
340
|
|
|
275
341
|
// Fractional part: multiply-by-radix, emitting the integer overflow each step.
|
|
276
342
|
if frac > 0.0 {
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
//! This module will contain shared object method implementations.
|
|
4
4
|
//! Functions will be migrated here from tishlang_runtime and tishlang_eval.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
#[cfg(feature = "portable")]
|
|
7
|
+
#[allow(unused_imports)]
|
|
8
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
9
|
+
|
|
10
|
+
use tishlang_core::Arc;
|
|
7
11
|
use tishlang_core::{ObjectData, PropMap, Value, VmRef};
|
|
8
12
|
|
|
9
13
|
/// Create a new empty object Value.
|
|
@@ -16,6 +20,7 @@ pub fn with_capacity(capacity: usize) -> Value {
|
|
|
16
20
|
Value::Object(VmRef::new(ObjectData {
|
|
17
21
|
strings: PropMap::with_capacity(capacity),
|
|
18
22
|
symbols: None,
|
|
23
|
+
frozen: false,
|
|
19
24
|
}))
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
//! All indices use character (Unicode scalar) positions for consistency with
|
|
4
4
|
//! JavaScript, matching .length and .charAt(). Byte offsets are never exposed.
|
|
5
5
|
|
|
6
|
+
#[cfg(feature = "portable")]
|
|
7
|
+
#[allow(unused_imports)]
|
|
8
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
9
|
+
|
|
6
10
|
use crate::helpers::normalize_index;
|
|
7
|
-
use
|
|
11
|
+
use core::cell::RefCell;
|
|
8
12
|
use tishlang_core::ArcStr;
|
|
9
13
|
use tishlang_core::Value;
|
|
10
14
|
use tishlang_core::VmRef;
|
|
15
|
+
#[cfg(feature = "portable")]
|
|
16
|
+
use tishlang_core::FloatExt;
|
|
11
17
|
|
|
12
18
|
// #203: a per-thread cursor cache that makes repeated character indexing (`charCodeAt(i)`, `s[i]`,
|
|
13
19
|
// `charAt(i)`) O(1)/near-O(1) instead of O(i). tish strings are UTF-8, so `chars().nth(i)` scans from
|
|
@@ -29,9 +35,14 @@ struct CharCursor {
|
|
|
29
35
|
byte_off: usize,
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
#[cfg(not(feature = "portable"))]
|
|
32
39
|
thread_local! {
|
|
33
40
|
static INDEX_CURSOR: RefCell<Option<CharCursor>> = const { RefCell::new(None) };
|
|
34
41
|
}
|
|
42
|
+
// Single-core: one shared cursor cache with the same `.with()` API.
|
|
43
|
+
#[cfg(feature = "portable")]
|
|
44
|
+
static INDEX_CURSOR: tishlang_core::SingleCore<RefCell<Option<CharCursor>>> =
|
|
45
|
+
tishlang_core::SingleCore::new(RefCell::new(None));
|
|
35
46
|
|
|
36
47
|
/// Borrow the cursor entry for `s`, reseeding (compute ASCII flag + character count) if it currently
|
|
37
48
|
/// caches a different backing allocation, then run `f` against it. Centralises the pointer-keyed
|
|
@@ -40,7 +51,7 @@ fn with_cursor<R>(s: &ArcStr, f: impl FnOnce(&mut CharCursor, &ArcStr) -> R) ->
|
|
|
40
51
|
INDEX_CURSOR.with(|cell| {
|
|
41
52
|
let mut slot = cell.borrow_mut();
|
|
42
53
|
let hit = matches!(slot.as_ref(), Some(c)
|
|
43
|
-
if
|
|
54
|
+
if core::ptr::eq(c.s.as_bytes().as_ptr(), s.as_bytes().as_ptr()) && c.s.len() == s.len());
|
|
44
55
|
if !hit {
|
|
45
56
|
let ascii = s.as_bytes().is_ascii();
|
|
46
57
|
// ASCII → character count equals byte length (free); otherwise count once.
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
//! ECMAScript-style `Symbol`, `Symbol.for`, `Symbol.keyFor`.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
#[cfg(feature = "portable")]
|
|
4
|
+
#[allow(unused_imports)]
|
|
5
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
5
6
|
|
|
6
|
-
use
|
|
7
|
+
use tishlang_core::sync::{Mutex, OnceLock};
|
|
8
|
+
use tishlang_core::{alloc_symbol_id, AHashMap, Arc, ObjectMap, TishSymbol, Value};
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
static SYMBOL_FOR_REGISTRY: OnceLock<Mutex<AHashMap<Arc<str>, Arc<TishSymbol>>>> = OnceLock::new();
|
|
9
11
|
|
|
10
|
-
static
|
|
11
|
-
|
|
12
|
-
fn symbol_registry() -> &'static Mutex<HashMap<Arc<str>, Arc<TishSymbol>>> {
|
|
13
|
-
SYMBOL_FOR_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
|
|
12
|
+
fn symbol_registry() -> &'static Mutex<AHashMap<Arc<str>, Arc<TishSymbol>>> {
|
|
13
|
+
SYMBOL_FOR_REGISTRY.get_or_init(|| Mutex::new(AHashMap::default()))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
fn symbol_for_impl(key: &str) -> Value {
|
|
17
17
|
let k: Arc<str> = key.into();
|
|
18
18
|
let mut reg = symbol_registry().lock().unwrap();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
// get-then-insert (rather than the `Entry` API) so this compiles against both
|
|
20
|
+
// std `HashMap` and the portable `hashbrown` map without a hasher-specific
|
|
21
|
+
// `Entry` import. `Symbol.for` is not a hot path.
|
|
22
|
+
if let Some(existing) = reg.get(&k) {
|
|
23
|
+
return Value::Symbol(existing.clone());
|
|
24
|
+
}
|
|
25
|
+
let id = alloc_symbol_id();
|
|
26
|
+
let sym = TishSymbol::new_registry(id, Arc::clone(&k), None);
|
|
27
|
+
reg.insert(k, Arc::clone(&sym));
|
|
28
28
|
Value::Symbol(sym)
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
//! - A typed array is a regular array at runtime, so `Array.isArray(ta)` is `true` and there is no
|
|
18
18
|
//! `instanceof` distinction.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
#[cfg(feature = "portable")]
|
|
21
|
+
#[allow(unused_imports)]
|
|
22
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
23
|
+
#[cfg(feature = "portable")]
|
|
24
|
+
use tishlang_core::FloatExt;
|
|
25
|
+
|
|
26
|
+
use tishlang_core::Arc;
|
|
21
27
|
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
22
28
|
|
|
23
29
|
const CONSTRUCT: &str = "__construct";
|
|
@@ -93,7 +99,7 @@ fn bytes_per_element(kind: Kind) -> f64 {
|
|
|
93
99
|
fn elements(value: &Value) -> Vec<Value> {
|
|
94
100
|
match value {
|
|
95
101
|
Value::Array(a) => a.borrow().clone(),
|
|
96
|
-
Value::NumberArray(a) => a.borrow().
|
|
102
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
97
103
|
_ => Vec::new(),
|
|
98
104
|
}
|
|
99
105
|
}
|
|
@@ -291,6 +297,7 @@ mod tests {
|
|
|
291
297
|
match &packed {
|
|
292
298
|
Value::NumberArray(a) => {
|
|
293
299
|
let v = a.borrow();
|
|
300
|
+
let v = v.as_packed().expect("packed backing");
|
|
294
301
|
assert_eq!(v[0], 1.1);
|
|
295
302
|
assert_eq!(v[1], 2.2);
|
|
296
303
|
assert!(v[2].is_nan());
|
|
@@ -11,7 +11,7 @@ use tishlang_ast::{
|
|
|
11
11
|
|
|
12
12
|
use crate::chunk::{Chunk, Constant};
|
|
13
13
|
use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
|
|
14
|
-
use crate::opcode::{MathUnaryFn, Opcode};
|
|
14
|
+
use crate::opcode::{MathBinaryFn, MathUnaryFn, Opcode};
|
|
15
15
|
|
|
16
16
|
enum SimpleMapResult {
|
|
17
17
|
Identity,
|
|
@@ -452,6 +452,23 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
|
452
452
|
/// so it can't run program-wide), this has an explicit `FunDecl` arm: a `function name` is itself a
|
|
453
453
|
/// binding, and every function body is recursed into. `true` (or any node it can't analyze) ⇒ NOT
|
|
454
454
|
/// stable — which only forgoes the cross-function-call optimization, never risks a miscompile.
|
|
455
|
+
/// #203: the f64 value of a `Math.<name>` constant, or `None` if `name` isn't one. The values are the
|
|
456
|
+
/// IEEE doubles ECMAScript specifies (identical to Rust's `std::f64::consts` and to the `Math` object
|
|
457
|
+
/// built on every backend, #539), so emitting them as a `LoadConst` is byte-identical to `Math.PI` etc.
|
|
458
|
+
fn math_constant(name: &str) -> Option<f64> {
|
|
459
|
+
Some(match name {
|
|
460
|
+
"PI" => std::f64::consts::PI,
|
|
461
|
+
"E" => std::f64::consts::E,
|
|
462
|
+
"LN2" => std::f64::consts::LN_2,
|
|
463
|
+
"LN10" => std::f64::consts::LN_10,
|
|
464
|
+
"LOG2E" => std::f64::consts::LOG2_E,
|
|
465
|
+
"LOG10E" => std::f64::consts::LOG10_E,
|
|
466
|
+
"SQRT2" => std::f64::consts::SQRT_2,
|
|
467
|
+
"SQRT1_2" => std::f64::consts::FRAC_1_SQRT_2,
|
|
468
|
+
_ => return None,
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
455
472
|
fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
|
|
456
473
|
match s {
|
|
457
474
|
Statement::FunDecl {
|
|
@@ -1968,6 +1985,9 @@ impl<'a> Compiler<'a> {
|
|
|
1968
1985
|
}
|
|
1969
1986
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
1970
1987
|
inner_comp.stable_globals = Arc::clone(&self.stable_globals);
|
|
1988
|
+
// #203: `Math` unshadowed is a whole-program property; thread it so `Math.<fn>` in this
|
|
1989
|
+
// function body lowers to the MathUnary intrinsic (else the numeric JIT bails).
|
|
1990
|
+
inner_comp.math_is_global = self.math_is_global;
|
|
1971
1991
|
// Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
|
|
1972
1992
|
// param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
|
|
1973
1993
|
// compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
|
|
@@ -2399,6 +2419,31 @@ impl<'a> Compiler<'a> {
|
|
|
2399
2419
|
}
|
|
2400
2420
|
}
|
|
2401
2421
|
}
|
|
2422
|
+
// #203: `Math.<binfn>(a, b)` → `<a>; <b>; MathBinary(id)` — the same unshadowed-global
|
|
2423
|
+
// gate, for exactly two positional args (variadic `Math.max(a,b,c)` falls through to a
|
|
2424
|
+
// generic call). Lets the numeric JIT lower clamp/pow kernels instead of bailing.
|
|
2425
|
+
if self.math_is_global && args.len() == 2 {
|
|
2426
|
+
if let Expr::Member {
|
|
2427
|
+
object,
|
|
2428
|
+
prop: MemberProp::Name { name: fname, .. },
|
|
2429
|
+
optional: false,
|
|
2430
|
+
..
|
|
2431
|
+
} = callee.as_ref()
|
|
2432
|
+
{
|
|
2433
|
+
if let (Expr::Ident { name: obj, .. }, CallArg::Expr(a0), CallArg::Expr(a1)) =
|
|
2434
|
+
(object.as_ref(), &args[0], &args[1])
|
|
2435
|
+
{
|
|
2436
|
+
if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
|
|
2437
|
+
if let Some(bfn) = MathBinaryFn::from_name(fname.as_ref()) {
|
|
2438
|
+
self.compile_expr(a0)?;
|
|
2439
|
+
self.compile_expr(a1)?;
|
|
2440
|
+
self.emit_u16(Opcode::MathBinary, bfn as u16);
|
|
2441
|
+
return Ok(());
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2402
2447
|
// Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
|
|
2403
2448
|
if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
|
|
2404
2449
|
&& args.len() == 1
|
|
@@ -2520,6 +2565,26 @@ impl<'a> Compiler<'a> {
|
|
|
2520
2565
|
optional,
|
|
2521
2566
|
..
|
|
2522
2567
|
} => {
|
|
2568
|
+
// #203: `Math.<CONST>` (PI / E / LN2 / …) when `Math` is the provably-unshadowed
|
|
2569
|
+
// global → a plain `LoadConst`, so the numeric JIT sees a constant instead of bailing
|
|
2570
|
+
// on a `GetMember`. The values match the `Math` object's on every backend (both use
|
|
2571
|
+
// `std::f64::consts`, #539), so this is byte-identical. Same gate as the intrinsics.
|
|
2572
|
+
if !*optional && self.math_is_global {
|
|
2573
|
+
if let (
|
|
2574
|
+
Expr::Ident { name: obj, .. },
|
|
2575
|
+
MemberProp::Name { name: p, .. },
|
|
2576
|
+
) = (object.as_ref(), prop)
|
|
2577
|
+
{
|
|
2578
|
+
if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
|
|
2579
|
+
if let Some(v) = math_constant(p.as_ref()) {
|
|
2580
|
+
let idx = self.constant_idx(Constant::Number(v));
|
|
2581
|
+
self.emit(Opcode::LoadConst);
|
|
2582
|
+
self.chunk.write_u16(idx);
|
|
2583
|
+
return Ok(());
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2523
2588
|
self.compile_expr(object)?;
|
|
2524
2589
|
if *optional {
|
|
2525
2590
|
self.emit(Opcode::Dup);
|
|
@@ -2686,6 +2751,7 @@ impl<'a> Compiler<'a> {
|
|
|
2686
2751
|
}
|
|
2687
2752
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
2688
2753
|
inner_comp.stable_globals = Arc::clone(&self.stable_globals); // #187
|
|
2754
|
+
inner_comp.math_is_global = self.math_is_global; // #203 (see other call site)
|
|
2689
2755
|
if let Some(map) = simple_slots {
|
|
2690
2756
|
inner_comp.slot_ctx = Some(map);
|
|
2691
2757
|
} else {
|
|
@@ -3116,7 +3182,15 @@ fn compile_internal(
|
|
|
3116
3182
|
// #186 — `Math` intrinsics are sound only if `Math` is never rebound anywhere in the program.
|
|
3117
3183
|
// `stmt_rebinds` is conservative (any rebind, destructure, FunDecl, or unknown node → true), so a
|
|
3118
3184
|
// `false` here only forgoes the optimization, never risks a miscompile.
|
|
3119
|
-
|
|
3185
|
+
// #203: whole-program scan (recurses into function bodies/params, unlike `stmt_rebinds` which is
|
|
3186
|
+
// conservatively `true` on any FunDecl) — so `Math` is provably-global even when the program
|
|
3187
|
+
// defines functions, letting `Math.<fn>` inside a function lower to the MathUnary intrinsic (the
|
|
3188
|
+
// numeric/array JIT then compiles the kernel instead of bailing on a generic call). Errs toward
|
|
3189
|
+
// "rebound" ⇒ never falsely global ⇒ no miscompile. Threaded into nested compilers below.
|
|
3190
|
+
compiler.math_is_global = !program
|
|
3191
|
+
.statements
|
|
3192
|
+
.iter()
|
|
3193
|
+
.any(|s| name_rebinds_in_stmt(s, "Math"));
|
|
3120
3194
|
// #187 — the set of top-level functions safe to call directly from JIT'd code (never reassigned/
|
|
3121
3195
|
// shadowed/redeclared anywhere). Conservative: a name absent here only forgoes the optimization.
|
|
3122
3196
|
compiler.stable_globals = Arc::new(compute_stable_globals(program));
|
|
@@ -16,5 +16,5 @@ pub use compiler::{
|
|
|
16
16
|
compile_with_source, CompileError,
|
|
17
17
|
};
|
|
18
18
|
pub use encoding::{binop_to_u8, compound_op_to_u8, u8_to_binop, u8_to_unaryop, unaryop_to_u8};
|
|
19
|
-
pub use opcode::{MathUnaryFn, Opcode};
|
|
19
|
+
pub use opcode::{MathBinaryFn, MathUnaryFn, Opcode};
|
|
20
20
|
pub use serialize::{deserialize, serialize};
|
|
@@ -145,6 +145,11 @@ pub enum Opcode {
|
|
|
145
145
|
/// lower it to a native op / libcall without a runtime shape guard. Behaviour is identical to
|
|
146
146
|
/// `LoadVar Math; GetMember fn; <arg>; Call 1` on a number.
|
|
147
147
|
MathUnary = 55,
|
|
148
|
+
/// #203 — apply a BINARY `Math.<fn>` intrinsic (max / min / pow / atan2 / hypot) to the top TWO
|
|
149
|
+
/// numbers: pop `b`, pop `a`, push `Math.fn(a, b)`. The u16 operand is the [`MathBinaryFn`] id.
|
|
150
|
+
/// Same emission gate as [`Opcode::MathUnary`] (`Math` provably the unshadowed global) but for
|
|
151
|
+
/// exactly two args. Behaviour identical to `LoadVar Math; GetMember fn; <a>; <b>; Call 2`.
|
|
152
|
+
MathBinary = 56,
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/// The unary `Math` functions the [`Opcode::MathUnary`] fast path recognizes (#186). f64→f64;
|
|
@@ -289,11 +294,81 @@ impl MathUnaryFn {
|
|
|
289
294
|
}
|
|
290
295
|
}
|
|
291
296
|
|
|
297
|
+
/// The binary `Math` functions the [`Opcode::MathBinary`] fast path recognizes (#203). `(f64, f64) →
|
|
298
|
+
/// f64`; the discriminant is the opcode operand. Kept in sync with the VM handler and JIT lowering.
|
|
299
|
+
/// (`hypot` is intentionally NOT here — it stays a correct generic call to avoid replicating its
|
|
300
|
+
/// scaled-sum-of-squares in this below-builtins crate.)
|
|
301
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
302
|
+
#[repr(u16)]
|
|
303
|
+
pub enum MathBinaryFn {
|
|
304
|
+
Max = 0,
|
|
305
|
+
Min = 1,
|
|
306
|
+
Pow = 2,
|
|
307
|
+
Atan2 = 3,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
impl MathBinaryFn {
|
|
311
|
+
/// Map a `Math.<name>` member to its id, or `None` if it isn't a supported 2-arg intrinsic.
|
|
312
|
+
pub fn from_name(name: &str) -> Option<MathBinaryFn> {
|
|
313
|
+
Some(match name {
|
|
314
|
+
"max" => MathBinaryFn::Max,
|
|
315
|
+
"min" => MathBinaryFn::Min,
|
|
316
|
+
"pow" => MathBinaryFn::Pow,
|
|
317
|
+
"atan2" => MathBinaryFn::Atan2,
|
|
318
|
+
_ => return None,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Decode an opcode operand to the fn id (safe match — no transmute).
|
|
323
|
+
pub fn from_u16(v: u16) -> Option<MathBinaryFn> {
|
|
324
|
+
use MathBinaryFn::*;
|
|
325
|
+
Some(match v {
|
|
326
|
+
0 => Max,
|
|
327
|
+
1 => Min,
|
|
328
|
+
2 => Pow,
|
|
329
|
+
3 => Atan2,
|
|
330
|
+
_ => return None,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Apply the intrinsic — the single source of truth for VM + JIT result parity. Replicates the JS
|
|
335
|
+
/// semantics of `tishlang_builtins::math` (this crate sits below builtins), so VM/JIT == interp.
|
|
336
|
+
#[inline]
|
|
337
|
+
pub fn apply(self, a: f64, b: f64) -> f64 {
|
|
338
|
+
match self {
|
|
339
|
+
// Math.max(a,b): NaN if either is NaN; else the larger, with +0 preferred over -0. Exactly
|
|
340
|
+
// `max_f64(&[a, b])`.
|
|
341
|
+
MathBinaryFn::Max => {
|
|
342
|
+
if a.is_nan() || b.is_nan() {
|
|
343
|
+
f64::NAN
|
|
344
|
+
} else if a > b || (a == 0.0 && b == 0.0 && a.is_sign_positive()) {
|
|
345
|
+
a
|
|
346
|
+
} else {
|
|
347
|
+
b
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Math.min(a,b): NaN if either is NaN; else the smaller, with -0 preferred. Exactly
|
|
351
|
+
// `min_f64(&[a, b])`.
|
|
352
|
+
MathBinaryFn::Min => {
|
|
353
|
+
if a.is_nan() || b.is_nan() {
|
|
354
|
+
f64::NAN
|
|
355
|
+
} else if a < b || (a == 0.0 && b == 0.0 && a.is_sign_negative()) {
|
|
356
|
+
a
|
|
357
|
+
} else {
|
|
358
|
+
b
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
MathBinaryFn::Pow => a.powf(b),
|
|
362
|
+
MathBinaryFn::Atan2 => a.atan2(b),
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
292
367
|
impl Opcode {
|
|
293
|
-
/// Decode byte to opcode. Safe for b in 0..=
|
|
368
|
+
/// Decode byte to opcode. Safe for b in 0..=56 (matches #[repr(u8)] discriminants).
|
|
294
369
|
#[inline]
|
|
295
370
|
pub fn from_u8(b: u8) -> Option<Opcode> {
|
|
296
|
-
if b <=
|
|
371
|
+
if b <= 56 {
|
|
297
372
|
Some(unsafe { std::mem::transmute::<u8, Opcode>(b) })
|
|
298
373
|
} else {
|
|
299
374
|
None
|