@tishlang/tish-lsp 2.12.0 → 2.35.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 +3 -0
- package/bin/tish-lsp +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- 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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//! #181 — direct Map method dispatch for `new Map()` locals.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
use tishlang_compile::compile_project_full;
|
|
6
|
+
|
|
7
|
+
fn enable_typed_flags() {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn k_nucleotide_uses_direct_map_has_get_set() {
|
|
12
|
+
enable_typed_flags();
|
|
13
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
14
|
+
let path = manifest
|
|
15
|
+
.join("../../tests/perf/k_nucleotide.tish")
|
|
16
|
+
.canonicalize()
|
|
17
|
+
.unwrap();
|
|
18
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
19
|
+
assert!(
|
|
20
|
+
rust.contains("tish_map_has("),
|
|
21
|
+
"expected direct map_has dispatch:\n{}",
|
|
22
|
+
rust.lines()
|
|
23
|
+
.filter(|l| l.contains("map_has") || l.contains("map_set"))
|
|
24
|
+
.take(6)
|
|
25
|
+
.collect::<Vec<_>>()
|
|
26
|
+
.join("\n")
|
|
27
|
+
);
|
|
28
|
+
assert!(
|
|
29
|
+
rust.contains("tish_map_get(") && rust.contains("tish_map_set("),
|
|
30
|
+
"expected direct map_get/set dispatch"
|
|
31
|
+
);
|
|
32
|
+
assert!(
|
|
33
|
+
!rust.contains("get_prop(&(m).clone(), \"has\")"),
|
|
34
|
+
"should not use bound-method has on map local m"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
//! #320 — honest native-array lowering for fn-call-built numeric arrays.
|
|
2
|
+
//!
|
|
3
|
+
//! Two sound, name-independent inference fixes:
|
|
4
|
+
//! fix1: a 0-param numeric fn (`nextBase`, mutates a numeric global, returns number) is eligible
|
|
5
|
+
//! for native `-> f64` promotion (`collect_native_fns` no longer requires ≥1 param).
|
|
6
|
+
//! fix2: a fn PROVEN to always return a number lets `infer_expr_type(f(...))` be `number`, so
|
|
7
|
+
//! `seq.push(nextBase())` keeps `seq` a native `Vec<f64>` instead of a boxed `Value[]`.
|
|
8
|
+
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
|
|
11
|
+
use tishlang_compile::compile_project_full;
|
|
12
|
+
|
|
13
|
+
/// The general typed-native inference flags.
|
|
14
|
+
fn enable_typed_flags() {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Write `src` under the workspace `target/` (scratch lives there, per repo convention) and compile.
|
|
18
|
+
fn compile_src(stem: &str, src: &str) -> String {
|
|
19
|
+
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/perf_codegen_320");
|
|
20
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
21
|
+
let path = dir.join(format!("{stem}.tish"));
|
|
22
|
+
std::fs::write(&path, src).unwrap();
|
|
23
|
+
let path = path.canonicalize().unwrap();
|
|
24
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
25
|
+
rust
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NON_ESCAPING: &str = r#"
|
|
29
|
+
let seed = 7
|
|
30
|
+
function nextBase() {
|
|
31
|
+
seed = (seed * 3877 + 29573) % 139968
|
|
32
|
+
if (seed < 34992) { return 0 }
|
|
33
|
+
if (seed < 69984) { return 1 }
|
|
34
|
+
if (seed < 104976) { return 2 }
|
|
35
|
+
return 3
|
|
36
|
+
}
|
|
37
|
+
let seq = []
|
|
38
|
+
for (let i = 0; i < 100; i++) { seq.push(nextBase()) }
|
|
39
|
+
let sum = 0
|
|
40
|
+
for (let i = 0; i < seq.length; i++) { sum = sum + seq[i] }
|
|
41
|
+
console.log("R " + sum)
|
|
42
|
+
"#;
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn zero_param_numeric_fn_goes_native() {
|
|
46
|
+
enable_typed_flags();
|
|
47
|
+
let rust = compile_src("zero_param", NON_ESCAPING);
|
|
48
|
+
// fix1: the 0-param fn is promoted to a native `-> f64` form.
|
|
49
|
+
assert!(
|
|
50
|
+
rust.contains("fn nextBase_native() -> f64"),
|
|
51
|
+
"0-param numeric fn should get a native f64 form (fix1)"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn numeric_returning_fn_keeps_pushed_array_native() {
|
|
57
|
+
enable_typed_flags();
|
|
58
|
+
let rust = compile_src("native_push", NON_ESCAPING);
|
|
59
|
+
// fix2: `seq.push(nextBase())` keeps `seq` a native `Vec<f64>` (the pushed value infers numeric).
|
|
60
|
+
assert!(
|
|
61
|
+
rust.contains("let mut seq: Vec<f64>"),
|
|
62
|
+
"seq built by push(numericFn()) should be a native Vec<f64> (fix2)"
|
|
63
|
+
);
|
|
64
|
+
// And the push routes straight to the native fn (no boxed Value array push).
|
|
65
|
+
assert!(
|
|
66
|
+
rust.contains("seq.push(nextBase_native())"),
|
|
67
|
+
"push should call the native fn into the native Vec (fix1+fix2)"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A fn that can fall through to an implicit `undefined` (no trailing unconditional return) must NOT
|
|
72
|
+
/// be treated as number-returning — else `a.push(maybe())` could put `undefined` in a `Vec<f64>`.
|
|
73
|
+
#[test]
|
|
74
|
+
fn fallthrough_fn_is_not_number_returning() {
|
|
75
|
+
enable_typed_flags();
|
|
76
|
+
let src = r#"
|
|
77
|
+
let g = 0
|
|
78
|
+
function maybe() {
|
|
79
|
+
g = g + 1
|
|
80
|
+
if (g < 10) { return 1 }
|
|
81
|
+
}
|
|
82
|
+
let a = []
|
|
83
|
+
for (let i = 0; i < 100; i++) { a.push(maybe()) }
|
|
84
|
+
let sum = 0
|
|
85
|
+
for (let i = 0; i < a.length; i++) { sum = sum + 1 }
|
|
86
|
+
console.log("R " + sum)
|
|
87
|
+
"#;
|
|
88
|
+
let rust = compile_src("fallthrough", src);
|
|
89
|
+
// Unsound to type `a` as Vec<f64> here — it must stay a boxed array.
|
|
90
|
+
assert!(
|
|
91
|
+
!rust.contains("let mut a: Vec<f64>"),
|
|
92
|
+
"fall-through fn must not make the pushed array native (soundness)"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// #320 part 2: a read-only `number[]` param of a normal (boxed-body, boxed-return) fn is unboxed
|
|
97
|
+
/// once into an owned native `Vec<f64>`, so the body indexes it natively. This is the
|
|
98
|
+
/// cross-fn-boundary lever — `kNucleotide(seq, k)` indexes `seq` but builds & returns a boxed Map,
|
|
99
|
+
/// so it is NOT a native-vec fn.
|
|
100
|
+
fn enable_arr_param() {}
|
|
101
|
+
|
|
102
|
+
const ARR_PARAM: &str = r#"
|
|
103
|
+
let seed = 7
|
|
104
|
+
function nextBase() {
|
|
105
|
+
seed = (seed * 3877 + 29573) % 139968
|
|
106
|
+
if (seed < 34992) { return 0 }
|
|
107
|
+
if (seed < 69984) { return 1 }
|
|
108
|
+
if (seed < 104976) { return 2 }
|
|
109
|
+
return 3
|
|
110
|
+
}
|
|
111
|
+
function count(seq, k) {
|
|
112
|
+
let m = new Map()
|
|
113
|
+
let n = seq.length - k + 1
|
|
114
|
+
for (let i = 0; i < n; i++) {
|
|
115
|
+
let key = 0
|
|
116
|
+
for (let j = 0; j < k; j++) { key = key * 4 + seq[i + j] }
|
|
117
|
+
if (m.has(key)) { m.set(key, m.get(key) + 1) } else { m.set(key, 1) }
|
|
118
|
+
}
|
|
119
|
+
return m
|
|
120
|
+
}
|
|
121
|
+
let seq = []
|
|
122
|
+
for (let i = 0; i < 100; i++) { seq.push(nextBase()) }
|
|
123
|
+
let m = count(seq, 4)
|
|
124
|
+
let sum = 0
|
|
125
|
+
for (let v of m.values()) { sum = sum + v }
|
|
126
|
+
console.log("R " + sum)
|
|
127
|
+
"#;
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn readonly_arr_param_unboxed_to_native_vec() {
|
|
131
|
+
enable_arr_param();
|
|
132
|
+
let rust = compile_src("arr_param", ARR_PARAM);
|
|
133
|
+
// The `seq` param is unboxed once into an owned native Vec<f64> at the closure entry (a packed
|
|
134
|
+
// NumberArray clones its backing; a boxed Array is mapped element-wise).
|
|
135
|
+
assert!(
|
|
136
|
+
rust.contains("let mut seq: Vec<f64> = match args.get(")
|
|
137
|
+
&& rust.contains("Some(Value::NumberArray(a)) => a.borrow().clone()"),
|
|
138
|
+
"read-only number[] param should be unboxed to a native Vec<f64>"
|
|
139
|
+
);
|
|
140
|
+
// And the hot inner loop indexes it natively (f64 mul + native vec read), no boxed get_index.
|
|
141
|
+
assert!(
|
|
142
|
+
rust.contains("key * 4_f64") && rust.contains("seq.get("),
|
|
143
|
+
"inner loop should be native f64 arithmetic over a native Vec read"
|
|
144
|
+
);
|
|
145
|
+
assert!(
|
|
146
|
+
!rust.contains("tishlang_runtime::get_index(&seq")
|
|
147
|
+
&& !rust.contains("get_index(&(seq)"),
|
|
148
|
+
"seq must not be read via the boxed runtime get_index inside the fn"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn mutating_arr_param_stays_boxed() {
|
|
154
|
+
enable_arr_param();
|
|
155
|
+
// `a` is index-ASSIGNED → an owned native copy would silently drop the write, so the param must
|
|
156
|
+
// stay boxed (classify_vec_param reports is_mut, and the detection requires read-only).
|
|
157
|
+
let src = r#"
|
|
158
|
+
function bump(a) { a[0] = a[0] + 1; return a.length }
|
|
159
|
+
let arr = []
|
|
160
|
+
for (let i = 0; i < 10; i++) { arr.push(i) }
|
|
161
|
+
let r = bump(arr)
|
|
162
|
+
console.log("R " + r)
|
|
163
|
+
"#;
|
|
164
|
+
let rust = compile_src("mut_arr_param", src);
|
|
165
|
+
assert!(
|
|
166
|
+
!rust.contains("let mut a: Vec<f64>"),
|
|
167
|
+
"a mutated array param must stay boxed (soundness — owned copy would lose writes)"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn escaping_arr_param_stays_boxed() {
|
|
173
|
+
enable_arr_param();
|
|
174
|
+
// `seq.push` MUTATES the param via a method (not index-assign): an owned copy would lose the
|
|
175
|
+
// caller-visible push. `scan_param_use` flags this as an escape → the param stays boxed.
|
|
176
|
+
let src = r#"
|
|
177
|
+
function build(seq) {
|
|
178
|
+
let m = new Map()
|
|
179
|
+
for (let i = 0; i < seq.length; i++) { m.set(i, seq[i]) }
|
|
180
|
+
seq.push(99)
|
|
181
|
+
return m
|
|
182
|
+
}
|
|
183
|
+
let arr = []
|
|
184
|
+
for (let i = 0; i < 10; i++) { arr.push(i) }
|
|
185
|
+
let m = build(arr)
|
|
186
|
+
console.log("R " + arr.length)
|
|
187
|
+
"#;
|
|
188
|
+
let rust = compile_src("escape_arr_param", src);
|
|
189
|
+
assert!(
|
|
190
|
+
!rust.contains("let mut seq: Vec<f64>"),
|
|
191
|
+
"an escaping (pushed) array param must stay boxed (soundness)"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn non_number_array_arg_keeps_callee_boxed() {
|
|
197
|
+
enable_arr_param();
|
|
198
|
+
// `joinup` reads its param read-only, but the ONLY caller passes a NON-number[] array. Unboxing
|
|
199
|
+
// to Vec<f64> would turn "x" into NaN (vs JS string concat) — so the call-site check keeps the
|
|
200
|
+
// callee fully boxed.
|
|
201
|
+
let src = r#"
|
|
202
|
+
function joinup(seq) { let r = ""; for (let i = 0; i < seq.length; i++) { r = r + seq[i] } return r }
|
|
203
|
+
let arr = [1, "x", 3]
|
|
204
|
+
console.log(joinup(arr))
|
|
205
|
+
"#;
|
|
206
|
+
let rust = compile_src("nonnum_arr_arg", src);
|
|
207
|
+
assert!(
|
|
208
|
+
!rust.contains("let mut seq: Vec<f64>"),
|
|
209
|
+
"a read-only fn whose caller passes a non-number[] array must stay boxed (call-site soundness)"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//! Hoisted module-level f64 array literals (`const G_XS`) must compile when used in for-of / join.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
use tishlang_compile::compile_project_full;
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn typed_array_forof_compiles() {
|
|
9
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
10
|
+
let path = manifest
|
|
11
|
+
.join("../../tests/core/typed_array_forof.tish")
|
|
12
|
+
.canonicalize()
|
|
13
|
+
.unwrap();
|
|
14
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
15
|
+
assert!(rust.contains("const G_XS:"), "expected hoisted xs array");
|
|
16
|
+
assert!(
|
|
17
|
+
!rust.contains("normalize_for_of((xs)"),
|
|
18
|
+
"for-of should use native index loop over G_XS, not boxed xs reference"
|
|
19
|
+
);
|
|
20
|
+
assert!(
|
|
21
|
+
rust.contains("for _fof_i0 in 0..G_XS.len()"),
|
|
22
|
+
"expected native for-of over hoisted G_XS"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn template_literals_hoisted_nums_compiles() {
|
|
28
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
29
|
+
let path = manifest
|
|
30
|
+
.join("../../tests/core/template_literals.tish")
|
|
31
|
+
.canonicalize()
|
|
32
|
+
.unwrap();
|
|
33
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
34
|
+
assert!(rust.contains("const G_NUMS:"));
|
|
35
|
+
assert!(
|
|
36
|
+
rust.contains("array_join(&Value::NumberArray(VmRef::new(G_NUMS"),
|
|
37
|
+
"nums.join should box hoisted G_NUMS, not reference missing local nums"
|
|
38
|
+
);
|
|
39
|
+
assert!(!rust.contains("&nums,"));
|
|
40
|
+
}
|
|
@@ -376,6 +376,19 @@ impl Codegen {
|
|
|
376
376
|
self.writeln(&format!("for (const {} of {})", escaped, it));
|
|
377
377
|
self.emit_js_control_body(body)?;
|
|
378
378
|
}
|
|
379
|
+
Statement::ForIn {
|
|
380
|
+
name,
|
|
381
|
+
object,
|
|
382
|
+
body,
|
|
383
|
+
..
|
|
384
|
+
} => {
|
|
385
|
+
// Native JS `for-in` — iterates own enumerable keys (#413). tish objects have no
|
|
386
|
+
// prototype chain, so this matches the key-enumeration the other backends perform.
|
|
387
|
+
let escaped = Self::escape_ident(name.as_ref());
|
|
388
|
+
let obj = self.emit_expr(object)?;
|
|
389
|
+
self.writeln(&format!("for (const {} in {})", escaped, obj));
|
|
390
|
+
self.emit_js_control_body(body)?;
|
|
391
|
+
}
|
|
379
392
|
Statement::Return { value, .. } => {
|
|
380
393
|
if let Some(v) = value {
|
|
381
394
|
let e = self.emit_expr(v)?;
|
|
@@ -677,6 +690,14 @@ impl Codegen {
|
|
|
677
690
|
}
|
|
678
691
|
Expr::Call { callee, args, .. } => {
|
|
679
692
|
let c = self.emit_expr(callee)?;
|
|
693
|
+
// An arrow-function callee (an IIFE — `(function(x){…})(n)` lowers to an arrow, as does
|
|
694
|
+
// `((x)=>…)(n)`) must be parenthesized: without it the call `(args)` binds to the arrow
|
|
695
|
+
// BODY, not the whole arrow — a `SyntaxError` for a block body, wrong result otherwise.
|
|
696
|
+
let c = if matches!(&**callee, Expr::ArrowFunction { .. }) {
|
|
697
|
+
format!("({})", c)
|
|
698
|
+
} else {
|
|
699
|
+
c
|
|
700
|
+
};
|
|
680
701
|
let arg_strs: Result<Vec<_>, _> =
|
|
681
702
|
args.iter().map(|a| self.emit_call_arg(a)).collect();
|
|
682
703
|
let arg_strs = arg_strs?.join(", ");
|
|
@@ -889,7 +910,9 @@ impl Codegen {
|
|
|
889
910
|
let val = self.emit_expr(value)?;
|
|
890
911
|
format!("({}[{}] = {})", obj, idx, val)
|
|
891
912
|
}
|
|
892
|
-
Expr::ArrowFunction {
|
|
913
|
+
Expr::ArrowFunction {
|
|
914
|
+
async_, params, body, ..
|
|
915
|
+
} => {
|
|
893
916
|
let ps = self.emit_params(params, None)?;
|
|
894
917
|
let body_str = match body {
|
|
895
918
|
ArrowBody::Expr(e) => self.emit_expr(e)?,
|
|
@@ -905,10 +928,11 @@ impl Codegen {
|
|
|
905
928
|
block
|
|
906
929
|
}
|
|
907
930
|
};
|
|
931
|
+
let prefix = if *async_ { "async " } else { "" }; // #428
|
|
908
932
|
if matches!(body, ArrowBody::Expr(_)) {
|
|
909
|
-
format!("({}) => ({})", ps, body_str)
|
|
933
|
+
format!("{}({}) => ({})", prefix, ps, body_str)
|
|
910
934
|
} else {
|
|
911
|
-
format!("({}) => {}", ps, body_str)
|
|
935
|
+
format!("{}({}) => {}", prefix, ps, body_str)
|
|
912
936
|
}
|
|
913
937
|
}
|
|
914
938
|
Expr::TemplateLiteral { quasis, exprs, .. } => {
|
|
@@ -1036,6 +1060,52 @@ impl Codegen {
|
|
|
1036
1060
|
let v = self.emit_expr(e)?;
|
|
1037
1061
|
self.writeln(&format!("export default {};", v));
|
|
1038
1062
|
}
|
|
1063
|
+
// #305: re-export in ESM mode — emit the native `export { … } from` / `export * from`,
|
|
1064
|
+
// rewriting the `.tish` specifier to the emitted `.js` module path.
|
|
1065
|
+
ExportDeclaration::ReExport {
|
|
1066
|
+
specifiers,
|
|
1067
|
+
all,
|
|
1068
|
+
from,
|
|
1069
|
+
..
|
|
1070
|
+
} => {
|
|
1071
|
+
let mut parts: Vec<String> = Vec::new();
|
|
1072
|
+
for s in specifiers {
|
|
1073
|
+
if let ImportSpecifier::Named { name, alias, .. } = s {
|
|
1074
|
+
match alias {
|
|
1075
|
+
Some(a) => parts.push(format!(
|
|
1076
|
+
"{} as {}",
|
|
1077
|
+
Self::escape_ident(name.as_ref()),
|
|
1078
|
+
Self::escape_ident(a.as_ref())
|
|
1079
|
+
)),
|
|
1080
|
+
None => parts.push(Self::escape_ident(name.as_ref()).to_string()),
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
match from {
|
|
1085
|
+
// Re-export from another module: `export … from "…"`.
|
|
1086
|
+
Some(from) => {
|
|
1087
|
+
let spec = rewrite_import_to_js(
|
|
1088
|
+
from.as_ref(),
|
|
1089
|
+
&self.module_path,
|
|
1090
|
+
&self.project_root,
|
|
1091
|
+
self.import_rewrite,
|
|
1092
|
+
)?;
|
|
1093
|
+
if *all {
|
|
1094
|
+
self.writeln(&format!("export * from {:?};", spec));
|
|
1095
|
+
} else {
|
|
1096
|
+
self.writeln(&format!(
|
|
1097
|
+
"export {{ {} }} from {:?};",
|
|
1098
|
+
parts.join(", "),
|
|
1099
|
+
spec
|
|
1100
|
+
));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// #415 local named export — no `from` clause.
|
|
1104
|
+
None => {
|
|
1105
|
+
self.writeln(&format!("export {{ {} }};", parts.join(", ")));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1039
1109
|
}
|
|
1040
1110
|
Ok(())
|
|
1041
1111
|
}
|
|
@@ -1182,6 +1252,7 @@ fn compile_project_js_inner(
|
|
|
1182
1252
|
merged.program.clone()
|
|
1183
1253
|
};
|
|
1184
1254
|
let stmt_sources = merged.statement_sources;
|
|
1255
|
+
let entry_exports = merged.entry_exports; // #295: entry module's named exports for the bundle
|
|
1185
1256
|
let default_export = program.statements.iter().find_map(|s| {
|
|
1186
1257
|
if let Statement::VarDecl { name, .. } = s {
|
|
1187
1258
|
let n = name.as_ref();
|
|
@@ -1221,6 +1292,26 @@ fn compile_project_js_inner(
|
|
|
1221
1292
|
gen.emit_program(&program, None, None)?;
|
|
1222
1293
|
}
|
|
1223
1294
|
let mut js = gen.output;
|
|
1295
|
+
// #295: re-emit the entry module's named exports as a real ES `export { … }` so the bundle is a
|
|
1296
|
+
// valid ES module (previously only `export default` was emitted; named `export fn/let/{…}` were
|
|
1297
|
+
// silently dropped, forcing downstream regex post-processing).
|
|
1298
|
+
if !entry_exports.is_empty() {
|
|
1299
|
+
let parts: Vec<String> = entry_exports
|
|
1300
|
+
.iter()
|
|
1301
|
+
.map(|(local, exported)| {
|
|
1302
|
+
if local == exported {
|
|
1303
|
+
Codegen::escape_ident(local)
|
|
1304
|
+
} else {
|
|
1305
|
+
format!(
|
|
1306
|
+
"{} as {}",
|
|
1307
|
+
Codegen::escape_ident(local),
|
|
1308
|
+
Codegen::escape_ident(exported)
|
|
1309
|
+
)
|
|
1310
|
+
}
|
|
1311
|
+
})
|
|
1312
|
+
.collect();
|
|
1313
|
+
js.push_str(&format!("\nexport {{ {} }};\n", parts.join(", ")));
|
|
1314
|
+
}
|
|
1224
1315
|
if let Some(name) = default_export {
|
|
1225
1316
|
js.push_str(&format!("\nexport default {};\n", name));
|
|
1226
1317
|
}
|
|
@@ -167,6 +167,33 @@ mod tests {
|
|
|
167
167
|
let _ = std::fs::remove_file(&path);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/// #295: `--target js --format bundle` must re-emit the entry module's named exports as a real ES
|
|
171
|
+
/// `export { … }` (previously only `export default` surfaced; `export fn/const/{…}` were silently
|
|
172
|
+
/// dropped, forcing downstream regex post-processing of every bundle).
|
|
173
|
+
#[test]
|
|
174
|
+
fn bundle_emits_named_exports() {
|
|
175
|
+
// `tempfile::tempdir()` (RAII, unique dir) rather than a fixed `std::env::temp_dir()` path.
|
|
176
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
177
|
+
let path = dir.path().join("entry.tish");
|
|
178
|
+
let src = "export fn mountFoo(h) { return h }\n\
|
|
179
|
+
export const VERSION = \"1\"\n\
|
|
180
|
+
fn helper() { return 2 }\n\
|
|
181
|
+
export { helper as helperExported }\n\
|
|
182
|
+
export default 42\n";
|
|
183
|
+
std::fs::write(&path, src).unwrap();
|
|
184
|
+
let js = compile_project_with_jsx(&path, Some(dir.path()), false)
|
|
185
|
+
.expect("compile_project_with_jsx failed");
|
|
186
|
+
assert!(
|
|
187
|
+
js.contains("export { VERSION, helper as helperExported, mountFoo };"),
|
|
188
|
+
"bundle must emit named exports, got:\n{}",
|
|
189
|
+
&js[js.len().saturating_sub(300)..]
|
|
190
|
+
);
|
|
191
|
+
assert!(
|
|
192
|
+
js.contains("export default __default_0;"),
|
|
193
|
+
"bundle must still emit the default export"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
170
197
|
#[test]
|
|
171
198
|
fn jsx_never_emits_vdom_helpers_or_prelude_flags() {
|
|
172
199
|
let src = r#"fn X() { return <div class="x">{"a"}</div> }"#;
|
|
@@ -98,6 +98,22 @@ fn resolve_import_to_key(
|
|
|
98
98
|
))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/// The module specifier a statement imports/re-exports from, if any — covers both
|
|
102
|
+
/// `import … from "x"` and `export … from "x"` (#305 re-export). Dependency discovery and cycle
|
|
103
|
+
/// detection use this so they follow re-export edges, not just plain imports (otherwise a module
|
|
104
|
+
/// reached only via a re-export would never be loaded).
|
|
105
|
+
fn stmt_module_source(stmt: &Statement) -> Option<&str> {
|
|
106
|
+
match stmt {
|
|
107
|
+
Statement::Import { from, .. } => Some(&**from),
|
|
108
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
109
|
+
// A local named export (`export { a }`, from=None) has no module dependency.
|
|
110
|
+
ExportDeclaration::ReExport { from, .. } => from.as_deref(),
|
|
111
|
+
_ => None,
|
|
112
|
+
},
|
|
113
|
+
_ => None,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
101
117
|
/// Resolve all modules starting from the entry file. Returns modules in dependency order.
|
|
102
118
|
pub fn resolve_virtual(
|
|
103
119
|
entry_path: &str,
|
|
@@ -163,7 +179,7 @@ fn load_module_recursive(
|
|
|
163
179
|
|
|
164
180
|
let from_dir = parent_dir(module_path);
|
|
165
181
|
for stmt in &program.statements {
|
|
166
|
-
if let
|
|
182
|
+
if let Some(from) = stmt_module_source(stmt) {
|
|
167
183
|
if is_native_import(from) {
|
|
168
184
|
continue;
|
|
169
185
|
}
|
|
@@ -217,7 +233,7 @@ fn has_cycle_from(
|
|
|
217
233
|
visiting: &mut HashSet<usize>,
|
|
218
234
|
) -> Result<bool, String> {
|
|
219
235
|
for stmt in &program.statements {
|
|
220
|
-
if let
|
|
236
|
+
if let Some(from) = stmt_module_source(stmt) {
|
|
221
237
|
if is_native_import(from) {
|
|
222
238
|
continue;
|
|
223
239
|
}
|
|
@@ -292,6 +308,54 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
292
308
|
let default_name = format!("__default_{}", idx);
|
|
293
309
|
module_exports[idx].insert("default".to_string(), default_name);
|
|
294
310
|
}
|
|
311
|
+
// #305: re-export — map each re-exported name to the DEP's binding (the dep is
|
|
312
|
+
// earlier in load order, so its export table is already built). `export *` copies
|
|
313
|
+
// every dep export without overriding an explicit one; `export { a as b }` maps
|
|
314
|
+
// b -> dep's binding for a. Mirrors `merge_modules` in tish_compile/src/resolve.rs.
|
|
315
|
+
ExportDeclaration::ReExport {
|
|
316
|
+
specifiers,
|
|
317
|
+
all,
|
|
318
|
+
from: Some(from),
|
|
319
|
+
..
|
|
320
|
+
} => {
|
|
321
|
+
let dir = parent_dir(&module.path);
|
|
322
|
+
let dep = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)
|
|
323
|
+
.ok()
|
|
324
|
+
.and_then(|key| path_to_idx.get(&key).copied())
|
|
325
|
+
.map(|dep_idx| module_exports[dep_idx].clone());
|
|
326
|
+
if let Some(dep) = dep {
|
|
327
|
+
if *all {
|
|
328
|
+
for (k, v) in &dep {
|
|
329
|
+
module_exports[idx]
|
|
330
|
+
.entry(k.clone())
|
|
331
|
+
.or_insert_with(|| v.clone());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for spec in specifiers {
|
|
335
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
336
|
+
if let Some(binding) = dep.get(name.as_ref()) {
|
|
337
|
+
let export_name =
|
|
338
|
+
alias.as_deref().unwrap_or(name.as_ref()).to_string();
|
|
339
|
+
module_exports[idx].insert(export_name, binding.clone());
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// #415 local named export (no `from`): map each export name to the local binding.
|
|
346
|
+
ExportDeclaration::ReExport {
|
|
347
|
+
specifiers,
|
|
348
|
+
from: None,
|
|
349
|
+
..
|
|
350
|
+
} => {
|
|
351
|
+
for spec in specifiers {
|
|
352
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
353
|
+
let export_name =
|
|
354
|
+
alias.as_deref().unwrap_or(name.as_ref()).to_string();
|
|
355
|
+
module_exports[idx].insert(export_name, name.to_string());
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
295
359
|
}
|
|
296
360
|
}
|
|
297
361
|
}
|
|
@@ -440,6 +504,10 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
440
504
|
span: espan,
|
|
441
505
|
});
|
|
442
506
|
}
|
|
507
|
+
// #305: re-export emits no code — `module_exports` already maps the re-exported
|
|
508
|
+
// names to the dep's bindings (in scope in the flattened bundle), so downstream
|
|
509
|
+
// imports resolve directly. Nothing to push here.
|
|
510
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
443
511
|
},
|
|
444
512
|
_ => statements.push(stmt.clone()),
|
|
445
513
|
}
|
|
@@ -471,4 +539,49 @@ mod tests {
|
|
|
471
539
|
let program = merge_modules_virtual(modules).unwrap();
|
|
472
540
|
assert!(!program.statements.is_empty());
|
|
473
541
|
}
|
|
542
|
+
|
|
543
|
+
#[test]
|
|
544
|
+
fn test_resolve_virtual_reexport_alias() {
|
|
545
|
+
// #305: `export { add as plus } from "./dep"` re-exports dep's `add` as `plus`; a downstream
|
|
546
|
+
// `import { plus }` must resolve to dep's binding. Regression guard: the merge previously did
|
|
547
|
+
// not handle `ExportDeclaration::ReExport` (E0004 build failure), and discovery did not follow
|
|
548
|
+
// the re-export edge (so a dep reached only via re-export was never loaded).
|
|
549
|
+
let mut files = HashMap::new();
|
|
550
|
+
files.insert(
|
|
551
|
+
"dep.tish".to_string(),
|
|
552
|
+
"export fn add(a, b) { return a + b }".to_string(),
|
|
553
|
+
);
|
|
554
|
+
files.insert(
|
|
555
|
+
"mid.tish".to_string(),
|
|
556
|
+
"export { add as plus } from \"./dep.tish\"".to_string(),
|
|
557
|
+
);
|
|
558
|
+
files.insert(
|
|
559
|
+
"main.tish".to_string(),
|
|
560
|
+
"import { plus } from \"./mid.tish\"\nconsole.log(plus(1, 2))".to_string(),
|
|
561
|
+
);
|
|
562
|
+
let modules = resolve_virtual("main.tish", &files).unwrap();
|
|
563
|
+
// dep.tish is reached ONLY via mid's re-export → discovery must have loaded it.
|
|
564
|
+
assert!(
|
|
565
|
+
modules.iter().any(|m| m.path == "dep.tish"),
|
|
566
|
+
"re-exported dep should be discovered and loaded"
|
|
567
|
+
);
|
|
568
|
+
detect_cycles_virtual(&modules).unwrap();
|
|
569
|
+
let program = merge_modules_virtual(modules).unwrap();
|
|
570
|
+
let has_add_fn = program
|
|
571
|
+
.statements
|
|
572
|
+
.iter()
|
|
573
|
+
.any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "add"));
|
|
574
|
+
let binds_plus_to_add = program.statements.iter().any(|s| {
|
|
575
|
+
matches!(
|
|
576
|
+
s,
|
|
577
|
+
Statement::VarDecl { name, init: Some(Expr::Ident { name: src, .. }), .. }
|
|
578
|
+
if name.as_ref() == "plus" && src.as_ref() == "add"
|
|
579
|
+
)
|
|
580
|
+
});
|
|
581
|
+
assert!(has_add_fn, "dep's `add` fn should be in the merged bundle");
|
|
582
|
+
assert!(
|
|
583
|
+
binds_plus_to_add,
|
|
584
|
+
"import of re-exported `plus` should bind to dep's `add`"
|
|
585
|
+
);
|
|
586
|
+
}
|
|
474
587
|
}
|
|
@@ -25,6 +25,10 @@ smallvec = "1"
|
|
|
25
25
|
indexmap = "2"
|
|
26
26
|
# Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
|
|
27
27
|
itoa = "1"
|
|
28
|
+
# Fast shortest-float→decimal for the number→string general path (JSON/template/log). ryu writes the
|
|
29
|
+
# shortest round-trip digits straight to a buffer with none of `core::fmt`'s `{:e}` Formatter overhead
|
|
30
|
+
# (profiled as ~40% of JSON.stringify self-time on numeric payloads). Already in the workspace lock.
|
|
31
|
+
ryu = "1"
|
|
28
32
|
fancy-regex = { version = "0.17.0", optional = true }
|
|
29
33
|
# Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
|
|
30
34
|
# single atomic with no pthread syscall — profiled as a top cost on object/array access.
|