@tishlang/tish-format 3.10.4 → 3.10.11

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.
Files changed (44) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/tish/src/cargo_native_registry.rs +4 -0
  3. package/crates/tish/src/main.rs +65 -25
  4. package/crates/tish/tests/integration_test.rs +3 -0
  5. package/crates/tish_builtins/src/globals.rs +3 -3
  6. package/crates/tish_builtins/src/symbol.rs +86 -4
  7. package/crates/tish_bytecode/src/compiler.rs +65 -25
  8. package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
  9. package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
  10. package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
  11. package/crates/tish_core/src/shape.rs +55 -2
  12. package/crates/tish_core/src/value.rs +229 -13
  13. package/crates/tish_eval/src/timers.rs +66 -2
  14. package/crates/tish_ffi/src/lib.rs +74 -12
  15. package/crates/tish_ffi/tests/loader.rs +35 -0
  16. package/crates/tish_fmt/Cargo.toml +1 -1
  17. package/crates/tish_lsp/src/main.rs +91 -2
  18. package/crates/tish_pg/src/lib.rs +59 -8
  19. package/crates/tish_pg/src/statement_registry.rs +250 -0
  20. package/crates/tish_runtime/src/http.rs +19 -0
  21. package/crates/tish_runtime/src/http_fetch.rs +159 -65
  22. package/crates/tish_runtime/src/http_hyper.rs +18 -2
  23. package/crates/tish_runtime/src/lib.rs +4 -0
  24. package/crates/tish_runtime/src/net.rs +125 -97
  25. package/crates/tish_runtime/src/process_spawn.rs +326 -113
  26. package/crates/tish_runtime/src/promise.rs +521 -90
  27. package/crates/tish_runtime/src/promise_io.rs +29 -6
  28. package/crates/tish_runtime/src/pty.rs +161 -107
  29. package/crates/tish_runtime/src/stream_buf.rs +419 -0
  30. package/crates/tish_runtime/src/ws.rs +431 -52
  31. package/crates/tish_vm/src/jit.rs +473 -67
  32. package/crates/tish_vm/src/lib.rs +6 -0
  33. package/crates/tish_vm/src/vm.rs +8 -5
  34. package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
  35. package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
  36. package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -0
  37. package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
  38. package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -0
  39. package/package.json +1 -1
  40. package/platform/darwin-arm64/tish-fmt +0 -0
  41. package/platform/darwin-x64/tish-fmt +0 -0
  42. package/platform/linux-arm64/tish-fmt +0 -0
  43. package/platform/linux-x64/tish-fmt +0 -0
  44. package/platform/win32-x64/tish-fmt.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)]
@@ -2350,10 +2350,11 @@ impl Vm {
2350
2350
  let names = &chunk.names;
2351
2351
 
2352
2352
  let mut ip = 0;
2353
- // Lazily allocated name-keyed scope. Slot-based chunks never WRITE it (params + body locals
2354
- // live in `slot_locals`; `StoreVar` checks-then-falls-through to globals; a slot-based chunk
2355
- // has no captured locals by construction), so on the hot slot-based call path we skip the
2356
- // `VmRef::new(Arc<Mutex<HashMap>>)` box entirely. Non-slot chunks need it eagerly for params.
2353
+ // Lazily allocated name-keyed scope. A slot-based chunk with no captured names never writes
2354
+ // it (params + body locals live in `slot_locals`), so the hot slot-based call path skips the
2355
+ // `VmRef::new(Arc<Mutex<HashMap>>)` box entirely; captured locals (`DeclareVar`) and captured
2356
+ // params (the #716 copy-in prologue's `DeclareVarPlain`) materialise it on first use.
2357
+ // Non-slot chunks need it eagerly for params.
2357
2358
  // `ls_get_or_init!()` lazily creates it on the first write/capture; reads treat `None` as empty.
2358
2359
  let mut local_scope: Option<ScopeMap> = if chunk.slot_based {
2359
2360
  None
@@ -2610,7 +2611,9 @@ impl Vm {
2610
2611
  .get(*nested_idx)
2611
2612
  .ok_or_else(|| "Nested chunk index out of bounds".to_string())?;
2612
2613
  // Numeric JIT fast path (native codegen, non-wasm): if this is a
2613
- // straight-line numeric function, compile it once (cached per chunk)
2614
+ // straight-line numeric function, compile it once (cached by chunk
2615
+ // CONTENT, #703 — the `inner.clone()` below makes addresses
2616
+ // per-instance, so every instance of one source fn shares a compile)
2614
2617
  // and call native code when all args are numbers; else fall back to
2615
2618
  // the interpreter below. Purely additive — can't change behaviour.
2616
2619
  #[cfg(not(target_arch = "wasm32"))]
@@ -0,0 +1,210 @@
1
+ //! #716 follow-up: functions whose PARAMS are captured by a nested closure were
2
+ //! disqualified from general slot mode entirely (`slot_analyze` returned `None`), so the
3
+ //! per-call frame-scope↔closure cycle from #716 persisted for them — the issue's exact
4
+ //! repro (`handleEvent(payload)` with a body helper reading `payload`) still leaked after
5
+ //! #728.
6
+ //!
7
+ //! The fix is a captured-param COPY-IN PROLOGUE: params land in slots 0..n as the VM's
8
+ //! calling convention requires, and each captured param is copied into `local_scope` at
9
+ //! entry (one `LoadLocal` + `DeclareVarPlain`). From then on the SCOPE is the single
10
+ //! source of truth for that param — every body read/write compiles name-based
11
+ //! (`LoadVar`/`StoreVar`), the very same map closures capture — so shared-cell mutation
12
+ //! semantics are preserved exactly. The chunk stays in slot mode, and #728's slot-binding
13
+ //! of the (uncaptured) helper NAME applies: no cycle, the frame drops on return.
14
+
15
+ use std::sync::Arc;
16
+ use tishlang_bytecode::compile;
17
+ use tishlang_core::{Value, VmRef};
18
+ use tishlang_vm::Vm;
19
+
20
+ fn run_src(src: &str) {
21
+ let program = tishlang_parser::parse(src).expect("parse");
22
+ let chunk = compile(&program).expect("compile");
23
+ tishlang_vm::run(&chunk).expect("run");
24
+ }
25
+
26
+ /// These tests pin slot-mode behavior; a suite run with `TISH_VM_SLOTS=0` deliberately
27
+ /// exercises the legacy name-based path (which still has the #716 cycle).
28
+ fn slots_disabled() -> bool {
29
+ std::env::var("TISH_VM_SLOTS")
30
+ .map(|v| v == "0")
31
+ .unwrap_or(false)
32
+ }
33
+
34
+ /// The leak regression — the #716 repro shape verbatim (helper captures the enclosing
35
+ /// PARAM): N calls must not retain the N frames. The probe is the payload argument, so
36
+ /// each leaked frame would hold one strong ref via its `local_scope` (`payload` →
37
+ /// probe). Pre-fix the chunk is name-based and `fmt`'s `DeclareVar` completes the
38
+ /// scope↔closure cycle: `VmRef::strong_count(&probe)` ends at `2 + N`.
39
+ #[test]
40
+ fn captured_param_frames_are_reclaimed() {
41
+ if slots_disabled() {
42
+ return;
43
+ }
44
+ let probe: VmRef<Vec<Value>> = VmRef::new(vec![Value::Number(1.0)]);
45
+ let mut vm = Vm::new();
46
+ vm.set_global(Arc::from("probe"), Value::Array(probe.clone()));
47
+ let src = r#"
48
+ function handleEvent(payload) {
49
+ function fmt() { return payload.length }
50
+ return fmt()
51
+ }
52
+ let i = 0
53
+ let acc = 0
54
+ while (i < 100) { acc = acc + handleEvent(probe); i = i + 1 }
55
+ if (acc !== 100) { throw "bad acc: " + acc }
56
+ "#;
57
+ let program = tishlang_parser::parse(src).expect("parse");
58
+ let chunk = compile(&program).expect("compile");
59
+ vm.run(&chunk).expect("run");
60
+ // Exactly two live references: this test's `probe` clone + the VM globals entry.
61
+ assert_eq!(
62
+ VmRef::strong_count(&probe),
63
+ 2,
64
+ "call frames retained the probe — the captured-param variant of the #716 \
65
+ frame-scope↔closure cycle is still leaking"
66
+ );
67
+ }
68
+
69
+ /// MUTATION SEMANTICS, direction 1: the body reassigns the param, then a closure reads
70
+ /// it. The scope must be the single source of truth (a stale copy would return 1).
71
+ #[test]
72
+ fn closure_reads_param_after_body_reassigns() {
73
+ run_src(
74
+ r#"
75
+ function outer(p) {
76
+ function get() { return p }
77
+ p = p + 1
78
+ let r = get()
79
+ return r
80
+ }
81
+ if (outer(1) !== 2) { throw "closure saw a stale param copy: " + outer(1) }
82
+ "#,
83
+ );
84
+ }
85
+
86
+ /// MUTATION SEMANTICS, direction 2: a closure reassigns the param (via a call), then the
87
+ /// body reads it. The body read must route to the shared scope (a slot read would see 1).
88
+ #[test]
89
+ fn body_reads_param_after_closure_reassigns() {
90
+ run_src(
91
+ r#"
92
+ function outer(p) {
93
+ function bump() { p = p + 10 }
94
+ bump()
95
+ let r = p
96
+ return r
97
+ }
98
+ if (outer(1) !== 11) { throw "body saw a stale param slot: " + outer(1) }
99
+ "#,
100
+ );
101
+ }
102
+
103
+ /// Both directions interleaved, plus a second UNCAPTURED param that must stay slotted.
104
+ #[test]
105
+ fn interleaved_mutation_and_mixed_params() {
106
+ run_src(
107
+ r#"
108
+ function outer(p, q) {
109
+ function bump() { p = p + 1 }
110
+ p = p * 2
111
+ bump()
112
+ let r = p + q
113
+ return r
114
+ }
115
+ if (outer(3, 10) !== 17) { throw "interleaved captured-param mutation broken: " + outer(3, 10) }
116
+ "#,
117
+ );
118
+ }
119
+
120
+ /// An ESCAPING closure keeps the captured param (including a body reassignment made
121
+ /// before the escape) alive after the frame returns.
122
+ #[test]
123
+ fn escaping_closure_keeps_reassigned_param() {
124
+ run_src(
125
+ r#"
126
+ function mk(p) {
127
+ function get() { return p }
128
+ p = p * 2
129
+ return get
130
+ }
131
+ let g = mk(4)
132
+ if (g() !== 8) { throw "escaping closure lost the reassigned param: " + g() }
133
+ "#,
134
+ );
135
+ }
136
+
137
+ /// A DEFAULT on a captured param: the copy-in must run before the defaults prologue so
138
+ /// the applied default (stored name-based) is not clobbered by the missing-arg null.
139
+ #[test]
140
+ fn captured_param_default_applies() {
141
+ run_src(
142
+ r#"
143
+ function withDefault(a, p = 5) {
144
+ function get() { return p }
145
+ let r = a + get()
146
+ return r
147
+ }
148
+ if (withDefault(1) !== 6) { throw "captured-param default lost: " + withDefault(1) }
149
+ if (withDefault(1, 2) !== 3) { throw "captured-param supplied arg broken: " + withDefault(1, 2) }
150
+ "#,
151
+ );
152
+ }
153
+
154
+ /// Recursion: every level gets a fresh frame scope; each closure sees its own level's
155
+ /// param.
156
+ #[test]
157
+ fn recursion_with_captured_param() {
158
+ run_src(
159
+ r#"
160
+ function recur(n) {
161
+ function dec() { return n - 1 }
162
+ if (n === 0) { return 0 }
163
+ let x = recur(dec())
164
+ return x + n
165
+ }
166
+ if (recur(3) !== 6) { throw "recursion with captured param broken: " + recur(3) }
167
+ "#,
168
+ );
169
+ }
170
+
171
+ /// A MISSING captured param reads null — matching the interpreter (and slot mode's
172
+ /// existing missing-arg behavior). The legacy name-based VM chunk left the name unbound
173
+ /// ("Undefined variable"), a pre-existing interp divergence this shape no longer hits.
174
+ #[test]
175
+ fn missing_captured_param_is_null() {
176
+ if slots_disabled() {
177
+ return;
178
+ }
179
+ run_src(
180
+ r#"
181
+ function m(a, b) {
182
+ function g() { return b === null }
183
+ let r = g()
184
+ return r
185
+ }
186
+ if (m(1) !== true) { throw "missing captured param not bound to null" }
187
+ "#,
188
+ );
189
+ }
190
+
191
+ /// A block-scoped `let` shadowing the captured param: the shadow is undone at block
192
+ /// exit and the param's (copied-in) binding is restored.
193
+ #[test]
194
+ fn block_shadow_of_captured_param_restores() {
195
+ run_src(
196
+ r#"
197
+ function outer(p) {
198
+ function get() { return p }
199
+ let inner = 0
200
+ {
201
+ let p = 100
202
+ inner = p
203
+ }
204
+ let r = get() + inner
205
+ return r
206
+ }
207
+ if (outer(7) !== 107) { throw "block shadow of captured param broken: " + outer(7) }
208
+ "#,
209
+ );
210
+ }
@@ -0,0 +1,64 @@
1
+ //! #716 follow-up parity guard: the captured-param mutation-semantics shapes must behave
2
+ //! identically with `TISH_VM_SLOTS=0` (the legacy name-based path). This file holds
3
+ //! exactly ONE test so the process-global env var cannot race a sibling test — cargo
4
+ //! builds each file in `tests/` as its own binary/process.
5
+ //!
6
+ //! (The missing-arg shape is deliberately absent: the legacy name-based path leaves a
7
+ //! missing param unbound — "Undefined variable" — a pre-existing divergence from the
8
+ //! interpreter that slot mode does not share.)
9
+
10
+ use tishlang_bytecode::compile;
11
+
12
+ fn run_src(src: &str) {
13
+ let program = tishlang_parser::parse(src).expect("parse");
14
+ let chunk = compile(&program).expect("compile");
15
+ tishlang_vm::run(&chunk).expect("run");
16
+ }
17
+
18
+ #[test]
19
+ fn captured_param_semantics_identical_with_slots_off() {
20
+ std::env::set_var("TISH_VM_SLOTS", "0");
21
+ run_src(
22
+ r#"
23
+ function outer(p) {
24
+ function get() { return p }
25
+ p = p + 1
26
+ let r = get()
27
+ return r
28
+ }
29
+ if (outer(1) !== 2) { throw "slots-off: closure saw stale param: " + outer(1) }
30
+
31
+ function outer2(p) {
32
+ function bump() { p = p + 10 }
33
+ bump()
34
+ let r = p
35
+ return r
36
+ }
37
+ if (outer2(1) !== 11) { throw "slots-off: body saw stale param: " + outer2(1) }
38
+
39
+ function mk(p) {
40
+ function get() { return p }
41
+ p = p * 2
42
+ return get
43
+ }
44
+ let g = mk(4)
45
+ if (g() !== 8) { throw "slots-off: escaping closure lost reassigned param: " + g() }
46
+
47
+ function withDefault(a, p = 5) {
48
+ function get() { return p }
49
+ let r = a + get()
50
+ return r
51
+ }
52
+ if (withDefault(1) !== 6) { throw "slots-off: captured-param default lost" }
53
+ if (withDefault(1, 2) !== 3) { throw "slots-off: captured-param supplied arg broken" }
54
+
55
+ function recur(n) {
56
+ function dec() { return n - 1 }
57
+ if (n === 0) { return 0 }
58
+ let x = recur(dec())
59
+ return x + n
60
+ }
61
+ if (recur(3) !== 6) { throw "slots-off: recursion with captured param broken" }
62
+ "#,
63
+ );
64
+ }
@@ -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
+ }
@@ -0,0 +1,126 @@
1
+ //! #727: VM general slot mode dropped simple-param DEFAULT values.
2
+ //!
3
+ //! `function f(a, b = 5) { let c = a + b; return c }` puts the chunk in GENERAL slot
4
+ //! mode (`let c` breaks the param-only fast path; nothing is captured, so `slot_analyze`
5
+ //! accepts). Params are slot-bound, and every READ of `b` resolves to `LoadLocal <slot>`
6
+ //! — but `emit_param_defaults_prologue` consulted only `slot_ctx` (the simple param-only
7
+ //! map, `None` here) and wrote the applied default via `DeclareVarPlain` into the
8
+ //! name-based scope map. The default was invisible: `b` read the uninitialized slot's
9
+ //! `Null`, so `f(1)` computed `1 + null` → NaN.
10
+ //!
11
+ //! These are behavior tests — they must hold identically with `TISH_VM_SLOTS=0` (the
12
+ //! name-based legacy path, correct all along) and with slots on (default).
13
+
14
+ use tishlang_bytecode::compile;
15
+
16
+ fn run_src(src: &str) {
17
+ let program = tishlang_parser::parse(src).expect("parse");
18
+ let chunk = compile(&program).expect("compile");
19
+ tishlang_vm::run(&chunk).expect("run");
20
+ }
21
+
22
+ /// The issue repro: a missing arg must get its default in general slot mode.
23
+ #[test]
24
+ fn general_slot_mode_missing_arg_gets_default() {
25
+ run_src(
26
+ r#"
27
+ function f(a, b = 5) {
28
+ let c = a + b
29
+ return c
30
+ }
31
+ if (f(1) !== 6) { throw "default lost in general slot mode: f(1) = " + f(1) }
32
+ "#,
33
+ );
34
+ }
35
+
36
+ /// A supplied arg must win over the default (and stay slot-read correct).
37
+ #[test]
38
+ fn general_slot_mode_supplied_arg_wins() {
39
+ run_src(
40
+ r#"
41
+ function f(a, b = 5) {
42
+ let c = a + b
43
+ return c
44
+ }
45
+ if (f(1, 2) !== 3) { throw "supplied arg broken: f(1, 2) = " + f(1, 2) }
46
+ "#,
47
+ );
48
+ }
49
+
50
+ /// A default referencing an EARLIER param evaluates against that param's slot and must
51
+ /// land in this param's slot.
52
+ #[test]
53
+ fn general_slot_mode_default_references_earlier_param() {
54
+ run_src(
55
+ r#"
56
+ function dep(a, b = a + 1) {
57
+ let c = a + b
58
+ return c
59
+ }
60
+ if (dep(3) !== 7) { throw "dependent default lost: dep(3) = " + dep(3) }
61
+ if (dep(3, 10) !== 13) { throw "dependent default should not fire: dep(3, 10) = " + dep(3, 10) }
62
+ "#,
63
+ );
64
+ }
65
+
66
+ /// A chain of dependent defaults: each later default sees the earlier ones' results.
67
+ #[test]
68
+ fn general_slot_mode_chained_dependent_defaults() {
69
+ run_src(
70
+ r#"
71
+ function chain(a, b = a * 2, c = b + a) {
72
+ let out = [a, b, c]
73
+ return out
74
+ }
75
+ let r = chain(3)
76
+ if (r[0] !== 3 || r[1] !== 6 || r[2] !== 9) { throw "chained defaults broken: " + r }
77
+ let r2 = chain(3, 10)
78
+ if (r2[1] !== 10 || r2[2] !== 13) { throw "chained defaults (partial) broken: " + r2 }
79
+ "#,
80
+ );
81
+ }
82
+
83
+ /// A fn whose PARAM is captured is disqualified from slot mode entirely — the name-based
84
+ /// `DeclareVarPlain` path was always correct and must stay so.
85
+ #[test]
86
+ fn slot_disqualified_captured_param_default_unchanged() {
87
+ run_src(
88
+ r#"
89
+ function g(a, b = 5) {
90
+ function h() { return b }
91
+ let c = a + h()
92
+ return c
93
+ }
94
+ if (g(1) !== 6) { throw "name-based default broken: g(1) = " + g(1) }
95
+ if (g(1, 2) !== 3) { throw "name-based supplied arg broken: g(1, 2) = " + g(1, 2) }
96
+ "#,
97
+ );
98
+ }
99
+
100
+ /// The simple param-only fast path (`slot_ctx`) was always correct and must stay so.
101
+ #[test]
102
+ fn simple_param_only_fast_path_default_unchanged() {
103
+ run_src(
104
+ r#"
105
+ function f(a, b = 5) { return a + b }
106
+ if (f(1) !== 6) { throw "param-only default broken: f(1) = " + f(1) }
107
+ if (f(1, 2) !== 3) { throw "param-only supplied arg broken: f(1, 2) = " + f(1, 2) }
108
+ "#,
109
+ );
110
+ }
111
+
112
+ /// An explicit `null` is NOT a missing arg: the default must not apply (tish has no
113
+ /// `undefined`; `ArgMissing(i)` is true iff `i >= argc`). Same in both slot modes.
114
+ #[test]
115
+ fn explicit_null_is_not_missing() {
116
+ run_src(
117
+ r#"
118
+ function f(a, b = 5) {
119
+ let isNull = b === null
120
+ return isNull
121
+ }
122
+ if (f(1, null) !== true) { throw "explicit null was replaced by the default" }
123
+ if (f(1) !== false) { throw "missing arg did not get the default" }
124
+ "#,
125
+ );
126
+ }
@@ -0,0 +1,44 @@
1
+ //! #727 parity guard: the same default-parameter shapes must behave identically with
2
+ //! `TISH_VM_SLOTS=0` (the legacy name-based path). This file holds exactly ONE test so
3
+ //! the process-global env var cannot race a sibling test — cargo builds each file in
4
+ //! `tests/` as its own binary/process.
5
+
6
+ use tishlang_bytecode::compile;
7
+
8
+ fn run_src(src: &str) {
9
+ let program = tishlang_parser::parse(src).expect("parse");
10
+ let chunk = compile(&program).expect("compile");
11
+ tishlang_vm::run(&chunk).expect("run");
12
+ }
13
+
14
+ #[test]
15
+ fn param_defaults_behave_identically_with_slots_off() {
16
+ std::env::set_var("TISH_VM_SLOTS", "0");
17
+ run_src(
18
+ r#"
19
+ function f(a, b = 5) {
20
+ let c = a + b
21
+ return c
22
+ }
23
+ if (f(1) !== 6) { throw "slots-off default broken: f(1) = " + f(1) }
24
+ if (f(1, 2) !== 3) { throw "slots-off supplied arg broken: f(1, 2) = " + f(1, 2) }
25
+
26
+ function dep(a, b = a + 1) {
27
+ let c = a + b
28
+ return c
29
+ }
30
+ if (dep(3) !== 7) { throw "slots-off dependent default broken: dep(3) = " + dep(3) }
31
+ if (dep(3, 10) !== 13) { throw "slots-off dependent supplied broken" }
32
+
33
+ function g(a, b = 5) {
34
+ function h() { return b }
35
+ let c = a + h()
36
+ return c
37
+ }
38
+ if (g(1) !== 6) { throw "slots-off captured-param default broken: g(1) = " + g(1) }
39
+
40
+ function po(a, b = 5) { return a + b }
41
+ if (po(1) !== 6) { throw "slots-off param-only default broken" }
42
+ "#,
43
+ );
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-format",
3
- "version": "3.10.4",
3
+ "version": "3.10.11",
4
4
  "description": "Tish code formatter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file