@tishlang/tish-lsp 3.10.4 → 3.10.9
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-lsp +0 -0
- package/crates/tish/src/main.rs +65 -25
- package/crates/tish_builtins/src/globals.rs +3 -3
- package/crates/tish_builtins/src/symbol.rs +86 -4
- package/crates/tish_bytecode/src/compiler.rs +22 -6
- package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
- package/crates/tish_core/src/shape.rs +55 -2
- package/crates/tish_core/src/value.rs +198 -9
- package/crates/tish_eval/src/timers.rs +66 -2
- package/crates/tish_ffi/src/lib.rs +74 -12
- package/crates/tish_ffi/tests/loader.rs +35 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_lsp/src/main.rs +91 -2
- package/crates/tish_runtime/src/http_hyper.rs +18 -2
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_runtime/src/net.rs +125 -97
- package/crates/tish_runtime/src/process_spawn.rs +326 -113
- package/crates/tish_runtime/src/pty.rs +161 -107
- package/crates/tish_runtime/src/stream_buf.rs +419 -0
- package/crates/tish_runtime/src/ws.rs +431 -52
- package/crates/tish_vm/src/jit.rs +473 -67
- package/crates/tish_vm/src/lib.rs +6 -0
- package/crates/tish_vm/src/vm.rs +3 -1
- package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -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
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
mod jit;
|
|
5
5
|
mod vm;
|
|
6
6
|
|
|
7
|
+
/// Test/diagnostic introspection of the process-global JIT cache sizes (#703). Hidden: not a stable
|
|
8
|
+
/// public API — exists so regression tests and embedder diagnostics can observe that the caches grow
|
|
9
|
+
/// with distinct program text, not with closure-instantiation volume.
|
|
10
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
11
|
+
#[doc(hidden)]
|
|
12
|
+
pub use jit::jit_cache_lens;
|
|
7
13
|
pub use vm::{all_compiled_capabilities, run, run_with_options, Vm, VmRunOptions};
|
|
8
14
|
|
|
9
15
|
#[cfg(test)]
|
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -2610,7 +2610,9 @@ impl Vm {
|
|
|
2610
2610
|
.get(*nested_idx)
|
|
2611
2611
|
.ok_or_else(|| "Nested chunk index out of bounds".to_string())?;
|
|
2612
2612
|
// Numeric JIT fast path (native codegen, non-wasm): if this is a
|
|
2613
|
-
// straight-line numeric function, compile it once (cached
|
|
2613
|
+
// straight-line numeric function, compile it once (cached by chunk
|
|
2614
|
+
// CONTENT, #703 — the `inner.clone()` below makes addresses
|
|
2615
|
+
// per-instance, so every instance of one source fn shares a compile)
|
|
2614
2616
|
// and call native code when all args are numbers; else fall back to
|
|
2615
2617
|
// the interpreter below. Purely additive — can't change behaviour.
|
|
2616
2618
|
#[cfg(not(target_arch = "wasm32"))]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//! #716: a body-level `function NAME` used to be bound with `DeclareVar` into the very
|
|
2
|
+
//! `local_scope` map its own closure had just captured (`Constant::Closure` →
|
|
3
|
+
//! `VmClosure.enclosing`), minting a deterministic closure→scope→closure reference cycle
|
|
4
|
+
//! per call. The runtime is refcount-only, so every such call frame — including everything
|
|
5
|
+
//! reachable from its name-based locals — leaked (measured ~6.7 KB/call in the issue).
|
|
6
|
+
//! In general slot mode an UNCAPTURED function name is now slot-bound instead, so the
|
|
7
|
+
//! closure never enters the captured map and the frame drops normally on return.
|
|
8
|
+
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use tishlang_bytecode::compile;
|
|
11
|
+
use tishlang_core::{Value, VmRef};
|
|
12
|
+
use tishlang_vm::Vm;
|
|
13
|
+
|
|
14
|
+
fn run_src(src: &str) {
|
|
15
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
16
|
+
let chunk = compile(&program).expect("compile");
|
|
17
|
+
tishlang_vm::run(&chunk).expect("run");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// These tests pin slot-mode behavior; a suite run with `TISH_VM_SLOTS=0` deliberately
|
|
21
|
+
/// exercises the legacy name-based path (which still has the #716 cycle).
|
|
22
|
+
fn slots_disabled() -> bool {
|
|
23
|
+
std::env::var("TISH_VM_SLOTS")
|
|
24
|
+
.map(|v| v == "0")
|
|
25
|
+
.unwrap_or(false)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The leak regression itself: N calls of a function with an uncaptured body-level named
|
|
29
|
+
/// fn must not retain the N frames. The probe array is reachable from each frame's
|
|
30
|
+
/// captured local `s`; before the fix `helper`'s `DeclareVar` completed the cycle and
|
|
31
|
+
/// `VmRef::strong_count(&probe)` ended at `2 + N` — every frame stayed alive.
|
|
32
|
+
#[test]
|
|
33
|
+
fn uncaptured_body_fn_frames_are_reclaimed() {
|
|
34
|
+
if slots_disabled() {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let probe: VmRef<Vec<Value>> = VmRef::new(vec![Value::Number(1.0)]);
|
|
38
|
+
let mut vm = Vm::new();
|
|
39
|
+
vm.set_global(Arc::from("probe"), Value::Array(probe.clone()));
|
|
40
|
+
let src = r#"
|
|
41
|
+
function work() {
|
|
42
|
+
let s = probe
|
|
43
|
+
function helper() { return s.length }
|
|
44
|
+
return helper()
|
|
45
|
+
}
|
|
46
|
+
let i = 0
|
|
47
|
+
let acc = 0
|
|
48
|
+
while (i < 100) { acc = acc + work(); i = i + 1 }
|
|
49
|
+
if (acc !== 100) { throw "bad acc: " + acc }
|
|
50
|
+
"#;
|
|
51
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
52
|
+
let chunk = compile(&program).expect("compile");
|
|
53
|
+
vm.run(&chunk).expect("run");
|
|
54
|
+
// Exactly two live references: this test's `probe` clone + the VM globals entry.
|
|
55
|
+
// Each leaked frame would hold one more (its `local_scope` binds `s` → probe).
|
|
56
|
+
assert_eq!(
|
|
57
|
+
VmRef::strong_count(&probe),
|
|
58
|
+
2,
|
|
59
|
+
"call frames retained the probe — the #716 frame-scope↔closure cycle is back"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Self-recursion keeps the scope path (its own name IS a capture) and still works.
|
|
64
|
+
#[test]
|
|
65
|
+
fn self_recursive_body_fn_still_works() {
|
|
66
|
+
run_src(
|
|
67
|
+
r#"
|
|
68
|
+
function outer(n) {
|
|
69
|
+
function fact(k) { if (k < 2) { return 1 } return k * fact(k - 1) }
|
|
70
|
+
return fact(n)
|
|
71
|
+
}
|
|
72
|
+
if (outer(6) !== 720) { throw "self-recursion broken" }
|
|
73
|
+
"#,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Mutual recursion: both names are captured by the sibling closure; both keep the scope path.
|
|
78
|
+
#[test]
|
|
79
|
+
fn mutually_recursive_body_fns_still_work() {
|
|
80
|
+
run_src(
|
|
81
|
+
r#"
|
|
82
|
+
function outer(n) {
|
|
83
|
+
function isEven(k) { if (k === 0) { return true } return isOdd(k - 1) }
|
|
84
|
+
function isOdd(k) { if (k === 0) { return false } return isEven(k - 1) }
|
|
85
|
+
return isEven(n)
|
|
86
|
+
}
|
|
87
|
+
if (outer(10) !== true) { throw "mutual recursion broken (even)" }
|
|
88
|
+
if (outer(7) !== false) { throw "mutual recursion broken (odd)" }
|
|
89
|
+
"#,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// tish binds body-level fn decls sequentially (no JS hoisting): a use BEFORE the
|
|
94
|
+
/// declaration statement sees the outer binding, exactly as with `let`. The slot-bound
|
|
95
|
+
/// variant must preserve that — the reference site compiles before the slot exists, so
|
|
96
|
+
/// it stays a name-based load and resolves outward.
|
|
97
|
+
#[test]
|
|
98
|
+
fn body_fn_binding_is_sequential_not_hoisted() {
|
|
99
|
+
run_src(
|
|
100
|
+
r#"
|
|
101
|
+
let f = "global"
|
|
102
|
+
function outer() {
|
|
103
|
+
let before = f
|
|
104
|
+
function f() { return "local" }
|
|
105
|
+
return before + "|" + f()
|
|
106
|
+
}
|
|
107
|
+
if (outer() !== "global|local") { throw "sequential fn binding broken: " + outer() }
|
|
108
|
+
"#,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// An uncaptured body-level fn is callable, reassignable, passable as a value, and a
|
|
113
|
+
/// block-scoped one stays confined to its block — identical to the name-based path.
|
|
114
|
+
#[test]
|
|
115
|
+
fn body_fn_value_semantics_unchanged() {
|
|
116
|
+
run_src(
|
|
117
|
+
r#"
|
|
118
|
+
function outer() {
|
|
119
|
+
function f() { return 1 }
|
|
120
|
+
let a = f()
|
|
121
|
+
f = () => 2
|
|
122
|
+
let b = f()
|
|
123
|
+
function double(v) { return v * 2 }
|
|
124
|
+
let out = [1, 2, 3].map(double)
|
|
125
|
+
let got = 0
|
|
126
|
+
{
|
|
127
|
+
function inner() { return 39 }
|
|
128
|
+
got = inner()
|
|
129
|
+
}
|
|
130
|
+
return a + b + got + out[0] + out[1] + out[2]
|
|
131
|
+
}
|
|
132
|
+
if (outer() !== 54) { throw "body fn value semantics broken: " + outer() }
|
|
133
|
+
"#,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// A slot-bound fn that ESCAPES its frame (returned) still resolves its captures: the
|
|
138
|
+
/// closure carries the captured `local_scope` chain; only the name binding moved to a slot.
|
|
139
|
+
#[test]
|
|
140
|
+
fn escaping_body_fn_keeps_captures() {
|
|
141
|
+
run_src(
|
|
142
|
+
r#"
|
|
143
|
+
function makeGetter() {
|
|
144
|
+
let n = 7
|
|
145
|
+
function get() { return n }
|
|
146
|
+
return get
|
|
147
|
+
}
|
|
148
|
+
let g = makeGetter()
|
|
149
|
+
if (g() !== 7) { throw "escaping body fn lost its capture" }
|
|
150
|
+
"#,
|
|
151
|
+
);
|
|
152
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|