@tishlang/tish 1.6.0 → 1.7.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.
Files changed (79) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/error.rs +2 -8
  4. package/crates/js_to_tish/src/transform/expr.rs +101 -130
  5. package/crates/js_to_tish/src/transform/stmt.rs +25 -22
  6. package/crates/tish/Cargo.toml +1 -1
  7. package/crates/tish/src/cli_help.rs +76 -29
  8. package/crates/tish/src/main.rs +85 -54
  9. package/crates/tish/tests/cargo_example_compile.rs +3 -1
  10. package/crates/tish/tests/integration_test.rs +197 -47
  11. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  12. package/crates/tish/tests/shortcircuit.rs +19 -4
  13. package/crates/tish_ast/src/ast.rs +12 -14
  14. package/crates/tish_build_utils/src/lib.rs +31 -6
  15. package/crates/tish_builtins/src/array.rs +52 -21
  16. package/crates/tish_builtins/src/construct.rs +2 -8
  17. package/crates/tish_builtins/src/globals.rs +30 -15
  18. package/crates/tish_builtins/src/lib.rs +5 -5
  19. package/crates/tish_builtins/src/math.rs +5 -3
  20. package/crates/tish_builtins/src/string.rs +71 -19
  21. package/crates/tish_bytecode/src/chunk.rs +0 -1
  22. package/crates/tish_bytecode/src/compiler.rs +164 -60
  23. package/crates/tish_bytecode/src/opcode.rs +13 -4
  24. package/crates/tish_bytecode/src/peephole.rs +2 -2
  25. package/crates/tish_compile/src/codegen.rs +921 -299
  26. package/crates/tish_compile/src/infer.rs +69 -19
  27. package/crates/tish_compile/src/lib.rs +15 -5
  28. package/crates/tish_compile/src/resolve.rs +112 -69
  29. package/crates/tish_compile/src/types.rs +10 -14
  30. package/crates/tish_compile_js/src/codegen.rs +34 -13
  31. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  32. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  33. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +39 -48
  34. package/crates/tish_core/src/json.rs +5 -3
  35. package/crates/tish_core/src/lib.rs +1 -1
  36. package/crates/tish_core/src/uri.rs +9 -6
  37. package/crates/tish_core/src/value.rs +92 -28
  38. package/crates/tish_cranelift/src/link.rs +6 -9
  39. package/crates/tish_cranelift/src/lower.rs +14 -8
  40. package/crates/tish_eval/src/eval.rs +389 -142
  41. package/crates/tish_eval/src/lib.rs +10 -6
  42. package/crates/tish_eval/src/natives.rs +95 -38
  43. package/crates/tish_eval/src/promise.rs +14 -8
  44. package/crates/tish_eval/src/timers.rs +28 -19
  45. package/crates/tish_eval/src/value.rs +10 -3
  46. package/crates/tish_fmt/src/lib.rs +29 -13
  47. package/crates/tish_lexer/src/lib.rs +217 -63
  48. package/crates/tish_lexer/src/token.rs +6 -6
  49. package/crates/tish_llvm/src/lib.rs +15 -8
  50. package/crates/tish_lsp/src/main.rs +41 -43
  51. package/crates/tish_native/src/build.rs +1 -6
  52. package/crates/tish_native/src/lib.rs +48 -19
  53. package/crates/tish_opt/src/lib.rs +67 -50
  54. package/crates/tish_parser/src/lib.rs +36 -11
  55. package/crates/tish_parser/src/parser.rs +172 -87
  56. package/crates/tish_runtime/src/http.rs +15 -6
  57. package/crates/tish_runtime/src/http_fetch.rs +24 -14
  58. package/crates/tish_runtime/src/lib.rs +224 -168
  59. package/crates/tish_runtime/src/promise.rs +1 -5
  60. package/crates/tish_runtime/src/ws.rs +45 -20
  61. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  62. package/crates/tish_ui/src/jsx.rs +41 -22
  63. package/crates/tish_ui/src/lib.rs +2 -2
  64. package/crates/tish_vm/src/vm.rs +309 -112
  65. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
  66. package/crates/tish_wasm/src/lib.rs +38 -28
  67. package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
  68. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  69. package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
  70. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  71. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  72. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  73. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  74. package/package.json +1 -1
  75. package/platform/darwin-arm64/tish +0 -0
  76. package/platform/darwin-x64/tish +0 -0
  77. package/platform/linux-arm64/tish +0 -0
  78. package/platform/linux-x64/tish +0 -0
  79. package/platform/win32-x64/tish.exe +0 -0
@@ -22,7 +22,9 @@ pub struct InferCtx {
22
22
 
23
23
  impl InferCtx {
24
24
  pub fn new() -> Self {
25
- Self { scopes: vec![HashMap::new()] }
25
+ Self {
26
+ scopes: vec![HashMap::new()],
27
+ }
26
28
  }
27
29
 
28
30
  fn push_scope(&mut self) {
@@ -75,17 +77,16 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
75
77
  Literal::Null => None,
76
78
  },
77
79
  Expr::Ident { name, .. } => ctx.lookup(name.as_ref()).cloned(),
78
- Expr::Binary { left, op, right, .. } => {
80
+ Expr::Binary {
81
+ left, op, right, ..
82
+ } => {
79
83
  let lt = infer_expr_type(left, ctx)?;
80
84
  let rt = infer_expr_type(right, ctx)?;
81
85
  if is_number(&lt) && is_number(&rt) {
82
86
  match op {
83
- BinOp::Add
84
- | BinOp::Sub
85
- | BinOp::Mul
86
- | BinOp::Div
87
- | BinOp::Mod
88
- | BinOp::Pow => Some(number_ann()),
87
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
88
+ Some(number_ann())
89
+ }
89
90
  BinOp::Lt
90
91
  | BinOp::Le
91
92
  | BinOp::Gt
@@ -103,7 +104,11 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
103
104
  match op {
104
105
  UnaryOp::Neg | UnaryOp::Pos => {
105
106
  let t = infer_expr_type(operand, ctx)?;
106
- if is_number(&t) { Some(number_ann()) } else { None }
107
+ if is_number(&t) {
108
+ Some(number_ann())
109
+ } else {
110
+ None
111
+ }
107
112
  }
108
113
  UnaryOp::Not => Some(bool_ann()),
109
114
  _ => None,
@@ -117,7 +122,9 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
117
122
  /// type annotations filled in on `VarDecl` nodes.
118
123
  pub fn infer_program(program: &Program) -> Program {
119
124
  let mut ctx = InferCtx::new();
120
- Program { statements: infer_statements(&program.statements, &mut ctx) }
125
+ Program {
126
+ statements: infer_statements(&program.statements, &mut ctx),
127
+ }
121
128
  }
122
129
 
123
130
  fn infer_statements(stmts: &[Statement], ctx: &mut InferCtx) -> Vec<Statement> {
@@ -126,7 +133,13 @@ fn infer_statements(stmts: &[Statement], ctx: &mut InferCtx) -> Vec<Statement> {
126
133
 
127
134
  fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
128
135
  match stmt {
129
- Statement::VarDecl { name, mutable, type_ann, init, span } => {
136
+ Statement::VarDecl {
137
+ name,
138
+ mutable,
139
+ type_ann,
140
+ init,
141
+ span,
142
+ } => {
130
143
  // Already annotated — propagate into ctx but don't change the node.
131
144
  if let Some(ann) = type_ann {
132
145
  ctx.define(name.as_ref(), ann.clone());
@@ -149,9 +162,18 @@ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
149
162
  ctx.push_scope();
150
163
  let stmts = infer_statements(statements, ctx);
151
164
  ctx.pop_scope();
152
- Statement::Block { statements: stmts, span: *span }
165
+ Statement::Block {
166
+ statements: stmts,
167
+ span: *span,
168
+ }
153
169
  }
154
- Statement::For { init, cond, update, body, span } => {
170
+ Statement::For {
171
+ init,
172
+ cond,
173
+ update,
174
+ body,
175
+ span,
176
+ } => {
155
177
  // Scope for loop variable
156
178
  ctx.push_scope();
157
179
  let new_init = init.as_ref().map(|i| Box::new(infer_statement(i, ctx)));
@@ -165,7 +187,12 @@ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
165
187
  span: *span,
166
188
  }
167
189
  }
168
- Statement::ForOf { name, iterable, body, span } => {
190
+ Statement::ForOf {
191
+ name,
192
+ iterable,
193
+ body,
194
+ span,
195
+ } => {
169
196
  ctx.push_scope();
170
197
  let new_body = Box::new(infer_statement(body, ctx));
171
198
  ctx.pop_scope();
@@ -180,17 +207,32 @@ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
180
207
  ctx.push_scope();
181
208
  let new_body = Box::new(infer_statement(body, ctx));
182
209
  ctx.pop_scope();
183
- Statement::While { cond: cond.clone(), body: new_body, span: *span }
210
+ Statement::While {
211
+ cond: cond.clone(),
212
+ body: new_body,
213
+ span: *span,
214
+ }
184
215
  }
185
216
  Statement::DoWhile { body, cond, span } => {
186
217
  ctx.push_scope();
187
218
  let new_body = Box::new(infer_statement(body, ctx));
188
219
  ctx.pop_scope();
189
- Statement::DoWhile { body: new_body, cond: cond.clone(), span: *span }
220
+ Statement::DoWhile {
221
+ body: new_body,
222
+ cond: cond.clone(),
223
+ span: *span,
224
+ }
190
225
  }
191
- Statement::If { cond, then_branch, else_branch, span } => {
226
+ Statement::If {
227
+ cond,
228
+ then_branch,
229
+ else_branch,
230
+ span,
231
+ } => {
192
232
  let new_then = Box::new(infer_statement(then_branch, ctx));
193
- let new_else = else_branch.as_ref().map(|e| Box::new(infer_statement(e, ctx)));
233
+ let new_else = else_branch
234
+ .as_ref()
235
+ .map(|e| Box::new(infer_statement(e, ctx)));
194
236
  Statement::If {
195
237
  cond: cond.clone(),
196
238
  then_branch: new_then,
@@ -198,7 +240,15 @@ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
198
240
  span: *span,
199
241
  }
200
242
  }
201
- Statement::FunDecl { async_, name, params, rest_param, return_type, body, span } => {
243
+ Statement::FunDecl {
244
+ async_,
245
+ name,
246
+ params,
247
+ rest_param,
248
+ return_type,
249
+ body,
250
+ span,
251
+ } => {
202
252
  ctx.push_scope();
203
253
  for p in params {
204
254
  if let FunParam::Simple(tp) = p {
@@ -7,11 +7,11 @@ mod infer;
7
7
  mod resolve;
8
8
  mod types;
9
9
 
10
+ pub use codegen::CompileError;
10
11
  pub use codegen::{
11
12
  compile, compile_project, compile_project_full, compile_with_features,
12
13
  compile_with_native_modules, compile_with_project_root,
13
14
  };
14
- pub use codegen::CompileError;
15
15
  pub use resolve::{
16
16
  cargo_export_fn_name, compute_native_build_artifacts, detect_cycles, export_name_to_rust_ident,
17
17
  extract_native_import_features, format_rust_dependencies_toml, generate_native_wrapper_rs,
@@ -44,7 +44,10 @@ fn sum(...args: number[]): number {
44
44
  // total should be declared as f64
45
45
  assert!(rust.contains("let mut total: f64"), "expected total: f64");
46
46
  // The return value of run() should convert total back to Value
47
- assert!(rust.contains("Value::Number(total)"), "expected Value::Number(total) wrapping");
47
+ assert!(
48
+ rust.contains("Value::Number(total)"),
49
+ "expected Value::Number(total) wrapping"
50
+ );
48
51
  }
49
52
 
50
53
  #[test]
@@ -60,7 +63,10 @@ for (let i = 0; i < 5; i = i + 1) {
60
63
  let program = parse(src).unwrap();
61
64
  let rust = compile(&program).unwrap();
62
65
  // outerVar and x are f64 (inferred) — Copy assignment, no .clone() needed.
63
- assert!(rust.contains("let mut outerVar: f64"), "expected outerVar: f64");
66
+ assert!(
67
+ rust.contains("let mut outerVar: f64"),
68
+ "expected outerVar: f64"
69
+ );
64
70
  assert!(rust.contains("let mut x: f64"), "expected x: f64");
65
71
  }
66
72
 
@@ -105,13 +111,17 @@ fn factory() {
105
111
  // This test verifies the full benchmark_granular project compiles and that outerVar
106
112
  // is emitted as the inferred f64 type rather than requiring a Value clone.
107
113
  let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
108
- let bench = manifest.join("../../tests/core/benchmark_granular.tish").canonicalize().unwrap();
114
+ let bench = manifest
115
+ .join("../../tests/core/benchmark_granular.tish")
116
+ .canonicalize()
117
+ .unwrap();
109
118
  // Use same default features as tish CLI (http, fs, process, regex)
110
119
  let features = ["http", "fs", "process", "regex"]
111
120
  .into_iter()
112
121
  .map(String::from)
113
122
  .collect::<Vec<_>>();
114
- let (rust, _, _, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
123
+ let (rust, _, _, _) =
124
+ compile_project_full(&bench, bench.parent(), &features, true).unwrap();
115
125
  // outerVar = 42 is inferred as f64; f64 is Copy so no .clone() is emitted.
116
126
  assert!(
117
127
  rust.contains("let mut outerVar: f64"),
@@ -73,7 +73,10 @@ pub fn is_builtin_native_spec(spec: &str) -> bool {
73
73
  /// Resolve all native imports in a merged program via package.json lookup.
74
74
  /// Built-in modules (tish:fs, tish:http, tish:process) are skipped - they use tishlang_runtime directly.
75
75
  /// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
76
- pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<Vec<ResolvedNativeModule>, String> {
76
+ pub fn resolve_native_modules(
77
+ program: &Program,
78
+ project_root: &Path,
79
+ ) -> Result<Vec<ResolvedNativeModule>, String> {
77
80
  let root_canon = project_root
78
81
  .canonicalize()
79
82
  .map_err(|e| format!("Cannot canonicalize project root: {}", e))?;
@@ -131,12 +134,17 @@ pub fn cargo_export_fn_name(spec: &str) -> String {
131
134
  out
132
135
  }
133
136
 
134
- fn resolve_cargo_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
137
+ fn resolve_cargo_native_module(
138
+ spec: &str,
139
+ project_root: &Path,
140
+ ) -> Result<ResolvedNativeModule, String> {
135
141
  let tail = spec
136
142
  .strip_prefix("cargo:")
137
143
  .ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
138
144
  if tail.is_empty() {
139
- return Err("cargo: import needs a dependency name, e.g. import { x } from 'cargo:serde_json'".into());
145
+ return Err(
146
+ "cargo: import needs a dependency name, e.g. import { x } from 'cargo:my_crate'".into(),
147
+ );
140
148
  }
141
149
  let dep_key = tail.to_string();
142
150
  let tish = read_project_tish_config(project_root);
@@ -179,14 +187,26 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
179
187
  let pkg_json = pkg_dir.join("package.json");
180
188
  let content = std::fs::read_to_string(&pkg_json)
181
189
  .map_err(|e| format!("Cannot read {}: {}", pkg_json.display(), e))?;
182
- let json: serde_json::Value =
183
- serde_json::from_str(&content).map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
190
+ let json: serde_json::Value = serde_json::from_str(&content)
191
+ .map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
184
192
  let tish = json
185
193
  .get("tish")
186
194
  .and_then(|v| v.as_object())
187
- .ok_or_else(|| format!("Package {} has no \"tish\" config in package.json", package_name))?;
188
- if !tish.get("module").and_then(|v| v.as_bool()).unwrap_or(false) {
189
- return Err(format!("Package {} is not a Tish native module (tish.module must be true)", package_name));
195
+ .ok_or_else(|| {
196
+ format!(
197
+ "Package {} has no \"tish\" config in package.json",
198
+ package_name
199
+ )
200
+ })?;
201
+ if !tish
202
+ .get("module")
203
+ .and_then(|v| v.as_bool())
204
+ .unwrap_or(false)
205
+ {
206
+ return Err(format!(
207
+ "Package {} is not a Tish native module (tish.module must be true)",
208
+ package_name
209
+ ));
190
210
  }
191
211
  let raw_crate = tish
192
212
  .get("crate")
@@ -219,7 +239,9 @@ pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
219
239
  let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
220
240
  return serde_json::json!({});
221
241
  };
222
- json.get("tish").cloned().unwrap_or_else(|| serde_json::json!({}))
242
+ json.get("tish")
243
+ .cloned()
244
+ .unwrap_or_else(|| serde_json::json!({}))
223
245
  }
224
246
 
225
247
  fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
@@ -233,7 +255,10 @@ fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
233
255
  resolved.display().to_string().replace('\\', "/")
234
256
  }
235
257
 
236
- fn json_to_cargo_inline_value(v: &serde_json::Value, project_root: &Path) -> Result<String, String> {
258
+ fn json_to_cargo_inline_value(
259
+ v: &serde_json::Value,
260
+ project_root: &Path,
261
+ ) -> Result<String, String> {
237
262
  match v {
238
263
  serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
239
264
  serde_json::Value::Bool(b) => Ok(b.to_string()),
@@ -264,7 +289,10 @@ fn json_to_cargo_inline_value(v: &serde_json::Value, project_root: &Path) -> Res
264
289
 
265
290
  /// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
266
291
  /// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
267
- pub fn format_rust_dependencies_toml(tish: &serde_json::Value, project_root: &Path) -> Result<String, String> {
292
+ pub fn format_rust_dependencies_toml(
293
+ tish: &serde_json::Value,
294
+ project_root: &Path,
295
+ ) -> Result<String, String> {
268
296
  let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
269
297
  return Ok(String::new());
270
298
  };
@@ -313,7 +341,10 @@ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet
313
341
  for stmt in &program.statements {
314
342
  match stmt {
315
343
  Statement::VarDecl {
316
- init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
344
+ init:
345
+ Some(Expr::NativeModuleLoad {
346
+ spec, export_name, ..
347
+ }),
317
348
  ..
318
349
  } => {
319
350
  let s = spec.as_ref();
@@ -324,8 +355,11 @@ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet
324
355
  .or_default()
325
356
  .insert(export_name.to_string());
326
357
  }
327
- Statement::Import { specifiers, from, .. } if is_native_import(from.as_ref()) => {
328
- let spec = normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
358
+ Statement::Import {
359
+ specifiers, from, ..
360
+ } if is_native_import(from.as_ref()) => {
361
+ let spec =
362
+ normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
329
363
  if is_builtin_native_spec(&spec) {
330
364
  continue;
331
365
  }
@@ -358,7 +392,11 @@ pub fn generate_native_wrapper_rs(
358
392
  );
359
393
  let mut any = false;
360
394
  for m in modules {
361
- let Some(NativeModuleInit::Generated { shim_crate, export_fn }) = init_by_spec.get(&m.spec) else {
395
+ let Some(NativeModuleInit::Generated {
396
+ shim_crate,
397
+ export_fn,
398
+ }) = init_by_spec.get(&m.spec)
399
+ else {
362
400
  continue;
363
401
  };
364
402
  let Some(names) = inferred.get(&m.spec) else {
@@ -405,9 +443,16 @@ pub fn compute_native_build_artifacts(
405
443
  let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
406
444
  for m in native_modules {
407
445
  let use_gen = if is_cargo_native_spec(&m.spec) {
408
- inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
446
+ inferred
447
+ .get(&m.spec)
448
+ .map(|s| !s.is_empty())
449
+ .unwrap_or(false)
409
450
  } else {
410
- gen_tish && inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
451
+ gen_tish
452
+ && inferred
453
+ .get(&m.spec)
454
+ .map(|s| !s.is_empty())
455
+ .unwrap_or(false)
411
456
  };
412
457
  let init = if use_gen {
413
458
  NativeModuleInit::Generated {
@@ -538,13 +583,18 @@ pub fn resolve_project(
538
583
  entry_path: &Path,
539
584
  project_root: Option<&Path>,
540
585
  ) -> Result<Vec<ResolvedModule>, String> {
541
- let project_root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
586
+ let project_root =
587
+ project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
542
588
  let entry_canon = entry_path
543
589
  .canonicalize()
544
590
  .map_err(|e| format!("Cannot canonicalize entry {}: {}", entry_path.display(), e))?;
545
- let root_canon = project_root
546
- .canonicalize()
547
- .map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
591
+ let root_canon = project_root.canonicalize().map_err(|e| {
592
+ format!(
593
+ "Cannot canonicalize project root {}: {}",
594
+ project_root.display(),
595
+ e
596
+ )
597
+ })?;
548
598
 
549
599
  let mut visited = HashSet::new();
550
600
  let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
@@ -574,21 +624,23 @@ pub fn resolve_project_from_stdin(
574
624
  source: &str,
575
625
  project_root: &Path,
576
626
  ) -> Result<Vec<ResolvedModule>, String> {
577
- let root_canon = project_root
578
- .canonicalize()
579
- .map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
627
+ let root_canon = project_root.canonicalize().map_err(|e| {
628
+ format!(
629
+ "Cannot canonicalize project root {}: {}",
630
+ project_root.display(),
631
+ e
632
+ )
633
+ })?;
580
634
 
581
635
  let stdin_path = root_canon.join("<stdin>");
582
- let program = tishlang_parser::parse(source)
583
- .map_err(|e| format!("Parse error (stdin): {}", e))?;
636
+ let program =
637
+ tishlang_parser::parse(source).map_err(|e| format!("Parse error (stdin): {}", e))?;
584
638
 
585
639
  let mut visited = HashSet::new();
586
640
  let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
587
641
  let mut load_order: Vec<PathBuf> = Vec::new();
588
642
 
589
- let from_dir = stdin_path
590
- .parent()
591
- .unwrap_or_else(|| Path::new("."));
643
+ let from_dir = stdin_path.parent().unwrap_or_else(|| Path::new("."));
592
644
 
593
645
  for stmt in &program.statements {
594
646
  if let Statement::Import { from, .. } = stmt {
@@ -787,7 +839,10 @@ pub fn detect_cycles(modules: &[ResolvedModule]) -> Result<(), String> {
787
839
  .iter()
788
840
  .map(|&i| modules[i].path.display().to_string())
789
841
  .collect();
790
- return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
842
+ return Err(format!(
843
+ "Circular import detected: {}",
844
+ path_names.join(" -> ")
845
+ ));
791
846
  }
792
847
  }
793
848
  Ok(())
@@ -817,14 +872,8 @@ fn has_cycle_from(
817
872
  stack.push(dep_idx);
818
873
  let dep = &modules[dep_idx];
819
874
  let dep_dir = dep.path.parent().unwrap_or(Path::new("."));
820
- if has_cycle_from(
821
- dep_dir,
822
- &dep.program,
823
- path_to_idx,
824
- modules,
825
- stack,
826
- visiting,
827
- )? {
875
+ if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
876
+ {
828
877
  return Ok(true);
829
878
  }
830
879
  stack.pop();
@@ -874,12 +923,15 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
874
923
  let dir = module.path.parent().unwrap_or(Path::new("."));
875
924
  for stmt in &module.program.statements {
876
925
  match stmt {
877
- Statement::Import { specifiers, from, span } => {
926
+ Statement::Import {
927
+ specifiers,
928
+ from,
929
+ span,
930
+ } => {
878
931
  if is_native_import(from.as_ref()) {
879
932
  // Normalize fs/http/process -> tish:fs etc. for Node compatibility
880
- let canonical_spec =
881
- normalize_builtin_spec(from.as_ref())
882
- .unwrap_or_else(|| from.to_string());
933
+ let canonical_spec = normalize_builtin_spec(from.as_ref())
934
+ .unwrap_or_else(|| from.to_string());
883
935
  // Emit VarDecl with NativeModuleLoad for each specifier
884
936
  for spec in specifiers {
885
937
  match spec {
@@ -918,9 +970,7 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
918
970
  continue;
919
971
  }
920
972
  let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
921
- let dep_path = dep_path
922
- .canonicalize()
923
- .unwrap_or(dep_path);
973
+ let dep_path = dep_path.canonicalize().unwrap_or(dep_path);
924
974
  let dep_idx = *path_to_idx
925
975
  .get(&dep_path)
926
976
  .ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
@@ -961,18 +1011,13 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
961
1011
  name: ns.clone(),
962
1012
  mutable: false,
963
1013
  type_ann: None,
964
- init: Some(Expr::Object {
965
- props,
966
- span: *span,
967
- }),
1014
+ init: Some(Expr::Object { props, span: *span }),
968
1015
  span: *span,
969
1016
  });
970
1017
  }
971
1018
  ImportSpecifier::Default(bind) => {
972
- let source = dep_exports
973
- .get("default")
974
- .cloned()
975
- .ok_or_else(|| {
1019
+ let source =
1020
+ dep_exports.get("default").cloned().ok_or_else(|| {
976
1021
  format!("Module '{}' has no default export", from)
977
1022
  })?;
978
1023
  statements.push(Statement::VarDecl {
@@ -989,21 +1034,19 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
989
1034
  }
990
1035
  }
991
1036
  }
992
- Statement::Export { declaration, .. } => {
993
- match declaration.as_ref() {
994
- ExportDeclaration::Named(s) => statements.push(*s.clone()),
995
- ExportDeclaration::Default(e) => {
996
- let default_name = format!("__default_{}", idx);
997
- statements.push(Statement::VarDecl {
998
- name: Arc::from(default_name),
999
- mutable: false,
1000
- type_ann: None,
1001
- init: Some((*e).clone()),
1002
- span: e.span(),
1003
- });
1004
- }
1037
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
1038
+ ExportDeclaration::Named(s) => statements.push(*s.clone()),
1039
+ ExportDeclaration::Default(e) => {
1040
+ let default_name = format!("__default_{}", idx);
1041
+ statements.push(Statement::VarDecl {
1042
+ name: Arc::from(default_name),
1043
+ mutable: false,
1044
+ type_ann: None,
1045
+ init: Some((*e).clone()),
1046
+ span: e.span(),
1047
+ });
1005
1048
  }
1006
- }
1049
+ },
1007
1050
  _ => statements.push(stmt.clone()),
1008
1051
  }
1009
1052
  }
@@ -1051,8 +1094,8 @@ mod cargo_spec_tests {
1051
1094
  #[test]
1052
1095
  fn cargo_export_fn_name_sanitizes() {
1053
1096
  assert_eq!(
1054
- cargo_export_fn_name("cargo:serde_json"),
1055
- "cargo_native_serde_json_object"
1097
+ cargo_export_fn_name("cargo:tish_serde_json"),
1098
+ "cargo_native_tish_serde_json_object"
1056
1099
  );
1057
1100
  assert_eq!(
1058
1101
  cargo_export_fn_name("cargo:my-crate"),
@@ -45,9 +45,7 @@ impl RustType {
45
45
  "any" => RustType::Value,
46
46
  _ => RustType::Value, // Unknown types fall back to Value
47
47
  },
48
- TypeAnnotation::Array(elem) => {
49
- RustType::Vec(Box::new(Self::from_annotation(elem)))
50
- }
48
+ TypeAnnotation::Array(elem) => RustType::Vec(Box::new(Self::from_annotation(elem))),
51
49
  TypeAnnotation::Object(fields) => {
52
50
  let typed_fields: Vec<_> = fields
53
51
  .iter()
@@ -66,13 +64,13 @@ impl RustType {
66
64
  TypeAnnotation::Union(types) => {
67
65
  // Check for T | null pattern -> Option<T>
68
66
  if types.len() == 2 {
69
- let has_null = types.iter().any(|t| {
70
- matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null")
71
- });
67
+ let has_null = types
68
+ .iter()
69
+ .any(|t| matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"));
72
70
  if has_null {
73
- let non_null = types.iter().find(|t| {
74
- !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null")
75
- });
71
+ let non_null = types.iter().find(
72
+ |t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
73
+ );
76
74
  if let Some(inner) = non_null {
77
75
  return RustType::Option(Box::new(Self::from_annotation(inner)));
78
76
  }
@@ -271,9 +269,7 @@ impl TypeContext {
271
269
 
272
270
  /// Check if a variable is typed (has a non-Value type).
273
271
  pub fn is_typed(&self, name: &str) -> bool {
274
- self.lookup(name)
275
- .map(|ty| ty.is_native())
276
- .unwrap_or(false)
272
+ self.lookup(name).map(|ty| ty.is_native()).unwrap_or(false)
277
273
  }
278
274
 
279
275
  /// Get the type of a variable, defaulting to Value if not found.
@@ -329,12 +325,12 @@ mod tests {
329
325
  ctx.define("x", RustType::F64);
330
326
  assert_eq!(ctx.get_type("x"), RustType::F64);
331
327
  assert!(ctx.is_typed("x"));
332
-
328
+
333
329
  ctx.push_scope();
334
330
  ctx.define("y", RustType::String);
335
331
  assert_eq!(ctx.get_type("y"), RustType::String);
336
332
  assert_eq!(ctx.get_type("x"), RustType::F64); // Can still see outer scope
337
-
333
+
338
334
  ctx.pop_scope();
339
335
  assert_eq!(ctx.get_type("y"), RustType::Value); // y no longer visible
340
336
  }