@tishlang/tish 1.13.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +61 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -4,7 +4,11 @@
4
4
  use std::collections::{HashMap, HashSet};
5
5
  use std::path::{Path, PathBuf};
6
6
  use std::sync::Arc;
7
- use tishlang_ast::{ExportDeclaration, Expr, ImportSpecifier, MemberProp, Program, Statement, CallArg};
7
+ use tishlang_ast::{
8
+ ArrayElement, ArrowBody, CallArg, DestructElement, DestructPattern, ExportDeclaration, Expr,
9
+ FunParam, ImportSpecifier, JsxAttrValue, JsxChild, JsxProp, MemberProp, ObjectProp, Program,
10
+ Statement,
11
+ };
8
12
 
9
13
  /// Resolved native module: crate path and init expression.
10
14
  #[derive(Debug, Clone)]
@@ -69,8 +73,8 @@ pub fn normalize_builtin_spec(spec: &str) -> Option<String> {
69
73
  pub fn is_builtin_native_spec(spec: &str) -> bool {
70
74
  matches!(
71
75
  spec,
72
- "tish:fs" | "tish:http" | "tish:timers" | "tish:process" | "tish:ws"
73
- ) || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
76
+ "tish:fs" | "tish:http" | "tish:timers" | "tish:process" | "tish:ws" | "tish:tty"
77
+ ) || matches!(spec, "fs" | "http" | "timers" | "process" | "ws" | "tty")
74
78
  }
75
79
 
76
80
  /// Resolve all native imports in a merged program via package.json lookup.
@@ -128,6 +132,7 @@ pub fn program_uses_document(program: &Program) -> bool {
128
132
  Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } => {
129
133
  expr_uses_document(operand)
130
134
  }
135
+ Expr::Delete { target, .. } => expr_uses_document(target),
131
136
  Expr::Call { callee, args, .. } => {
132
137
  expr_uses_document(callee)
133
138
  || args.iter().any(|a| match a {
@@ -206,10 +211,10 @@ pub fn program_uses_document(program: &Program) -> bool {
206
211
 
207
212
  fn stmt_uses_document(s: &Statement) -> bool {
208
213
  match s {
209
- Statement::VarDecl { init, .. } => init.as_ref().is_some_and(|e| expr_uses_document(e)),
214
+ Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_uses_document),
210
215
  Statement::VarDeclDestructure { init, .. } => expr_uses_document(init),
211
216
  Statement::ExprStmt { expr, .. } => expr_uses_document(expr),
212
- Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_uses_document(e)),
217
+ Statement::Return { value, .. } => value.as_ref().is_some_and(expr_uses_document),
213
218
  Statement::Throw { value, .. } => expr_uses_document(value),
214
219
  Statement::If {
215
220
  cond,
@@ -229,8 +234,8 @@ pub fn program_uses_document(program: &Program) -> bool {
229
234
  }
230
235
  Statement::For { init, cond, update, body, .. } => {
231
236
  init.as_ref().is_some_and(|s| stmt_uses_document(s.as_ref()))
232
- || cond.as_ref().is_some_and(|e| expr_uses_document(e))
233
- || update.as_ref().is_some_and(|e| expr_uses_document(e))
237
+ || cond.as_ref().is_some_and(expr_uses_document)
238
+ || update.as_ref().is_some_and(expr_uses_document)
234
239
  || stmt_uses_document(body)
235
240
  }
236
241
  Statement::ForOf { iterable, body, .. } => {
@@ -244,14 +249,16 @@ pub fn program_uses_document(program: &Program) -> bool {
244
249
  } => {
245
250
  expr_uses_document(expr)
246
251
  || cases.iter().any(|(e, stmts)| {
247
- e.as_ref().is_some_and(|e| expr_uses_document(e))
252
+ e.as_ref().is_some_and(expr_uses_document)
248
253
  || stmts.iter().any(stmt_uses_document)
249
254
  })
250
255
  || default_body
251
256
  .as_ref()
252
257
  .is_some_and(|stmts| stmts.iter().any(stmt_uses_document))
253
258
  }
254
- Statement::Block { statements, .. } => statements.iter().any(stmt_uses_document),
259
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
260
+ statements.iter().any(stmt_uses_document)
261
+ }
255
262
  Statement::FunDecl { body, .. } => stmt_uses_document(body),
256
263
  Statement::Try {
257
264
  body,
@@ -603,7 +610,10 @@ pub fn generate_native_wrapper_rs(
603
610
  key_lit, shim_crate, rust_fn
604
611
  ));
605
612
  }
606
- file.push_str(" Value::Object(VmRef::new(m))\n}\n\n");
613
+ // `Value::object(m)` wraps the `ObjectMap` into the `ObjectData` that `Value::Object`
614
+ // now holds; `Value::Object(VmRef::new(m))` (raw map) stopped type-checking after the
615
+ // PropMap/ObjectData refactor (#78).
616
+ file.push_str(" Value::object(m)\n}\n\n");
607
617
  }
608
618
  if !any {
609
619
  return String::new();
@@ -747,7 +757,9 @@ pub fn has_native_imports(program: &Program) -> bool {
747
757
  pub fn has_external_native_imports(program: &Program) -> bool {
748
758
  for stmt in &program.statements {
749
759
  for spec in stmt_native_specs(stmt) {
750
- if !is_builtin_native_spec(spec.as_ref()) {
760
+ // `ffi:` is portable (loadable on every backend via the C ABI), so it is NOT an
761
+ // "external native import" that non-rust backends must reject — only `cargo:` is.
762
+ if !is_builtin_native_spec(spec.as_ref()) && !is_ffi_native_spec(&spec) {
751
763
  return true;
752
764
  }
753
765
  }
@@ -755,6 +767,20 @@ pub fn has_external_native_imports(program: &Program) -> bool {
755
767
  false
756
768
  }
757
769
 
770
+ /// Every `ffi:…` spec imported anywhere in `program` (deduplicated, in first-seen order). The CLI
771
+ /// loads each cdylib with `tish_ffi::load_module` and registers it before running.
772
+ pub fn ffi_native_specs(program: &Program) -> Vec<String> {
773
+ let mut out: Vec<String> = Vec::new();
774
+ for stmt in &program.statements {
775
+ for spec in stmt_native_specs(stmt) {
776
+ if is_ffi_native_spec(&spec) && !out.contains(&spec) {
777
+ out.push(spec);
778
+ }
779
+ }
780
+ }
781
+ out
782
+ }
783
+
758
784
  /// A resolved module: path and its parsed program.
759
785
  #[derive(Debug, Clone)]
760
786
  pub struct ResolvedModule {
@@ -907,28 +933,47 @@ fn load_module_recursive(
907
933
  /// - fs, http, timers, process, ws (Node-compatible aliases for tish:*)
908
934
  /// - tish:egui, tish:polars, etc.
909
935
  /// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
936
+ /// - ffi:… (a C-ABI cdylib loaded via `tish_ffi::load_module` — portable across backends)
910
937
  ///
911
938
  /// Scoped npm packages (`@scope/pkg`) are merged as Tish source unless imported via `tish:…`.
912
939
  pub fn is_native_import(spec: &str) -> bool {
913
940
  spec.starts_with("tish:")
914
941
  || spec.starts_with("cargo:")
942
+ || spec.starts_with("ffi:")
915
943
  || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
916
944
  }
917
945
 
946
+ /// True for `ffi:…` specs (portable C-ABI cdylib extensions, loadable on every backend). The
947
+ /// path after `ffi:` is resolved relative to the importing program and loaded with
948
+ /// `tish_ffi::load_module`. Unlike `cargo:` (rust-AOT only), `ffi:` is allowed everywhere.
949
+ pub fn is_ffi_native_spec(spec: &str) -> bool {
950
+ spec.starts_with("ffi:")
951
+ }
952
+
918
953
  /// Map native spec to Cargo feature name for built-in tish:* modules.
919
954
  pub fn native_spec_to_feature(spec: &str) -> Option<String> {
920
955
  let canonical = normalize_builtin_spec(spec)?;
921
956
  canonical.strip_prefix("tish:").map(|s| s.to_string())
922
957
  }
923
958
 
924
- /// Resolve `package.json` at `pkg_root` to the package's main `.tish` entry, if `name` matches `spec`.
925
- fn resolve_package_root_to_entry(pkg_root: &Path, spec: &str) -> Option<PathBuf> {
959
+ /// Resolve `package.json` at `pkg_root` to the package's main `.tish` entry.
960
+ ///
961
+ /// `require_name`: when `Some(spec)`, the package's `package.json` `name` must equal `spec` (used for
962
+ /// the sibling / walk-up heuristic, where a coincidental directory name would otherwise false-match).
963
+ /// When `None`, the directory is authoritative — used for a `node_modules/<spec>` lookup, because npm
964
+ /// installs a dependency under its *dependency key* (the directory), not the package's internal `name`
965
+ /// (e.g. an aliased / scoped package, or a `file:`/workspace link). This matches Node's resolution and
966
+ /// tish's own path-dep rewriting, so a workspace package linked in as `lattish` resolves even though its
967
+ /// own `name` is `@tishlang/lattish`.
968
+ fn resolve_package_entry(pkg_root: &Path, require_name: Option<&str>) -> Option<PathBuf> {
926
969
  let pkg_json = pkg_root.join("package.json");
927
970
  if !pkg_json.exists() {
928
971
  return None;
929
972
  }
930
- if read_package_name(&pkg_json).as_deref() != Some(spec) {
931
- return None;
973
+ if let Some(spec) = require_name {
974
+ if read_package_name(&pkg_json).as_deref() != Some(spec) {
975
+ return None;
976
+ }
932
977
  }
933
978
  let content = std::fs::read_to_string(&pkg_json).ok()?;
934
979
  let json: serde_json::Value = serde_json::from_str(&content).ok()?;
@@ -958,15 +1003,18 @@ fn resolve_package_root_to_entry(pkg_root: &Path, spec: &str) -> Option<PathBuf>
958
1003
  pub fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> Option<PathBuf> {
959
1004
  let mut search = from_dir.to_path_buf();
960
1005
  loop {
961
- if let Some(p) =
962
- resolve_package_root_to_entry(&search.join("node_modules").join(spec), spec)
963
- {
1006
+ // node_modules/<spec>: the directory is authoritative (npm installs by dependency key, like
1007
+ // Node) — do NOT require the package's internal `name` to match, so aliased/scoped/`file:`
1008
+ // workspace packages linked in under this name resolve.
1009
+ if let Some(p) = resolve_package_entry(&search.join("node_modules").join(spec), None) {
964
1010
  return Some(p);
965
1011
  }
966
- if let Some(p) = resolve_package_root_to_entry(&search.join(spec), spec) {
1012
+ // sibling <spec>/ and the search dir itself: require name match (a bare directory name is a
1013
+ // weaker signal — guard against a coincidental same-named dir in a monorepo walk).
1014
+ if let Some(p) = resolve_package_entry(&search.join(spec), Some(spec)) {
967
1015
  return Some(p);
968
1016
  }
969
- if let Some(p) = resolve_package_root_to_entry(&search, spec) {
1017
+ if let Some(p) = resolve_package_entry(&search, Some(spec)) {
970
1018
  return Some(p);
971
1019
  }
972
1020
  if let Some(parent) = search.parent() {
@@ -1110,10 +1158,525 @@ fn merge_push(
1110
1158
  statement_sources.push(source);
1111
1159
  }
1112
1160
 
1161
+ // ── #97: module-private top-level binding isolation ─────────────────────────────────────
1162
+ //
1163
+ // All modules are concatenated into one flat program, so two modules that declare the same
1164
+ // *non-exported* top-level name (`let SHARED`, `fn err`, …) collide: a silent wrong value at
1165
+ // runtime, and a duplicate `let` (SyntaxError) in the `--target js` bundle. We give each such
1166
+ // private binding a module-unique name and rewrite references within that module. Exported
1167
+ // and imported names are never touched, so the import/export resolution below is unaffected —
1168
+ // and a name that doesn't collide is left exactly as-is (zero blast radius).
1169
+
1170
+ /// Names a module contributes to the merged flat namespace at its top level.
1171
+ /// `decls` = names declared (`let`/`const`/`fn`/destructure), `exported` = the subset that is
1172
+ /// exported, `imports` = local names bound by `import`. Type-only declarations are erased.
1173
+ fn collect_module_top_level_names(
1174
+ stmts: &[Statement],
1175
+ decls: &mut HashSet<String>,
1176
+ exported: &mut HashSet<String>,
1177
+ imports: &mut HashSet<String>,
1178
+ ) {
1179
+ for stmt in stmts {
1180
+ match stmt {
1181
+ Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
1182
+ decls.insert(name.to_string());
1183
+ }
1184
+ Statement::VarDeclDestructure { pattern, .. } => {
1185
+ collect_destructure_names(pattern, decls);
1186
+ }
1187
+ Statement::Multi { statements, .. } => {
1188
+ collect_module_top_level_names(statements, decls, exported, imports);
1189
+ }
1190
+ Statement::Export { declaration, .. } => {
1191
+ if let ExportDeclaration::Named(inner) = declaration.as_ref() {
1192
+ match inner.as_ref() {
1193
+ Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
1194
+ decls.insert(name.to_string());
1195
+ exported.insert(name.to_string());
1196
+ }
1197
+ Statement::VarDeclDestructure { pattern, .. } => {
1198
+ let mut names = HashSet::new();
1199
+ collect_destructure_names(pattern, &mut names);
1200
+ for n in names {
1201
+ decls.insert(n.clone());
1202
+ exported.insert(n);
1203
+ }
1204
+ }
1205
+ _ => {}
1206
+ }
1207
+ }
1208
+ }
1209
+ Statement::Import { specifiers, .. } => {
1210
+ for spec in specifiers {
1211
+ let n = match spec {
1212
+ ImportSpecifier::Named { name, alias, .. } => {
1213
+ alias.as_deref().unwrap_or(name).to_string()
1214
+ }
1215
+ ImportSpecifier::Namespace { name, .. }
1216
+ | ImportSpecifier::Default { name, .. } => name.to_string(),
1217
+ };
1218
+ imports.insert(n);
1219
+ }
1220
+ }
1221
+ _ => {}
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ fn collect_destructure_names(pattern: &DestructPattern, out: &mut HashSet<String>) {
1227
+ let push = |el: &DestructElement, out: &mut HashSet<String>| match el {
1228
+ DestructElement::Ident(n, _) | DestructElement::Rest(n, _) => {
1229
+ out.insert(n.to_string());
1230
+ }
1231
+ DestructElement::Pattern(p) => collect_destructure_names(p, out),
1232
+ };
1233
+ match pattern {
1234
+ DestructPattern::Array(elements) => {
1235
+ for el in elements.iter().flatten() {
1236
+ push(el, out);
1237
+ }
1238
+ }
1239
+ DestructPattern::Object(props) => {
1240
+ for p in props {
1241
+ push(&p.value, out);
1242
+ }
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ /// Rename each module's non-exported top-level bindings whose name also occurs as a top-level
1248
+ /// name in another module, isolating module-private declarations (#97).
1249
+ fn isolate_private_top_level_bindings(modules: &mut [ResolvedModule]) {
1250
+ let n = modules.len();
1251
+ if n < 2 {
1252
+ return; // a single module cannot collide with another
1253
+ }
1254
+ let mut decls: Vec<HashSet<String>> = vec![HashSet::new(); n];
1255
+ let mut exported: Vec<HashSet<String>> = vec![HashSet::new(); n];
1256
+ // `occupancy[i]` = every top-level name module i contributes (decls ∪ import bindings).
1257
+ let mut occupancy: Vec<HashSet<String>> = vec![HashSet::new(); n];
1258
+ for (i, m) in modules.iter().enumerate() {
1259
+ let mut imports = HashSet::new();
1260
+ collect_module_top_level_names(
1261
+ &m.program.statements,
1262
+ &mut decls[i],
1263
+ &mut exported[i],
1264
+ &mut imports,
1265
+ );
1266
+ occupancy[i] = decls[i].union(&imports).cloned().collect();
1267
+ }
1268
+ // How many modules contribute each top-level name.
1269
+ let mut count: HashMap<&str, usize> = HashMap::new();
1270
+ for occ in &occupancy {
1271
+ for name in occ {
1272
+ *count.entry(name.as_str()).or_insert(0) += 1;
1273
+ }
1274
+ }
1275
+ for (i, m) in modules.iter_mut().enumerate() {
1276
+ let mut renames: HashMap<String, Arc<str>> = HashMap::new();
1277
+ for name in &decls[i] {
1278
+ if exported[i].contains(name) {
1279
+ continue; // exported names stay stable so imports keep resolving
1280
+ }
1281
+ if count.get(name.as_str()).copied().unwrap_or(0) > 1 {
1282
+ renames.insert(name.clone(), Arc::from(format!("{name}__m{i}")));
1283
+ }
1284
+ }
1285
+ if renames.is_empty() {
1286
+ continue;
1287
+ }
1288
+ for stmt in &mut m.program.statements {
1289
+ // Each top-level statement starts from the full rename set (module top level is a
1290
+ // single scope; nested scopes shadow within their own cloned set).
1291
+ let mut active = renames.clone();
1292
+ rewrite_stmt_scope(stmt, &mut active, true);
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ /// Apply the rename for a *declared* name. At module top level the binding is the canonical
1298
+ /// private one — rename it. In a nested scope the same name is a shadow — drop it from `active`
1299
+ /// so the inner binding and its references keep their own identity.
1300
+ fn apply_binding(name: &mut Arc<str>, active: &mut HashMap<String, Arc<str>>, top_level: bool) {
1301
+ if top_level {
1302
+ if let Some(renamed) = active.get(name.as_ref()) {
1303
+ *name = Arc::clone(renamed);
1304
+ }
1305
+ } else {
1306
+ active.remove(name.as_ref());
1307
+ }
1308
+ }
1309
+
1310
+ /// Rename / shadow the names bound by a destructuring pattern (mirrors [`apply_binding`]).
1311
+ fn rewrite_destructure_binding(
1312
+ pattern: &mut DestructPattern,
1313
+ active: &mut HashMap<String, Arc<str>>,
1314
+ top_level: bool,
1315
+ ) {
1316
+ fn one(el: &mut DestructElement, active: &mut HashMap<String, Arc<str>>, top_level: bool) {
1317
+ match el {
1318
+ DestructElement::Ident(n, _) | DestructElement::Rest(n, _) => {
1319
+ if top_level {
1320
+ if let Some(renamed) = active.get(n.as_ref()) {
1321
+ *n = Arc::clone(renamed);
1322
+ }
1323
+ } else {
1324
+ active.remove(n.as_ref());
1325
+ }
1326
+ }
1327
+ DestructElement::Pattern(p) => rewrite_destructure_binding(p, active, top_level),
1328
+ }
1329
+ }
1330
+ match pattern {
1331
+ DestructPattern::Array(elements) => {
1332
+ for el in elements.iter_mut().flatten() {
1333
+ one(el, active, top_level);
1334
+ }
1335
+ }
1336
+ DestructPattern::Object(props) => {
1337
+ for p in props.iter_mut() {
1338
+ one(&mut p.value, active, top_level); // p.key is the source property — untouched
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ /// Remove function/arrow parameter names from a (child-scope) rename set so the body's
1345
+ /// references to them are not rewritten, and rewrite any default-value expressions in the
1346
+ /// enclosing scope.
1347
+ fn shadow_params(
1348
+ params: &mut [FunParam],
1349
+ child: &mut HashMap<String, Arc<str>>,
1350
+ parent: &HashMap<String, Arc<str>>,
1351
+ ) {
1352
+ for p in params.iter_mut() {
1353
+ match p {
1354
+ FunParam::Simple(tp) => {
1355
+ if let Some(d) = &mut tp.default {
1356
+ rewrite_expr_scope(d, parent);
1357
+ }
1358
+ child.remove(tp.name.as_ref());
1359
+ }
1360
+ FunParam::Destructure {
1361
+ pattern, default, ..
1362
+ } => {
1363
+ if let Some(d) = default {
1364
+ rewrite_expr_scope(d, parent);
1365
+ }
1366
+ let mut names = HashSet::new();
1367
+ collect_destructure_names(pattern, &mut names);
1368
+ for n in &names {
1369
+ child.remove(n);
1370
+ }
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1375
+
1376
+ /// Scope-aware statement rewriter for [`isolate_private_top_level_bindings`].
1377
+ fn rewrite_stmt_scope(
1378
+ stmt: &mut Statement,
1379
+ active: &mut HashMap<String, Arc<str>>,
1380
+ top_level: bool,
1381
+ ) {
1382
+ match stmt {
1383
+ Statement::VarDecl { name, init, .. } => {
1384
+ if let Some(e) = init {
1385
+ rewrite_expr_scope(e, active);
1386
+ }
1387
+ apply_binding(name, active, top_level);
1388
+ }
1389
+ Statement::VarDeclDestructure { pattern, init, .. } => {
1390
+ rewrite_expr_scope(init, active);
1391
+ rewrite_destructure_binding(pattern, active, top_level);
1392
+ }
1393
+ Statement::Multi { statements, .. } => {
1394
+ // Same-scope group (`let a = 1, b = 2`): thread `active`, keep `top_level`.
1395
+ for s in statements {
1396
+ rewrite_stmt_scope(s, active, top_level);
1397
+ }
1398
+ }
1399
+ Statement::ExprStmt { expr, .. } => rewrite_expr_scope(expr, active),
1400
+ Statement::Block { statements, .. } => {
1401
+ let mut child = active.clone();
1402
+ for s in statements {
1403
+ rewrite_stmt_scope(s, &mut child, false);
1404
+ }
1405
+ }
1406
+ Statement::If {
1407
+ cond,
1408
+ then_branch,
1409
+ else_branch,
1410
+ ..
1411
+ } => {
1412
+ rewrite_expr_scope(cond, active);
1413
+ rewrite_stmt_scope(then_branch, &mut active.clone(), false);
1414
+ if let Some(e) = else_branch {
1415
+ rewrite_stmt_scope(e, &mut active.clone(), false);
1416
+ }
1417
+ }
1418
+ Statement::While { cond, body, .. } => {
1419
+ rewrite_expr_scope(cond, active);
1420
+ rewrite_stmt_scope(body, &mut active.clone(), false);
1421
+ }
1422
+ Statement::DoWhile { body, cond, .. } => {
1423
+ rewrite_stmt_scope(body, &mut active.clone(), false);
1424
+ rewrite_expr_scope(cond, active);
1425
+ }
1426
+ Statement::For {
1427
+ init,
1428
+ cond,
1429
+ update,
1430
+ body,
1431
+ ..
1432
+ } => {
1433
+ let mut child = active.clone();
1434
+ if let Some(i) = init {
1435
+ rewrite_stmt_scope(i, &mut child, false);
1436
+ }
1437
+ if let Some(e) = cond {
1438
+ rewrite_expr_scope(e, &child);
1439
+ }
1440
+ if let Some(e) = update {
1441
+ rewrite_expr_scope(e, &child);
1442
+ }
1443
+ rewrite_stmt_scope(body, &mut child, false);
1444
+ }
1445
+ Statement::ForOf {
1446
+ name,
1447
+ iterable,
1448
+ body,
1449
+ ..
1450
+ } => {
1451
+ rewrite_expr_scope(iterable, active);
1452
+ let mut child = active.clone();
1453
+ child.remove(name.as_ref()); // loop variable shadows
1454
+ rewrite_stmt_scope(body, &mut child, false);
1455
+ }
1456
+ Statement::Return { value, .. } => {
1457
+ if let Some(e) = value {
1458
+ rewrite_expr_scope(e, active);
1459
+ }
1460
+ }
1461
+ Statement::Throw { value, .. } => rewrite_expr_scope(value, active),
1462
+ Statement::Break { .. } | Statement::Continue { .. } => {}
1463
+ Statement::FunDecl {
1464
+ name,
1465
+ params,
1466
+ rest_param,
1467
+ body,
1468
+ ..
1469
+ } => {
1470
+ apply_binding(name, active, top_level);
1471
+ let mut child = active.clone();
1472
+ shadow_params(params, &mut child, active);
1473
+ if let Some(rp) = rest_param {
1474
+ child.remove(rp.name.as_ref());
1475
+ }
1476
+ rewrite_stmt_scope(body, &mut child, false);
1477
+ }
1478
+ Statement::Switch {
1479
+ expr,
1480
+ cases,
1481
+ default_body,
1482
+ ..
1483
+ } => {
1484
+ rewrite_expr_scope(expr, active);
1485
+ // A switch body shares one block scope across all cases.
1486
+ let mut child = active.clone();
1487
+ for (test, body) in cases.iter_mut() {
1488
+ if let Some(t) = test {
1489
+ rewrite_expr_scope(t, &child);
1490
+ }
1491
+ for s in body {
1492
+ rewrite_stmt_scope(s, &mut child, false);
1493
+ }
1494
+ }
1495
+ if let Some(body) = default_body {
1496
+ for s in body {
1497
+ rewrite_stmt_scope(s, &mut child, false);
1498
+ }
1499
+ }
1500
+ }
1501
+ Statement::Try {
1502
+ body,
1503
+ catch_param,
1504
+ catch_body,
1505
+ finally_body,
1506
+ ..
1507
+ } => {
1508
+ rewrite_stmt_scope(body, &mut active.clone(), false);
1509
+ if let Some(cb) = catch_body {
1510
+ let mut child = active.clone();
1511
+ if let Some(p) = catch_param {
1512
+ child.remove(p.as_ref()); // catch binding shadows
1513
+ }
1514
+ rewrite_stmt_scope(cb, &mut child, false);
1515
+ }
1516
+ if let Some(fb) = finally_body {
1517
+ rewrite_stmt_scope(fb, &mut active.clone(), false);
1518
+ }
1519
+ }
1520
+ Statement::Export { declaration, .. } => match declaration.as_mut() {
1521
+ // Exported declarations keep their name (not in `active`), but their initializer /
1522
+ // body can still reference module-private names that were renamed.
1523
+ ExportDeclaration::Named(inner) => rewrite_stmt_scope(inner, active, top_level),
1524
+ ExportDeclaration::Default(e) => rewrite_expr_scope(e, active),
1525
+ },
1526
+ Statement::Import { .. }
1527
+ | Statement::TypeAlias { .. }
1528
+ | Statement::DeclareVar { .. }
1529
+ | Statement::DeclareFun { .. } => {}
1530
+ }
1531
+ }
1532
+
1533
+ /// Scope-aware expression rewriter: rename every free reference to a name in `active`.
1534
+ /// Expressions never declare module-level bindings, so `active` is read-only here; arrow
1535
+ /// functions clone it for their own (shadowed) parameter scope.
1536
+ fn rewrite_expr_scope(expr: &mut Expr, active: &HashMap<String, Arc<str>>) {
1537
+ let rename = |name: &mut Arc<str>| {
1538
+ if let Some(renamed) = active.get(name.as_ref()) {
1539
+ *name = Arc::clone(renamed);
1540
+ }
1541
+ };
1542
+ match expr {
1543
+ Expr::Ident { name, .. } => rename(name),
1544
+ Expr::Assign { name, value, .. }
1545
+ | Expr::CompoundAssign { name, value, .. }
1546
+ | Expr::LogicalAssign { name, value, .. } => {
1547
+ rename(name);
1548
+ rewrite_expr_scope(value, active);
1549
+ }
1550
+ Expr::PostfixInc { name, .. }
1551
+ | Expr::PostfixDec { name, .. }
1552
+ | Expr::PrefixInc { name, .. }
1553
+ | Expr::PrefixDec { name, .. } => rename(name),
1554
+ Expr::Binary { left, right, .. } => {
1555
+ rewrite_expr_scope(left, active);
1556
+ rewrite_expr_scope(right, active);
1557
+ }
1558
+ Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } | Expr::Await { operand, .. } => {
1559
+ rewrite_expr_scope(operand, active)
1560
+ }
1561
+ Expr::Delete { target, .. } => rewrite_expr_scope(target, active),
1562
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
1563
+ rewrite_expr_scope(callee, active);
1564
+ for a in args {
1565
+ match a {
1566
+ CallArg::Expr(e) | CallArg::Spread(e) => rewrite_expr_scope(e, active),
1567
+ }
1568
+ }
1569
+ }
1570
+ Expr::Member { object, prop, .. } => {
1571
+ rewrite_expr_scope(object, active);
1572
+ if let MemberProp::Expr(e) = prop {
1573
+ rewrite_expr_scope(e, active); // computed key; `obj.name` is untouched
1574
+ }
1575
+ }
1576
+ Expr::Index { object, index, .. } => {
1577
+ rewrite_expr_scope(object, active);
1578
+ rewrite_expr_scope(index, active);
1579
+ }
1580
+ Expr::Conditional {
1581
+ cond,
1582
+ then_branch,
1583
+ else_branch,
1584
+ ..
1585
+ } => {
1586
+ rewrite_expr_scope(cond, active);
1587
+ rewrite_expr_scope(then_branch, active);
1588
+ rewrite_expr_scope(else_branch, active);
1589
+ }
1590
+ Expr::NullishCoalesce { left, right, .. } => {
1591
+ rewrite_expr_scope(left, active);
1592
+ rewrite_expr_scope(right, active);
1593
+ }
1594
+ Expr::Array { elements, .. } => {
1595
+ for el in elements {
1596
+ match el {
1597
+ ArrayElement::Expr(e) | ArrayElement::Spread(e) => rewrite_expr_scope(e, active),
1598
+ }
1599
+ }
1600
+ }
1601
+ Expr::Object { props, .. } => {
1602
+ for p in props {
1603
+ match p {
1604
+ ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
1605
+ rewrite_expr_scope(e, active) // key is a property name; value recurses
1606
+ }
1607
+ }
1608
+ }
1609
+ }
1610
+ Expr::MemberAssign { object, value, .. } => {
1611
+ rewrite_expr_scope(object, active);
1612
+ rewrite_expr_scope(value, active);
1613
+ }
1614
+ Expr::IndexAssign {
1615
+ object,
1616
+ index,
1617
+ value,
1618
+ ..
1619
+ } => {
1620
+ rewrite_expr_scope(object, active);
1621
+ rewrite_expr_scope(index, active);
1622
+ rewrite_expr_scope(value, active);
1623
+ }
1624
+ Expr::ArrowFunction { params, body, .. } => {
1625
+ let mut child = active.clone();
1626
+ shadow_params(params, &mut child, active);
1627
+ match body {
1628
+ ArrowBody::Expr(e) => rewrite_expr_scope(e, &child),
1629
+ ArrowBody::Block(s) => rewrite_stmt_scope(s, &mut child, false),
1630
+ }
1631
+ }
1632
+ Expr::TemplateLiteral { exprs, .. } => {
1633
+ for e in exprs {
1634
+ rewrite_expr_scope(e, active);
1635
+ }
1636
+ }
1637
+ Expr::JsxElement {
1638
+ tag,
1639
+ props,
1640
+ children,
1641
+ ..
1642
+ } => {
1643
+ // `<Component …>` references a binding; lowercase HTML tags aren't in `active`.
1644
+ if let Some(renamed) = active.get(tag.as_ref()) {
1645
+ *tag = Arc::clone(renamed);
1646
+ }
1647
+ for prop in props {
1648
+ match prop {
1649
+ JsxProp::Attr { value, .. } => match value {
1650
+ JsxAttrValue::Expr(e) => rewrite_expr_scope(e, active),
1651
+ JsxAttrValue::String(_) | JsxAttrValue::ImplicitTrue => {}
1652
+ },
1653
+ JsxProp::Spread(e) => rewrite_expr_scope(e, active),
1654
+ }
1655
+ }
1656
+ for child in children {
1657
+ if let JsxChild::Expr(e) = child {
1658
+ rewrite_expr_scope(e, active);
1659
+ }
1660
+ }
1661
+ }
1662
+ Expr::JsxFragment { children, .. } => {
1663
+ for child in children {
1664
+ if let JsxChild::Expr(e) = child {
1665
+ rewrite_expr_scope(e, active);
1666
+ }
1667
+ }
1668
+ }
1669
+ Expr::Literal { .. } | Expr::NativeModuleLoad { .. } => {}
1670
+ }
1671
+ }
1672
+
1113
1673
  /// Merge all resolved modules into a single program. Dependencies are emitted first.
1114
1674
  /// Import statements are rewritten as bindings from already-emitted dep exports.
1115
1675
  /// Export statements are unwrapped (the inner declaration is emitted).
1116
- pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<MergedProgram, String> {
1676
+ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram, String> {
1677
+ // #97: isolate module-private top-level bindings before they are flattened together.
1678
+ isolate_private_top_level_bindings(&mut modules);
1679
+
1117
1680
  let path_to_idx: HashMap<PathBuf, usize> = modules
1118
1681
  .iter()
1119
1682
  .enumerate()