@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.
- package/bin/tish-format +0 -0
- package/crates/tish/src/cargo_native_registry.rs +4 -0
- package/crates/tish/src/main.rs +65 -25
- package/crates/tish/tests/integration_test.rs +3 -0
- 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 +65 -25
- package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
- package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
- package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
- package/crates/tish_core/src/shape.rs +55 -2
- package/crates/tish_core/src/value.rs +229 -13
- 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_fmt/Cargo.toml +1 -1
- package/crates/tish_lsp/src/main.rs +91 -2
- package/crates/tish_pg/src/lib.rs +59 -8
- package/crates/tish_pg/src/statement_registry.rs +250 -0
- package/crates/tish_runtime/src/http.rs +19 -0
- package/crates/tish_runtime/src/http_fetch.rs +159 -65
- 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/promise.rs +521 -90
- package/crates/tish_runtime/src/promise_io.rs +29 -6
- 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 +8 -5
- package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
- package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
- package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -0
- package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
- package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
package/bin/tish-format
CHANGED
|
Binary file
|
|
@@ -15,6 +15,10 @@ pub(crate) fn register_bytecode_native_modules(vm: &mut tishlang_vm::Vm) {
|
|
|
15
15
|
);
|
|
16
16
|
om.insert(Arc::from("connect"), Value::native(tishlang_pg::connect));
|
|
17
17
|
om.insert(Arc::from("prepare"), Value::native(tishlang_pg::prepare));
|
|
18
|
+
om.insert(
|
|
19
|
+
Arc::from("unprepare"),
|
|
20
|
+
Value::native(tishlang_pg::unprepare),
|
|
21
|
+
);
|
|
18
22
|
om.insert(
|
|
19
23
|
Arc::from("query_prepared"),
|
|
20
24
|
Value::native(tishlang_pg::query_prepared),
|
package/crates/tish/src/main.rs
CHANGED
|
@@ -57,13 +57,17 @@ fn vm_capabilities_for_cli_run(cli_features: &[String]) -> HashSet<String> {
|
|
|
57
57
|
/// `--feature` list for `tish build --target native`: same default as `tish run` (all linked-in caps).
|
|
58
58
|
fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
|
|
59
59
|
let mut v: Vec<String> = if cli_features.is_empty() {
|
|
60
|
-
tishlang_vm::all_compiled_capabilities()
|
|
60
|
+
tishlang_vm::all_compiled_capabilities()
|
|
61
|
+
.into_iter()
|
|
62
|
+
.collect()
|
|
61
63
|
} else {
|
|
62
64
|
// Normalize exactly like `tish run`: split comma-separated values (`--feature http,timers`)
|
|
63
65
|
// and expand `full`. Without this, a value like "http,timers,process" was treated as one
|
|
64
66
|
// opaque feature name, so `has_feature("http")` was false and the http runtime imports were
|
|
65
67
|
// never emitted — async `fetch` then failed to compile (issue #209).
|
|
66
|
-
normalize_capability_flags(cli_features)
|
|
68
|
+
normalize_capability_flags(cli_features)
|
|
69
|
+
.into_iter()
|
|
70
|
+
.collect()
|
|
67
71
|
};
|
|
68
72
|
v.sort();
|
|
69
73
|
v
|
|
@@ -73,8 +77,15 @@ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
|
|
|
73
77
|
fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
|
|
74
78
|
if argv.len() >= 2 {
|
|
75
79
|
let first = argv[1].as_str();
|
|
76
|
-
const SUBCOMMANDS: &[&str] =
|
|
77
|
-
|
|
80
|
+
const SUBCOMMANDS: &[&str] = &[
|
|
81
|
+
"run",
|
|
82
|
+
"repl",
|
|
83
|
+
"test",
|
|
84
|
+
"build",
|
|
85
|
+
"compile-module",
|
|
86
|
+
"resolve-id",
|
|
87
|
+
"dump-ast",
|
|
88
|
+
];
|
|
78
89
|
let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.contains(&first);
|
|
79
90
|
if looks_like_file {
|
|
80
91
|
argv.insert(1, "run".to_string());
|
|
@@ -112,19 +123,26 @@ fn main() {
|
|
|
112
123
|
// Expose a node-shaped `process.argv` to the script: `[tish-exe, <file>, args...]`,
|
|
113
124
|
// so `tish run main.tish a b` gives the script `["…/tish", "main.tish", "a", "b"]`
|
|
114
125
|
// (not the `run` subcommand). Interp/VM read this via `tishlang_core::process_argv`. #88
|
|
115
|
-
let exe = std::env::args()
|
|
126
|
+
let exe = std::env::args()
|
|
127
|
+
.next()
|
|
128
|
+
.unwrap_or_else(|| "tish".to_string());
|
|
116
129
|
let mut argv = Vec::with_capacity(a.script_args.len() + 2);
|
|
117
130
|
argv.push(exe);
|
|
118
131
|
argv.push(a.file.clone());
|
|
119
132
|
argv.extend(a.script_args.iter().cloned());
|
|
120
133
|
tishlang_core::set_process_argv(argv);
|
|
121
|
-
run_file(
|
|
134
|
+
run_file(
|
|
135
|
+
&a.file,
|
|
136
|
+
&a.backend,
|
|
137
|
+
&a.features,
|
|
138
|
+
a.no_optimize || no_opt_env,
|
|
139
|
+
)
|
|
122
140
|
}
|
|
123
141
|
Some(Commands::Test(a)) => {
|
|
124
142
|
#[cfg(feature = "test-runner")]
|
|
125
143
|
{
|
|
126
|
-
use tishlang_test::{run_tests, TestOptions};
|
|
127
144
|
use tishlang_test::report::ReporterKind;
|
|
145
|
+
use tishlang_test::{run_tests, TestOptions};
|
|
128
146
|
|
|
129
147
|
let reporter = match a.reporter.as_str() {
|
|
130
148
|
"dots" => ReporterKind::Dots,
|
|
@@ -169,7 +187,9 @@ fn main() {
|
|
|
169
187
|
} else {
|
|
170
188
|
"`--backend native` is not implemented yet"
|
|
171
189
|
};
|
|
172
|
-
eprintln!(
|
|
190
|
+
eprintln!(
|
|
191
|
+
"error: unsupported --backend `{backend}` for tish test (use `vm`); {hint}"
|
|
192
|
+
);
|
|
173
193
|
std::process::exit(2);
|
|
174
194
|
}
|
|
175
195
|
let opts = TestOptions {
|
|
@@ -219,10 +239,9 @@ fn main() {
|
|
|
219
239
|
if let Some(mode) = &a.check {
|
|
220
240
|
std::env::set_var("TISH_CHECK", mode);
|
|
221
241
|
}
|
|
222
|
-
if let Err(e) =
|
|
223
|
-
a.platform.as_deref(),
|
|
224
|
-
|
|
225
|
-
) {
|
|
242
|
+
if let Err(e) =
|
|
243
|
+
tishlang_compile::apply_resolve_env(a.platform.as_deref(), a.surface.as_deref())
|
|
244
|
+
{
|
|
226
245
|
eprintln!("Error: {}", e);
|
|
227
246
|
std::process::exit(1);
|
|
228
247
|
}
|
|
@@ -241,10 +260,9 @@ fn main() {
|
|
|
241
260
|
)
|
|
242
261
|
}
|
|
243
262
|
Some(Commands::CompileModule(a)) => {
|
|
244
|
-
if let Err(e) =
|
|
245
|
-
a.platform.as_deref(),
|
|
246
|
-
|
|
247
|
-
) {
|
|
263
|
+
if let Err(e) =
|
|
264
|
+
tishlang_compile::apply_resolve_env(a.platform.as_deref(), a.surface.as_deref())
|
|
265
|
+
{
|
|
248
266
|
eprintln!("Error: {}", e);
|
|
249
267
|
std::process::exit(1);
|
|
250
268
|
}
|
|
@@ -371,7 +389,14 @@ fn run_file(
|
|
|
371
389
|
.parent()
|
|
372
390
|
.map(|p| p.to_path_buf())
|
|
373
391
|
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
|
374
|
-
run_program(
|
|
392
|
+
run_program(
|
|
393
|
+
&program,
|
|
394
|
+
&ffi_base,
|
|
395
|
+
backend,
|
|
396
|
+
no_optimize,
|
|
397
|
+
features,
|
|
398
|
+
Some(path),
|
|
399
|
+
)
|
|
375
400
|
}
|
|
376
401
|
|
|
377
402
|
/// Load every `ffi:<path>` cdylib the program imports, resolving each path relative to `base_dir`
|
|
@@ -382,6 +407,8 @@ fn load_ffi_modules(
|
|
|
382
407
|
base_dir: &Path,
|
|
383
408
|
) -> Result<Vec<(String, tishlang_core::ObjectMap)>, String> {
|
|
384
409
|
let mut out = Vec::new();
|
|
410
|
+
// `ffi_native_specs` yields each spec once (deduplicated in first-seen order); distinct
|
|
411
|
+
// spellings of the same library collapse inside `load_module`'s resolved-path cache (#711).
|
|
385
412
|
for spec in tishlang_compile::ffi_native_specs(program) {
|
|
386
413
|
let rel = spec.strip_prefix("ffi:").unwrap_or(spec.as_str());
|
|
387
414
|
let lib_path = base_dir.join(rel);
|
|
@@ -804,17 +831,27 @@ fn compile_to_js_esm(
|
|
|
804
831
|
.into(),
|
|
805
832
|
);
|
|
806
833
|
}
|
|
807
|
-
let modules =
|
|
808
|
-
|
|
809
|
-
|
|
834
|
+
let modules = tishlang_compile_js::compile_project_esm(
|
|
835
|
+
input_path,
|
|
836
|
+
project_root,
|
|
837
|
+
optimize,
|
|
838
|
+
jsx_import_source,
|
|
839
|
+
)
|
|
840
|
+
.map_err(|e| format!("{}", e))?;
|
|
810
841
|
let out_dir = Path::new(output_path);
|
|
811
|
-
fs::create_dir_all(out_dir)
|
|
812
|
-
|
|
842
|
+
fs::create_dir_all(out_dir).map_err(|e| {
|
|
843
|
+
format!(
|
|
844
|
+
"Cannot create output directory {}: {}",
|
|
845
|
+
out_dir.display(),
|
|
846
|
+
e
|
|
847
|
+
)
|
|
848
|
+
})?;
|
|
813
849
|
for module in &modules {
|
|
814
850
|
let out = out_dir.join(&module.relative_path);
|
|
815
851
|
if let Some(parent) = out.parent() {
|
|
816
|
-
fs::create_dir_all(parent)
|
|
817
|
-
|
|
852
|
+
fs::create_dir_all(parent).map_err(|e| {
|
|
853
|
+
format!("Cannot create output directory {}: {}", parent.display(), e)
|
|
854
|
+
})?;
|
|
818
855
|
}
|
|
819
856
|
fs::write(&out, &module.js)
|
|
820
857
|
.map_err(|e| format!("Cannot write {}: {}", out.display(), e))?;
|
|
@@ -1195,7 +1232,10 @@ mod cli_tests {
|
|
|
1195
1232
|
// `full` expands to every capability, matching `tish run`.
|
|
1196
1233
|
let full = native_build_features_from_cli(&["full".to_string()]);
|
|
1197
1234
|
for cap in ["http", "timers", "fs", "process", "regex", "ws", "tty"] {
|
|
1198
|
-
assert!(
|
|
1235
|
+
assert!(
|
|
1236
|
+
full.iter().any(|s| s == cap),
|
|
1237
|
+
"`full` missing `{cap}` in {full:?}"
|
|
1238
|
+
);
|
|
1199
1239
|
}
|
|
1200
1240
|
}
|
|
1201
1241
|
|
|
@@ -1068,6 +1068,9 @@ allSettled[1] rejected: boom
|
|
|
1068
1068
|
allSettled[2] ok: 30
|
|
1069
1069
|
race winner: A
|
|
1070
1070
|
any passthrough: 42
|
|
1071
|
+
race pending-loser winner: fast
|
|
1072
|
+
loser still awaitable: late
|
|
1073
|
+
re-await: 7 7
|
|
1071
1074
|
";
|
|
1072
1075
|
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
|
|
1073
1076
|
let mut args = backend_args.clone();
|
|
@@ -436,9 +436,9 @@ pub fn object_assign(args: &[Value]) -> Value {
|
|
|
436
436
|
};
|
|
437
437
|
if let Value::Object(src) = source {
|
|
438
438
|
let src_borrow = src.borrow();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
// merge_from propagates dictionary-ness: copying from a DICT_SHAPE source (whose
|
|
440
|
+
// stored key order is not meaningful) must not re-mint tracked shape chains (#708).
|
|
441
|
+
target_mut.strings.merge_from(&src_borrow.strings);
|
|
442
442
|
if let Some(ss) = &src_borrow.symbols {
|
|
443
443
|
if target_mut.symbols.is_none() {
|
|
444
444
|
target_mut.symbols = Some(Default::default());
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
#[cfg(feature = "portable")]
|
|
4
4
|
#[allow(unused_imports)]
|
|
5
|
-
use alloc::{
|
|
5
|
+
use alloc::{
|
|
6
|
+
borrow::ToOwned,
|
|
7
|
+
boxed::Box,
|
|
8
|
+
format,
|
|
9
|
+
string::{String, ToString},
|
|
10
|
+
vec,
|
|
11
|
+
vec::Vec,
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
use tishlang_core::sync::{Mutex, OnceLock};
|
|
8
15
|
use tishlang_core::{alloc_symbol_id, AHashMap, Arc, ObjectMap, TishSymbol, Value};
|
|
@@ -33,7 +40,9 @@ fn symbol_new(args: &[Value]) -> Value {
|
|
|
33
40
|
if matches!(v, Value::Null) {
|
|
34
41
|
None
|
|
35
42
|
} else {
|
|
36
|
-
|
|
43
|
+
// ECMAScript ToString of the description (`Symbol({})` → "[object Object]"), not the
|
|
44
|
+
// console-inspect form (#715). Conformance-only here: unique symbols are refcounted.
|
|
45
|
+
Some(v.to_js_string().into())
|
|
37
46
|
}
|
|
38
47
|
});
|
|
39
48
|
Value::Symbol(TishSymbol::new_unique(desc))
|
|
@@ -54,10 +63,16 @@ fn symbol_key_for_impl(args: &[Value]) -> Value {
|
|
|
54
63
|
pub fn symbol_object() -> Value {
|
|
55
64
|
let call = Value::native(symbol_new);
|
|
56
65
|
let for_fn = Value::native(|args: &[Value]| {
|
|
66
|
+
// The registry key is the ECMAScript ToString of the argument (spec: `Symbol.for`
|
|
67
|
+
// coerces with ToString), NOT the console-inspect form (#715): every object keys the
|
|
68
|
+
// single "[object Object]" entry and an array keys its comma-joined form, exactly as
|
|
69
|
+
// in JS. The inspect form turned the immortal registry's program-text-bounded key set
|
|
70
|
+
// into a data-driven one — `Symbol.for({id: n})` interned one permanent entry per
|
|
71
|
+
// distinct rendered object. A missing argument keys "undefined" (ToString(undefined)).
|
|
57
72
|
let key = args
|
|
58
73
|
.first()
|
|
59
|
-
.map(|v| v.
|
|
60
|
-
.
|
|
74
|
+
.map(|v| v.to_js_string())
|
|
75
|
+
.unwrap_or_else(|| "undefined".to_string());
|
|
61
76
|
symbol_for_impl(&key)
|
|
62
77
|
});
|
|
63
78
|
let key_for = Value::native(symbol_key_for_impl);
|
|
@@ -80,4 +95,71 @@ mod tests {
|
|
|
80
95
|
let r = value_call(&o, &[Value::String("hi".into())]);
|
|
81
96
|
assert!(matches!(r, Value::Symbol(_)));
|
|
82
97
|
}
|
|
98
|
+
|
|
99
|
+
fn member(o: &Value, name: &str) -> Value {
|
|
100
|
+
match o {
|
|
101
|
+
Value::Object(obj) => obj
|
|
102
|
+
.borrow()
|
|
103
|
+
.strings
|
|
104
|
+
.get(name)
|
|
105
|
+
.cloned()
|
|
106
|
+
.unwrap_or_else(|| panic!("missing member {name}")),
|
|
107
|
+
other => panic!("expected object, got {other:?}"),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn as_symbol(v: Value) -> Arc<TishSymbol> {
|
|
112
|
+
match v {
|
|
113
|
+
Value::Symbol(s) => s,
|
|
114
|
+
other => panic!("expected symbol, got {other:?}"),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn test_object(n: f64) -> Value {
|
|
119
|
+
let mut m = ObjectMap::default();
|
|
120
|
+
m.insert(Arc::from("id"), Value::Number(n));
|
|
121
|
+
Value::object(m)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// #715: the registry keys on ToString, so two DISTINCT objects intern the SAME entry
|
|
125
|
+
// ("[object Object]") — previously each distinct inspect string ("{id: 1}", "{id: 2}", …)
|
|
126
|
+
// interned one immortal registry entry per call.
|
|
127
|
+
#[test]
|
|
128
|
+
fn symbol_for_keys_objects_by_tostring() {
|
|
129
|
+
let for_fn = member(&symbol_object(), "for");
|
|
130
|
+
let a = as_symbol(value_call(&for_fn, &[test_object(1.0)]));
|
|
131
|
+
let b = as_symbol(value_call(&for_fn, &[test_object(2.0)]));
|
|
132
|
+
assert!(
|
|
133
|
+
Arc::ptr_eq(&a, &b),
|
|
134
|
+
"distinct objects must intern one shared '[object Object]' registry entry"
|
|
135
|
+
);
|
|
136
|
+
assert_eq!(a.registry_key.as_deref(), Some("[object Object]"));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// String and number args keep their (spec-shaped) keys; ToString makes Symbol.for(1) and
|
|
140
|
+
// Symbol.for("1") the same symbol, as in JS.
|
|
141
|
+
#[test]
|
|
142
|
+
fn symbol_for_string_and_number_keys_unchanged() {
|
|
143
|
+
let for_fn = member(&symbol_object(), "for");
|
|
144
|
+
let s1 = as_symbol(value_call(&for_fn, &[Value::String("sym.key.715".into())]));
|
|
145
|
+
let s2 = as_symbol(value_call(&for_fn, &[Value::String("sym.key.715".into())]));
|
|
146
|
+
assert!(Arc::ptr_eq(&s1, &s2));
|
|
147
|
+
assert_eq!(s1.registry_key.as_deref(), Some("sym.key.715"));
|
|
148
|
+
|
|
149
|
+
let n = as_symbol(value_call(&for_fn, &[Value::Number(1.0)]));
|
|
150
|
+
let n_str = as_symbol(value_call(&for_fn, &[Value::String("1".into())]));
|
|
151
|
+
assert_eq!(n.registry_key.as_deref(), Some("1"));
|
|
152
|
+
assert!(
|
|
153
|
+
Arc::ptr_eq(&n, &n_str),
|
|
154
|
+
"Symbol.for(1) === Symbol.for(\"1\")"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A missing argument keys "undefined" (ToString(undefined)), not "".
|
|
159
|
+
#[test]
|
|
160
|
+
fn symbol_for_missing_arg_keys_undefined() {
|
|
161
|
+
let for_fn = member(&symbol_object(), "for");
|
|
162
|
+
let s = as_symbol(value_call(&for_fn, &[]));
|
|
163
|
+
assert_eq!(s.registry_key.as_deref(), Some("undefined"));
|
|
164
|
+
}
|
|
83
165
|
}
|
|
@@ -844,8 +844,11 @@ impl SlotScan {
|
|
|
844
844
|
|
|
845
845
|
/// Capture-aware eligibility for general slot-based locals in a FUNCTION. Returns the captured-name set
|
|
846
846
|
/// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
|
|
847
|
-
/// flag is on, no rest param, all params simple, the body fully analysable
|
|
848
|
-
///
|
|
847
|
+
/// flag is on, no rest param, all params simple, and the body fully analysable. A CAPTURED param does
|
|
848
|
+
/// NOT disqualify the chunk (#716 follow-up): the VM still binds the arg into its positional slot, and
|
|
849
|
+
/// a copy-in prologue moves it into `local_scope` at entry — from then on the scope is that param's
|
|
850
|
+
/// single source of truth (its name stays out of the slot map, so every body read/write compiles
|
|
851
|
+
/// name-based to the very map closures capture — shared-cell mutation semantics preserved exactly).
|
|
849
852
|
fn slot_analyze(
|
|
850
853
|
params: &[FunParam],
|
|
851
854
|
has_rest: bool,
|
|
@@ -863,13 +866,6 @@ fn slot_analyze(
|
|
|
863
866
|
if !scan.stmt(body, false) {
|
|
864
867
|
return None;
|
|
865
868
|
}
|
|
866
|
-
for p in params {
|
|
867
|
-
if let FunParam::Simple(tp) = p {
|
|
868
|
-
if scan.captured.contains(&tp.name) {
|
|
869
|
-
return None;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
869
|
Some(scan.captured)
|
|
874
870
|
}
|
|
875
871
|
|
|
@@ -1060,12 +1056,12 @@ impl<'a> Compiler<'a> {
|
|
|
1060
1056
|
self.emit_u16(Opcode::ArgMissing, i as u16);
|
|
1061
1057
|
let skip = self.emit_jump(Opcode::JumpIfFalse);
|
|
1062
1058
|
self.compile_expr(default_expr)?;
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
match
|
|
1059
|
+
// #727: resolve through BOTH slot maps (`resolve_slot`), not `slot_ctx` alone. In
|
|
1060
|
+
// GENERAL slot mode params live in `slot_scopes` (declared before this prologue) and
|
|
1061
|
+
// every read of the param lowers to `LoadLocal <slot>` — a `DeclareVarPlain` here wrote
|
|
1062
|
+
// the default into the name-based scope map no read resolves to, leaving the slot's
|
|
1063
|
+
// missing-arg `Null` visible (`f(a, b = 5); f(1)` → `1 + null` → NaN).
|
|
1064
|
+
match self.resolve_slot(tp.name.as_ref()) {
|
|
1069
1065
|
Some(slot) => self.emit_u16(Opcode::StoreLocal, slot),
|
|
1070
1066
|
None => {
|
|
1071
1067
|
let idx = self.name_idx(&tp.name);
|
|
@@ -2022,14 +2018,42 @@ impl<'a> Compiler<'a> {
|
|
|
2022
2018
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2023
2019
|
inner_comp.compile_fn_body(body)?;
|
|
2024
2020
|
} else if let Some(cap) = captured {
|
|
2025
|
-
// Params
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2021
|
+
// Params → slots 0..n (matching the VM's positional arg binding); uncaptured
|
|
2022
|
+
// body `let`s get fresh slots via the scope-aware allocator; captured locals
|
|
2023
|
+
// stay name-based in `local_scope` (which closures capture).
|
|
2024
|
+
//
|
|
2025
|
+
// #716 follow-up: a CAPTURED param no longer disqualifies the chunk. It keeps
|
|
2026
|
+
// its slot NUMBER (the VM binds args positionally, so numbering must stay
|
|
2027
|
+
// 0..n) but is NOT slot-resolvable: its name never enters `slot_scopes`, so
|
|
2028
|
+
// every body read/write compiles name-based (`LoadVar`/`StoreVar`) against
|
|
2029
|
+
// `local_scope` — the very map closures capture — keeping the scope the
|
|
2030
|
+
// single source of truth (shared-cell mutation semantics preserved exactly;
|
|
2031
|
+
// a body reassignment is visible to the closure and vice versa). The COPY-IN
|
|
2032
|
+
// prologue below moves each captured arg slot → scope once at entry, BEFORE
|
|
2033
|
+
// the defaults prologue so an applied default (stored name-based for a
|
|
2034
|
+
// captured param) overwrites the copied-in missing-arg null, never the other
|
|
2035
|
+
// way around. This keeps the chunk in slot mode, so the #716 fix (#728) slot-
|
|
2036
|
+
// binds an uncaptured body-level fn NAME and the per-call frame-scope↔closure
|
|
2037
|
+
// cycle never forms for the `handleEvent(payload){ function fmt(){…payload…} }`
|
|
2038
|
+
// shape.
|
|
2028
2039
|
inner_comp.general_slots = true;
|
|
2029
2040
|
inner_comp.slot_captured = cap;
|
|
2030
2041
|
inner_comp.slot_scopes.push(HashMap::new());
|
|
2042
|
+
let mut captured_param_copy_ins: Vec<(u16, Arc<str>)> = Vec::new();
|
|
2031
2043
|
for p in ¶m_names {
|
|
2032
|
-
inner_comp.
|
|
2044
|
+
if inner_comp.slot_captured.contains(p.as_ref()) {
|
|
2045
|
+
// Reserve the positional slot without making it resolvable.
|
|
2046
|
+
let slot = inner_comp.next_slot;
|
|
2047
|
+
inner_comp.next_slot += 1;
|
|
2048
|
+
captured_param_copy_ins.push((slot, Arc::clone(p)));
|
|
2049
|
+
} else {
|
|
2050
|
+
inner_comp.declare_slot(p);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
for (slot, p) in &captured_param_copy_ins {
|
|
2054
|
+
inner_comp.emit_u16(Opcode::LoadLocal, *slot);
|
|
2055
|
+
let idx = inner_comp.name_idx(p);
|
|
2056
|
+
inner_comp.emit_u16(Opcode::DeclareVarPlain, idx);
|
|
2033
2057
|
}
|
|
2034
2058
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2035
2059
|
inner_comp.compile_fn_body(body)?;
|
|
@@ -2056,12 +2080,28 @@ impl<'a> Compiler<'a> {
|
|
|
2056
2080
|
self.emit(Opcode::LoadConst);
|
|
2057
2081
|
let idx = self.constant_idx(Constant::Closure(nested_idx));
|
|
2058
2082
|
self.chunk.write_u16(idx);
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2083
|
+
// #716: bind the function's NAME like any other local. `DeclareVar` inserts the
|
|
2084
|
+
// closure into the very `local_scope` map the closure just captured (vm.rs
|
|
2085
|
+
// `Constant::Closure` → `VmClosure.enclosing`), minting a deterministic
|
|
2086
|
+
// closure→scope→closure cycle per call — refcount-only, so the whole frame leaked.
|
|
2087
|
+
// In general slot mode an UNCAPTURED name gets a frame slot instead: the closure
|
|
2088
|
+
// never enters the captured map, and the frame drops normally on return. A name
|
|
2089
|
+
// referenced from inside ANY nested closure — the function's own body
|
|
2090
|
+
// (self-recursion) or a sibling (mutual recursion) — is in `slot_captured`
|
|
2091
|
+
// (`SlotScan` scans FunDecl bodies as closures) and stays on the `DeclareVar`
|
|
2092
|
+
// path, exactly like a captured `let` (see `Statement::VarDecl` above).
|
|
2093
|
+
// `TISH_VM_SLOTS=0` ⇒ `general_slots` is false ⇒ byte-identical output.
|
|
2094
|
+
if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
|
|
2095
|
+
let slot = self.declare_slot(name);
|
|
2096
|
+
self.emit_u16(Opcode::StoreLocal, slot);
|
|
2097
|
+
} else {
|
|
2098
|
+
let idx = self.name_idx(name);
|
|
2099
|
+
self.emit_u16(Opcode::DeclareVar, idx);
|
|
2100
|
+
self.scope
|
|
2101
|
+
.last_mut()
|
|
2102
|
+
.unwrap()
|
|
2103
|
+
.insert(Arc::clone(name), false);
|
|
2104
|
+
}
|
|
2065
2105
|
}
|
|
2066
2106
|
Statement::DoWhile { body, cond, .. } => {
|
|
2067
2107
|
let body_lets = Self::loop_body_block_lets(body);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//! #716 follow-up: a function whose PARAM is captured by a nested closure must still
|
|
2
|
+
//! compile in general slot mode, with a captured-param COPY-IN PROLOGUE (`LoadLocal
|
|
3
|
+
//! <param slot>` + `DeclareVarPlain <name>`) so the closure reads the param from
|
|
4
|
+
//! `local_scope` while the chunk keeps frame slots for everything uncaptured —
|
|
5
|
+
//! including the body-level helper NAME (#728), which is what breaks the per-call
|
|
6
|
+
//! frame-scope↔closure cycle for the #716 repro shape.
|
|
7
|
+
|
|
8
|
+
use tishlang_bytecode::{compile, Chunk, Opcode};
|
|
9
|
+
|
|
10
|
+
/// Count occurrences of `target` in `chunk.code`, walking real instruction boundaries.
|
|
11
|
+
fn count_opcode(chunk: &Chunk, target: Opcode) -> usize {
|
|
12
|
+
let code = &chunk.code;
|
|
13
|
+
let mut ip = 0usize;
|
|
14
|
+
let mut n = 0usize;
|
|
15
|
+
while ip < code.len() {
|
|
16
|
+
let op = Opcode::from_u8(code[ip]).expect("valid opcode");
|
|
17
|
+
if op == target {
|
|
18
|
+
n += 1;
|
|
19
|
+
}
|
|
20
|
+
ip += op.instruction_size(code, ip).expect("sized instruction");
|
|
21
|
+
}
|
|
22
|
+
n
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn compile_src(src: &str) -> Chunk {
|
|
26
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
27
|
+
compile(&program).expect("compile")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// These tests assert slot-mode emission; running the suite with `TISH_VM_SLOTS=0`
|
|
31
|
+
/// deliberately exercises the legacy name-based path, where they do not apply.
|
|
32
|
+
fn slots_disabled() -> bool {
|
|
33
|
+
std::env::var("TISH_VM_SLOTS")
|
|
34
|
+
.map(|v| v == "0")
|
|
35
|
+
.unwrap_or(false)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The #716 repro shape: the chunk must stay slot-based, copy the captured param in
|
|
39
|
+
/// exactly once, and slot-bind the (uncaptured) helper name — zero `DeclareVar`.
|
|
40
|
+
#[test]
|
|
41
|
+
fn captured_param_chunk_stays_slot_based_with_copy_in() {
|
|
42
|
+
if slots_disabled() {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let chunk = compile_src(
|
|
46
|
+
r#"
|
|
47
|
+
function handleEvent(payload) {
|
|
48
|
+
function fmt(x) { return x + payload.tag }
|
|
49
|
+
return fmt("v")
|
|
50
|
+
}
|
|
51
|
+
console.log(handleEvent({ tag: "t" }))
|
|
52
|
+
"#,
|
|
53
|
+
);
|
|
54
|
+
let outer = &chunk.nested[0];
|
|
55
|
+
assert!(
|
|
56
|
+
outer.slot_based,
|
|
57
|
+
"a captured-param chunk must no longer be disqualified from general slot mode"
|
|
58
|
+
);
|
|
59
|
+
assert_eq!(
|
|
60
|
+
count_opcode(outer, Opcode::DeclareVarPlain),
|
|
61
|
+
1,
|
|
62
|
+
"exactly one copy-in store for the one captured param"
|
|
63
|
+
);
|
|
64
|
+
assert_eq!(
|
|
65
|
+
count_opcode(outer, Opcode::DeclareVar),
|
|
66
|
+
0,
|
|
67
|
+
"the uncaptured helper name must be slot-bound (#728), not DeclareVar-bound — \
|
|
68
|
+
that is the cycle"
|
|
69
|
+
);
|
|
70
|
+
assert!(
|
|
71
|
+
count_opcode(outer, Opcode::StoreLocal) >= 1,
|
|
72
|
+
"the helper closure should be stored to a frame slot"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Mixed params: only the CAPTURED one is copied in; the uncaptured one stays
|
|
77
|
+
/// slot-resolved (no name-based traffic for it).
|
|
78
|
+
#[test]
|
|
79
|
+
fn only_captured_params_are_copied_in() {
|
|
80
|
+
if slots_disabled() {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let chunk = compile_src(
|
|
84
|
+
r#"
|
|
85
|
+
function outer(p, q) {
|
|
86
|
+
function get() { return p }
|
|
87
|
+
let r = get() + q
|
|
88
|
+
return r
|
|
89
|
+
}
|
|
90
|
+
console.log(outer(1, 2))
|
|
91
|
+
"#,
|
|
92
|
+
);
|
|
93
|
+
let outer = &chunk.nested[0];
|
|
94
|
+
assert!(outer.slot_based, "mixed-param chunk should be slot-based");
|
|
95
|
+
assert_eq!(
|
|
96
|
+
count_opcode(outer, Opcode::DeclareVarPlain),
|
|
97
|
+
1,
|
|
98
|
+
"only the captured param `p` gets a copy-in store; `q` stays slotted"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// A self-recursive helper capturing the param: the helper NAME is captured (its own
|
|
103
|
+
/// body references it) and stays on the `DeclareVar` scope path, while the chunk itself
|
|
104
|
+
/// remains slot-based with the param copy-in.
|
|
105
|
+
#[test]
|
|
106
|
+
fn self_recursive_helper_keeps_scope_path_beside_copy_in() {
|
|
107
|
+
if slots_disabled() {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let chunk = compile_src(
|
|
111
|
+
r#"
|
|
112
|
+
function outer(n) {
|
|
113
|
+
function fact(k) { if (k < 2) { return n } return k * fact(k - 1) }
|
|
114
|
+
return fact(4)
|
|
115
|
+
}
|
|
116
|
+
console.log(outer(1))
|
|
117
|
+
"#,
|
|
118
|
+
);
|
|
119
|
+
let outer = &chunk.nested[0];
|
|
120
|
+
assert!(outer.slot_based, "chunk should stay slot-based");
|
|
121
|
+
assert_eq!(
|
|
122
|
+
count_opcode(outer, Opcode::DeclareVarPlain),
|
|
123
|
+
1,
|
|
124
|
+
"the captured param `n` gets its copy-in"
|
|
125
|
+
);
|
|
126
|
+
assert_eq!(
|
|
127
|
+
count_opcode(outer, Opcode::DeclareVar),
|
|
128
|
+
1,
|
|
129
|
+
"the self-recursive helper name is captured and stays DeclareVar-bound"
|
|
130
|
+
);
|
|
131
|
+
}
|