@tishlang/tish-lsp 2.16.13 → 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/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/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +77 -3
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +195 -16
- package/crates/tish_builtins/src/collections.rs +37 -2
- 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 +168 -15
- package/crates/tish_bytecode/src/compiler.rs +56 -4
- package/crates/tish_bytecode/src/opcode.rs +4 -2
- package/crates/tish_compile/src/codegen.rs +513 -44
- package/crates/tish_compile/src/infer.rs +26 -11
- package/crates/tish_compile/src/resolve.rs +87 -10
- package/crates/tish_compile_js/src/codegen.rs +82 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
- package/crates/tish_core/src/json.rs +5 -1
- package/crates/tish_core/src/lib.rs +48 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +6 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +655 -27
- package/crates/tish_eval/src/natives.rs +264 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_fmt/src/lib.rs +23 -4
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/src/lib.rs +41 -0
- package/crates/tish_parser/src/parser.rs +180 -11
- package/crates/tish_resolve/src/lib.rs +66 -5
- package/crates/tish_runtime/Cargo.toml +5 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/lib.rs +128 -9
- package/crates/tish_ui/src/jsx.rs +18 -3
- package/crates/tish_vm/src/jit.rs +1 -1
- package/crates/tish_vm/src/vm.rs +270 -60
- 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
|
@@ -1220,7 +1220,7 @@ fn check_returns_numeric(s: &Statement, ctx: &InferCtx, ok: &mut bool) {
|
|
|
1220
1220
|
check_returns_numeric(e, ctx, ok);
|
|
1221
1221
|
}
|
|
1222
1222
|
}
|
|
1223
|
-
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
1223
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1224
1224
|
check_returns_numeric(body, ctx, ok);
|
|
1225
1225
|
}
|
|
1226
1226
|
For { init, body, .. } => {
|
|
@@ -1319,7 +1319,12 @@ fn count_uses_stmt(s: &Statement, f: &str, calls: &mut usize, others: &mut usize
|
|
|
1319
1319
|
}
|
|
1320
1320
|
count_uses_stmt(body, f, calls, others);
|
|
1321
1321
|
}
|
|
1322
|
-
ForOf { iterable, body, .. }
|
|
1322
|
+
ForOf { iterable, body, .. }
|
|
1323
|
+
| ForIn {
|
|
1324
|
+
object: iterable,
|
|
1325
|
+
body,
|
|
1326
|
+
..
|
|
1327
|
+
} => {
|
|
1323
1328
|
count_uses_expr(iterable, f, calls, others);
|
|
1324
1329
|
count_uses_stmt(body, f, calls, others);
|
|
1325
1330
|
}
|
|
@@ -1522,7 +1527,7 @@ fn stmt_contains_fundecl(s: &Statement) -> bool {
|
|
|
1522
1527
|
stmt_contains_fundecl(then_branch)
|
|
1523
1528
|
|| else_branch.as_ref().is_some_and(|e| stmt_contains_fundecl(e))
|
|
1524
1529
|
}
|
|
1525
|
-
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } => {
|
|
1530
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1526
1531
|
stmt_contains_fundecl(body)
|
|
1527
1532
|
}
|
|
1528
1533
|
Try { body, catch_body, finally_body, .. } => {
|
|
@@ -1574,7 +1579,12 @@ fn walk_exprs_stmt_full(s: &Statement, f: &mut impl FnMut(&Expr)) {
|
|
|
1574
1579
|
}
|
|
1575
1580
|
walk_exprs_stmt_full(body, f);
|
|
1576
1581
|
}
|
|
1577
|
-
ForOf { iterable, body, .. }
|
|
1582
|
+
ForOf { iterable, body, .. }
|
|
1583
|
+
| ForIn {
|
|
1584
|
+
object: iterable,
|
|
1585
|
+
body,
|
|
1586
|
+
..
|
|
1587
|
+
} => {
|
|
1578
1588
|
walk_exprs_expr(iterable, f);
|
|
1579
1589
|
walk_exprs_stmt_full(body, f);
|
|
1580
1590
|
}
|
|
@@ -1639,7 +1649,7 @@ fn count_returns(s: &Statement) -> usize {
|
|
|
1639
1649
|
count_returns(then_branch)
|
|
1640
1650
|
+ else_branch.as_ref().map_or(0, |e| count_returns(e))
|
|
1641
1651
|
}
|
|
1642
|
-
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } => {
|
|
1652
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1643
1653
|
count_returns(body)
|
|
1644
1654
|
}
|
|
1645
1655
|
Try { body, catch_body, finally_body, .. } => {
|
|
@@ -2694,7 +2704,7 @@ fn seed_numeric_locals(s: &Statement, hyp: &mut InferCtx) {
|
|
|
2694
2704
|
}
|
|
2695
2705
|
seed_numeric_locals(body, hyp);
|
|
2696
2706
|
}
|
|
2697
|
-
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
2707
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
2698
2708
|
seed_numeric_locals(body, hyp)
|
|
2699
2709
|
}
|
|
2700
2710
|
_ => {}
|
|
@@ -3649,7 +3659,7 @@ fn collect_return_shapes(
|
|
|
3649
3659
|
}
|
|
3650
3660
|
}
|
|
3651
3661
|
}
|
|
3652
|
-
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
3662
|
+
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
3653
3663
|
if !collect_return_shapes(body, pctx, found, ok) {
|
|
3654
3664
|
return false;
|
|
3655
3665
|
}
|
|
@@ -3916,7 +3926,7 @@ fn collect_element_aliases(s: &Statement, p: &str, out: &mut HashSet<String>) {
|
|
|
3916
3926
|
}
|
|
3917
3927
|
collect_element_aliases(body, p, out);
|
|
3918
3928
|
}
|
|
3919
|
-
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
3929
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
3920
3930
|
collect_element_aliases(body, p, out)
|
|
3921
3931
|
}
|
|
3922
3932
|
_ => {}
|
|
@@ -4241,7 +4251,12 @@ fn walk_exprs_stmt(s: &Statement, f: &mut impl FnMut(&Expr)) {
|
|
|
4241
4251
|
}
|
|
4242
4252
|
walk_exprs_stmt(body, f);
|
|
4243
4253
|
}
|
|
4244
|
-
ForOf { iterable, body, .. }
|
|
4254
|
+
ForOf { iterable, body, .. }
|
|
4255
|
+
| ForIn {
|
|
4256
|
+
object: iterable,
|
|
4257
|
+
body,
|
|
4258
|
+
..
|
|
4259
|
+
} => {
|
|
4245
4260
|
walk_exprs_expr(iterable, f);
|
|
4246
4261
|
walk_exprs_stmt(body, f);
|
|
4247
4262
|
}
|
|
@@ -4354,7 +4369,7 @@ fn collect_body_struct_locals(
|
|
|
4354
4369
|
collect_body_struct_locals(e, analysis, out);
|
|
4355
4370
|
}
|
|
4356
4371
|
}
|
|
4357
|
-
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
4372
|
+
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
4358
4373
|
collect_body_struct_locals(body, analysis, out)
|
|
4359
4374
|
}
|
|
4360
4375
|
_ => {}
|
|
@@ -4388,7 +4403,7 @@ fn find_array_return(s: &Statement, locals: &HashMap<String, String>) -> Option<
|
|
|
4388
4403
|
}
|
|
4389
4404
|
If { then_branch, else_branch, .. } => find_array_return(then_branch, locals)
|
|
4390
4405
|
.or_else(|| else_branch.as_ref().and_then(|e| find_array_return(e, locals))),
|
|
4391
|
-
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
4406
|
+
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
4392
4407
|
find_array_return(body, locals)
|
|
4393
4408
|
}
|
|
4394
4409
|
_ => None,
|
|
@@ -49,17 +49,23 @@ pub struct NativeBuildArtifacts {
|
|
|
49
49
|
pub native_init: std::collections::HashMap<String, NativeModuleInit>,
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
|
|
52
|
+
/// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.). The `node:` prefix
|
|
53
|
+
/// (e.g. `node:fs`, `node:fs/promises`) is stripped before lookup.
|
|
53
54
|
const BUILTIN_ALIASES: &[(&str, &str)] = &[
|
|
54
55
|
("fs", "tish:fs"),
|
|
56
|
+
("fs/promises", "tish:fs/promises"),
|
|
55
57
|
("http", "tish:http"),
|
|
56
58
|
("timers", "tish:timers"),
|
|
57
59
|
("process", "tish:process"),
|
|
58
60
|
("ws", "tish:ws"),
|
|
61
|
+
("tty", "tish:tty"),
|
|
59
62
|
];
|
|
60
63
|
|
|
61
|
-
/// Normalize built-in spec to canonical form.
|
|
64
|
+
/// Normalize built-in spec to canonical form. Handles the Node `node:` prefix
|
|
65
|
+
/// (`node:fs` -> `tish:fs`, `node:fs/promises` -> `tish:fs/promises`) and bare aliases.
|
|
62
66
|
pub fn normalize_builtin_spec(spec: &str) -> Option<String> {
|
|
67
|
+
// Strip a leading `node:` so `node:fs` resolves the same as `fs`.
|
|
68
|
+
let spec = spec.strip_prefix("node:").unwrap_or(spec);
|
|
63
69
|
if spec.starts_with("tish:") {
|
|
64
70
|
return Some(spec.to_string());
|
|
65
71
|
}
|
|
@@ -71,10 +77,20 @@ pub fn normalize_builtin_spec(spec: &str) -> Option<String> {
|
|
|
71
77
|
|
|
72
78
|
/// Built-in modules that come from tishlang_runtime, not from package.json.
|
|
73
79
|
pub fn is_builtin_native_spec(spec: &str) -> bool {
|
|
80
|
+
let spec = spec.strip_prefix("node:").unwrap_or(spec);
|
|
74
81
|
matches!(
|
|
75
82
|
spec,
|
|
76
|
-
"tish:fs"
|
|
77
|
-
|
|
83
|
+
"tish:fs"
|
|
84
|
+
| "tish:fs/promises"
|
|
85
|
+
| "tish:http"
|
|
86
|
+
| "tish:timers"
|
|
87
|
+
| "tish:process"
|
|
88
|
+
| "tish:ws"
|
|
89
|
+
| "tish:tty"
|
|
90
|
+
) || matches!(
|
|
91
|
+
spec,
|
|
92
|
+
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty"
|
|
93
|
+
)
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
/// Resolve all native imports in a merged program via package.json lookup.
|
|
@@ -238,9 +254,12 @@ pub fn program_uses_document(program: &Program) -> bool {
|
|
|
238
254
|
|| update.as_ref().is_some_and(expr_uses_document)
|
|
239
255
|
|| stmt_uses_document(body)
|
|
240
256
|
}
|
|
241
|
-
Statement::ForOf { iterable, body, .. }
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
Statement::ForOf { iterable, body, .. }
|
|
258
|
+
| Statement::ForIn {
|
|
259
|
+
object: iterable,
|
|
260
|
+
body,
|
|
261
|
+
..
|
|
262
|
+
} => expr_uses_document(iterable) || stmt_uses_document(body),
|
|
244
263
|
Statement::Switch {
|
|
245
264
|
expr,
|
|
246
265
|
cases,
|
|
@@ -890,7 +909,8 @@ fn stmt_module_dep(stmt: &Statement) -> Option<&std::sync::Arc<str>> {
|
|
|
890
909
|
match stmt {
|
|
891
910
|
Statement::Import { from, .. } => Some(from),
|
|
892
911
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
893
|
-
|
|
912
|
+
// A local named export (`export { a }`, from=None) has no module dependency.
|
|
913
|
+
ExportDeclaration::ReExport { from, .. } => from.as_ref(),
|
|
894
914
|
_ => None,
|
|
895
915
|
},
|
|
896
916
|
_ => None,
|
|
@@ -951,10 +971,15 @@ fn load_module_recursive(
|
|
|
951
971
|
///
|
|
952
972
|
/// Scoped npm packages (`@scope/pkg`) are merged as Tish source unless imported via `tish:…`.
|
|
953
973
|
pub fn is_native_import(spec: &str) -> bool {
|
|
974
|
+
// A leading `node:` (node:fs, node:fs/promises) resolves the same as the bare form.
|
|
975
|
+
let spec = spec.strip_prefix("node:").unwrap_or(spec);
|
|
954
976
|
spec.starts_with("tish:")
|
|
955
977
|
|| spec.starts_with("cargo:")
|
|
956
978
|
|| spec.starts_with("ffi:")
|
|
957
|
-
|| matches!(
|
|
979
|
+
|| matches!(
|
|
980
|
+
spec,
|
|
981
|
+
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty"
|
|
982
|
+
)
|
|
958
983
|
}
|
|
959
984
|
|
|
960
985
|
/// True for `ffi:…` specs (portable C-ABI cdylib extensions, loadable on every backend). The
|
|
@@ -1160,6 +1185,11 @@ fn has_cycle_from(
|
|
|
1160
1185
|
pub struct MergedProgram {
|
|
1161
1186
|
pub program: Program,
|
|
1162
1187
|
pub statement_sources: Vec<PathBuf>,
|
|
1188
|
+
/// #295: the ENTRY module's exported `(local_name, exported_name)` pairs, excluding `default`
|
|
1189
|
+
/// (handled separately as `export default`). Merge unwraps exports into plain top-level
|
|
1190
|
+
/// declarations, so `--target js --format bundle` re-emits these as a trailing `export { … }` to
|
|
1191
|
+
/// produce a valid ES module. Sorted by local name for deterministic output.
|
|
1192
|
+
pub entry_exports: Vec<(String, String)>,
|
|
1163
1193
|
}
|
|
1164
1194
|
|
|
1165
1195
|
fn merge_push(
|
|
@@ -1465,6 +1495,12 @@ pub(crate) fn rewrite_stmt_scope(
|
|
|
1465
1495
|
iterable,
|
|
1466
1496
|
body,
|
|
1467
1497
|
..
|
|
1498
|
+
}
|
|
1499
|
+
| Statement::ForIn {
|
|
1500
|
+
name,
|
|
1501
|
+
object: iterable,
|
|
1502
|
+
body,
|
|
1503
|
+
..
|
|
1468
1504
|
} => {
|
|
1469
1505
|
rewrite_expr_scope(iterable, active);
|
|
1470
1506
|
let mut child = active.clone();
|
|
@@ -1696,6 +1732,14 @@ pub(crate) fn rewrite_expr_scope(expr: &mut Expr, active: &HashMap<String, Arc<s
|
|
|
1696
1732
|
/// Import statements are rewritten as bindings from already-emitted dep exports.
|
|
1697
1733
|
/// Export statements are unwrapped (the inner declaration is emitted).
|
|
1698
1734
|
pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram, String> {
|
|
1735
|
+
// #469: give the ENTRY module a Go/Rust/Swift-style `main` entry point. The entry is the last
|
|
1736
|
+
// module (post-order dependency load order), and this runs BEFORE private-binding isolation so the
|
|
1737
|
+
// synthetic `main()` call is renamed in lockstep with the `main` declaration if isolation renames
|
|
1738
|
+
// it. Every backend consumes the merged program, so this keeps interp/vm/native/js consistent.
|
|
1739
|
+
if let Some(entry) = modules.last_mut() {
|
|
1740
|
+
tishlang_ast::append_main_entry(&mut entry.program.statements);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1699
1743
|
// #97: isolate module-private top-level bindings before they are flattened together.
|
|
1700
1744
|
isolate_private_top_level_bindings(&mut modules);
|
|
1701
1745
|
|
|
@@ -1730,7 +1774,7 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1730
1774
|
ExportDeclaration::ReExport {
|
|
1731
1775
|
specifiers,
|
|
1732
1776
|
all,
|
|
1733
|
-
from,
|
|
1777
|
+
from: Some(from),
|
|
1734
1778
|
..
|
|
1735
1779
|
} => {
|
|
1736
1780
|
let dir = module.path.parent().unwrap_or_else(|| Path::new("."));
|
|
@@ -1758,6 +1802,22 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1758
1802
|
}
|
|
1759
1803
|
}
|
|
1760
1804
|
}
|
|
1805
|
+
// #415 local named export (`export { a, b as c }`, no `from`): each specifier
|
|
1806
|
+
// exports an already-declared LOCAL top-level binding, mapping export-name -> that
|
|
1807
|
+
// binding. (`isolate_private_top_level_bindings` keeps exported names un-renamed.)
|
|
1808
|
+
ExportDeclaration::ReExport {
|
|
1809
|
+
specifiers,
|
|
1810
|
+
from: None,
|
|
1811
|
+
..
|
|
1812
|
+
} => {
|
|
1813
|
+
for spec in specifiers {
|
|
1814
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
1815
|
+
let export_name =
|
|
1816
|
+
alias.as_deref().unwrap_or(name.as_ref()).to_string();
|
|
1817
|
+
module_exports[idx].insert(export_name, name.to_string());
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1761
1821
|
}
|
|
1762
1822
|
}
|
|
1763
1823
|
}
|
|
@@ -1956,9 +2016,26 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1956
2016
|
}
|
|
1957
2017
|
}
|
|
1958
2018
|
}
|
|
2019
|
+
// #295: capture the ENTRY module's exports (the entry is last — deps are emitted first). Merge
|
|
2020
|
+
// already built `module_exports[idx]` as exported_name -> local_binding; re-express as
|
|
2021
|
+
// (local, exported) for the JS bundle emitter, excluding `default` (emitted as `export default`).
|
|
2022
|
+
let entry_exports: Vec<(String, String)> = modules
|
|
2023
|
+
.len()
|
|
2024
|
+
.checked_sub(1)
|
|
2025
|
+
.map(|last| {
|
|
2026
|
+
let mut v: Vec<(String, String)> = module_exports[last]
|
|
2027
|
+
.iter()
|
|
2028
|
+
.filter(|(exported, _)| exported.as_str() != "default")
|
|
2029
|
+
.map(|(exported, local)| (local.clone(), exported.clone()))
|
|
2030
|
+
.collect();
|
|
2031
|
+
v.sort();
|
|
2032
|
+
v
|
|
2033
|
+
})
|
|
2034
|
+
.unwrap_or_default();
|
|
1959
2035
|
Ok(MergedProgram {
|
|
1960
2036
|
program: Program { statements },
|
|
1961
2037
|
statement_sources,
|
|
2038
|
+
entry_exports,
|
|
1962
2039
|
})
|
|
1963
2040
|
}
|
|
1964
2041
|
|
|
@@ -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, .. } => {
|
|
@@ -1044,29 +1068,42 @@ impl Codegen {
|
|
|
1044
1068
|
from,
|
|
1045
1069
|
..
|
|
1046
1070
|
} => {
|
|
1047
|
-
let
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
+
));
|
|
1067
1101
|
}
|
|
1068
1102
|
}
|
|
1069
|
-
|
|
1103
|
+
// #415 local named export — no `from` clause.
|
|
1104
|
+
None => {
|
|
1105
|
+
self.writeln(&format!("export {{ {} }};", parts.join(", ")));
|
|
1106
|
+
}
|
|
1070
1107
|
}
|
|
1071
1108
|
}
|
|
1072
1109
|
}
|
|
@@ -1215,6 +1252,7 @@ fn compile_project_js_inner(
|
|
|
1215
1252
|
merged.program.clone()
|
|
1216
1253
|
};
|
|
1217
1254
|
let stmt_sources = merged.statement_sources;
|
|
1255
|
+
let entry_exports = merged.entry_exports; // #295: entry module's named exports for the bundle
|
|
1218
1256
|
let default_export = program.statements.iter().find_map(|s| {
|
|
1219
1257
|
if let Statement::VarDecl { name, .. } = s {
|
|
1220
1258
|
let n = name.as_ref();
|
|
@@ -1254,6 +1292,26 @@ fn compile_project_js_inner(
|
|
|
1254
1292
|
gen.emit_program(&program, None, None)?;
|
|
1255
1293
|
}
|
|
1256
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
|
+
}
|
|
1257
1315
|
if let Some(name) = default_export {
|
|
1258
1316
|
js.push_str(&format!("\nexport default {};\n", name));
|
|
1259
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> }"#;
|
|
@@ -106,7 +106,8 @@ fn stmt_module_source(stmt: &Statement) -> Option<&str> {
|
|
|
106
106
|
match stmt {
|
|
107
107
|
Statement::Import { from, .. } => Some(&**from),
|
|
108
108
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
109
|
-
|
|
109
|
+
// A local named export (`export { a }`, from=None) has no module dependency.
|
|
110
|
+
ExportDeclaration::ReExport { from, .. } => from.as_deref(),
|
|
110
111
|
_ => None,
|
|
111
112
|
},
|
|
112
113
|
_ => None,
|
|
@@ -314,7 +315,7 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
314
315
|
ExportDeclaration::ReExport {
|
|
315
316
|
specifiers,
|
|
316
317
|
all,
|
|
317
|
-
from,
|
|
318
|
+
from: Some(from),
|
|
318
319
|
..
|
|
319
320
|
} => {
|
|
320
321
|
let dir = parent_dir(&module.path);
|
|
@@ -341,6 +342,20 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
341
342
|
}
|
|
342
343
|
}
|
|
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
|
+
}
|
|
344
359
|
}
|
|
345
360
|
}
|
|
346
361
|
}
|
|
@@ -224,7 +224,11 @@ fn json_stringify_into_guarded(buf: &mut String, value: &Value, ancestors: &mut
|
|
|
224
224
|
/// to `buf`. Optimised for the common case where the input is ASCII and
|
|
225
225
|
/// contains no characters that need escaping — we fast-pass the bytes
|
|
226
226
|
/// straight through, only falling into the per-char path on a hit.
|
|
227
|
-
|
|
227
|
+
/// Append `s` to `buf` as a JSON string body (WITHOUT the surrounding quotes), escaping `"`, `\`,
|
|
228
|
+
/// and every control char < 0x20 (`\n \r \t \b \f`, else `\u00xx`). The single source of truth for
|
|
229
|
+
/// JSON string escaping, shared by the vm/native path here and the interpreter's `JSON.stringify`
|
|
230
|
+
/// (which otherwise hand-rolled an escaper that missed `\b`/`\f`/`\u00xx` and emitted invalid JSON).
|
|
231
|
+
pub fn escape_json_string_into(buf: &mut String, s: &str) {
|
|
228
232
|
let bytes = s.as_bytes();
|
|
229
233
|
let mut start = 0usize;
|
|
230
234
|
for (i, &b) in bytes.iter().enumerate() {
|
|
@@ -12,9 +12,11 @@ mod value;
|
|
|
12
12
|
mod vmref;
|
|
13
13
|
|
|
14
14
|
pub use console_style::{format_value_styled, format_values_for_console, use_console_colors};
|
|
15
|
-
pub use json::{
|
|
15
|
+
pub use json::{
|
|
16
|
+
escape_json_string_into, json_parse, json_stringify, json_stringify_into, write_json_number,
|
|
17
|
+
};
|
|
16
18
|
pub use shape::{ShapeId, DICT_SHAPE, EMPTY_SHAPE};
|
|
17
|
-
pub use uri::{percent_decode, percent_encode};
|
|
19
|
+
pub use uri::{percent_decode, percent_decode_component, percent_encode, percent_encode_component};
|
|
18
20
|
pub use arcstr::ArcStr;
|
|
19
21
|
pub use value::*;
|
|
20
22
|
pub use vmref::{VmReadGuard, VmRef, VmWriteGuard};
|
|
@@ -171,6 +173,50 @@ pub fn not_a_function_error(message: impl Into<String>) -> Value {
|
|
|
171
173
|
Value::object(e)
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/// A catchable `TypeError` with an arbitrary message (`{ name: "TypeError", message }`), matching the
|
|
177
|
+
/// VM/interpreter/node shape. Used to PARK a pending throw from a shared builtin that returns a plain
|
|
178
|
+
/// `Value` and so cannot signal an error with `Result` (e.g. `[].reduce(fn)` with no initial value).
|
|
179
|
+
pub fn type_error(message: impl Into<String>) -> Value {
|
|
180
|
+
let mut e = ObjectMap::default();
|
|
181
|
+
e.insert(
|
|
182
|
+
std::sync::Arc::from("name"),
|
|
183
|
+
Value::String("TypeError".into()),
|
|
184
|
+
);
|
|
185
|
+
e.insert(
|
|
186
|
+
std::sync::Arc::from("message"),
|
|
187
|
+
Value::String(message.into().into()),
|
|
188
|
+
);
|
|
189
|
+
Value::object(e)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// A string key names a canonical array index iff it is a non-negative integer whose decimal form
|
|
193
|
+
/// round-trips and is below the array-index ceiling (`2³²−1`): `"0"`, `"12"` → `Some`; `"01"`, `"-1"`,
|
|
194
|
+
/// `"1.5"`, `"foo"`, `" 1"`, `""` → `None`. Shared so `arr["0"] === arr[0]` on every backend (#432).
|
|
195
|
+
pub fn str_to_array_index(s: &str) -> Option<usize> {
|
|
196
|
+
let i: usize = s.parse().ok()?;
|
|
197
|
+
if i < 4_294_967_295 && i.to_string() == s {
|
|
198
|
+
Some(i)
|
|
199
|
+
} else {
|
|
200
|
+
None
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// A catchable `RangeError` with an arbitrary message (`{ name: "RangeError", message }`), matching the
|
|
205
|
+
/// VM/interpreter/node shape. Used to PARK a pending throw from a shared builtin that returns a plain
|
|
206
|
+
/// `Value` (e.g. `[1,2].with(5, x)` with an out-of-range index).
|
|
207
|
+
pub fn range_error(message: impl Into<String>) -> Value {
|
|
208
|
+
let mut e = ObjectMap::default();
|
|
209
|
+
e.insert(
|
|
210
|
+
std::sync::Arc::from("name"),
|
|
211
|
+
Value::String("RangeError".into()),
|
|
212
|
+
);
|
|
213
|
+
e.insert(
|
|
214
|
+
std::sync::Arc::from("message"),
|
|
215
|
+
Value::String(message.into().into()),
|
|
216
|
+
);
|
|
217
|
+
Value::object(e)
|
|
218
|
+
}
|
|
219
|
+
|
|
174
220
|
/// A catchable `TypeError` for reading a property/index of the nullish value — the `{ name, message }`
|
|
175
221
|
/// shape matches the VM/interpreter/node (`e.name === "TypeError"`). Used to PARK a pending throw in
|
|
176
222
|
/// the native/runtime `get_prop`/`get_index` null arm (#425), so `null.length` / `null[0]` surface a
|
|
@@ -88,6 +88,51 @@ pub fn percent_encode(input: &str) -> String {
|
|
|
88
88
|
result
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/// Percent-DECODE for `decodeURIComponent` — decodes EVERY `%XX` (unlike `percent_decode`/`decodeURI`,
|
|
92
|
+
/// which preserves the reserved delimiters). Bytes are collected then UTF-8 decoded, so multi-byte
|
|
93
|
+
/// escapes (`%E2%82%AC` → `€`) round-trip. Errors on a malformed `%` sequence (JS `URIError`).
|
|
94
|
+
pub fn percent_decode_component(input: &str) -> Result<String, String> {
|
|
95
|
+
let bytes = input.as_bytes();
|
|
96
|
+
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
|
|
97
|
+
let mut i = 0;
|
|
98
|
+
while i < bytes.len() {
|
|
99
|
+
if bytes[i] == b'%' {
|
|
100
|
+
if i + 2 < bytes.len() {
|
|
101
|
+
if let Some(b) = std::str::from_utf8(&bytes[i + 1..i + 3])
|
|
102
|
+
.ok()
|
|
103
|
+
.and_then(|s| u8::from_str_radix(s, 16).ok())
|
|
104
|
+
{
|
|
105
|
+
out.push(b);
|
|
106
|
+
i += 3;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Err("URIError: malformed URI sequence".to_string());
|
|
111
|
+
}
|
|
112
|
+
out.push(bytes[i]);
|
|
113
|
+
i += 1;
|
|
114
|
+
}
|
|
115
|
+
String::from_utf8(out).map_err(|_| "URIError: malformed URI sequence".to_string())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Percent-encode for `encodeURIComponent` — escapes everything except the unreserved set
|
|
119
|
+
/// `A-Za-z0-9 - _ . ! ~ * ' ( )` (stricter than `encodeURI`, which also preserves the URI reserved
|
|
120
|
+
/// delimiters `; / ? : @ & = + $ , #`).
|
|
121
|
+
pub fn percent_encode_component(input: &str) -> String {
|
|
122
|
+
const UNRESERVED: &[char] = &['-', '_', '.', '!', '~', '*', '\'', '(', ')'];
|
|
123
|
+
let mut result = String::with_capacity(input.len());
|
|
124
|
+
for c in input.chars() {
|
|
125
|
+
if c.is_ascii_alphanumeric() || UNRESERVED.contains(&c) {
|
|
126
|
+
result.push(c);
|
|
127
|
+
} else {
|
|
128
|
+
for byte in c.to_string().as_bytes() {
|
|
129
|
+
result.push_str(&format!("%{:02X}", byte));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
result
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
#[cfg(test)]
|
|
92
137
|
mod tests {
|
|
93
138
|
use super::*;
|