@tishlang/tish 1.5.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 (85) 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 +67 -0
  10. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  11. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  12. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  13. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  14. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  15. package/crates/tish/tests/integration_test.rs +197 -47
  16. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  17. package/crates/tish/tests/shortcircuit.rs +19 -4
  18. package/crates/tish_ast/src/ast.rs +12 -14
  19. package/crates/tish_build_utils/src/lib.rs +64 -6
  20. package/crates/tish_builtins/src/array.rs +52 -21
  21. package/crates/tish_builtins/src/construct.rs +2 -8
  22. package/crates/tish_builtins/src/globals.rs +30 -15
  23. package/crates/tish_builtins/src/lib.rs +5 -5
  24. package/crates/tish_builtins/src/math.rs +5 -3
  25. package/crates/tish_builtins/src/string.rs +71 -19
  26. package/crates/tish_bytecode/src/chunk.rs +0 -1
  27. package/crates/tish_bytecode/src/compiler.rs +164 -60
  28. package/crates/tish_bytecode/src/opcode.rs +13 -4
  29. package/crates/tish_bytecode/src/peephole.rs +2 -2
  30. package/crates/tish_compile/Cargo.toml +1 -0
  31. package/crates/tish_compile/src/codegen.rs +989 -318
  32. package/crates/tish_compile/src/infer.rs +69 -19
  33. package/crates/tish_compile/src/lib.rs +21 -8
  34. package/crates/tish_compile/src/resolve.rs +515 -94
  35. package/crates/tish_compile/src/types.rs +10 -14
  36. package/crates/tish_compile_js/src/codegen.rs +34 -13
  37. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  38. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  39. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +40 -48
  40. package/crates/tish_core/src/json.rs +5 -3
  41. package/crates/tish_core/src/lib.rs +1 -1
  42. package/crates/tish_core/src/uri.rs +9 -6
  43. package/crates/tish_core/src/value.rs +92 -28
  44. package/crates/tish_cranelift/src/link.rs +6 -9
  45. package/crates/tish_cranelift/src/lower.rs +14 -8
  46. package/crates/tish_eval/src/eval.rs +398 -141
  47. package/crates/tish_eval/src/lib.rs +10 -6
  48. package/crates/tish_eval/src/natives.rs +95 -38
  49. package/crates/tish_eval/src/promise.rs +14 -8
  50. package/crates/tish_eval/src/timers.rs +28 -19
  51. package/crates/tish_eval/src/value.rs +10 -3
  52. package/crates/tish_fmt/src/lib.rs +29 -13
  53. package/crates/tish_lexer/src/lib.rs +217 -63
  54. package/crates/tish_lexer/src/token.rs +6 -6
  55. package/crates/tish_llvm/src/lib.rs +15 -8
  56. package/crates/tish_lsp/src/main.rs +41 -43
  57. package/crates/tish_native/src/build.rs +38 -15
  58. package/crates/tish_native/src/lib.rs +76 -32
  59. package/crates/tish_opt/src/lib.rs +67 -50
  60. package/crates/tish_parser/src/lib.rs +36 -11
  61. package/crates/tish_parser/src/parser.rs +172 -87
  62. package/crates/tish_runtime/src/http.rs +15 -6
  63. package/crates/tish_runtime/src/http_fetch.rs +24 -14
  64. package/crates/tish_runtime/src/lib.rs +224 -168
  65. package/crates/tish_runtime/src/promise.rs +1 -5
  66. package/crates/tish_runtime/src/ws.rs +45 -20
  67. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  68. package/crates/tish_ui/src/jsx.rs +41 -22
  69. package/crates/tish_ui/src/lib.rs +2 -2
  70. package/crates/tish_vm/src/vm.rs +320 -116
  71. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
  72. package/crates/tish_wasm/src/lib.rs +38 -28
  73. package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
  74. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  75. package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
  76. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  77. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  78. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  79. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  80. package/package.json +1 -1
  81. package/platform/darwin-arm64/tish +0 -0
  82. package/platform/darwin-x64/tish +0 -0
  83. package/platform/linux-arm64/tish +0 -0
  84. package/platform/linux-x64/tish +0 -0
  85. package/platform/win32-x64/tish.exe +0 -0
@@ -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
  }
@@ -16,7 +16,9 @@ struct Codegen {
16
16
  fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
17
17
  matches!(
18
18
  stmt,
19
- Some(Statement::Break { .. }) | Some(Statement::Return { .. }) | Some(Statement::Throw { .. })
19
+ Some(Statement::Break { .. })
20
+ | Some(Statement::Return { .. })
21
+ | Some(Statement::Throw { .. })
20
22
  )
21
23
  }
22
24
 
@@ -174,7 +176,12 @@ impl Codegen {
174
176
  self.emit_statement(body)?;
175
177
  self.indent -= 1;
176
178
  }
177
- Statement::ForOf { name, iterable, body, .. } => {
179
+ Statement::ForOf {
180
+ name,
181
+ iterable,
182
+ body,
183
+ ..
184
+ } => {
178
185
  let escaped = Self::escape_ident(name.as_ref());
179
186
  let it = self.emit_expr(iterable)?;
180
187
  self.writeln(&format!("for (const {} of {})", escaped, it));
@@ -204,7 +211,10 @@ impl Codegen {
204
211
  let async_prefix = if *async_ { "async " } else { "" };
205
212
  let escaped = Self::escape_ident(name.as_ref());
206
213
  let params_str = self.emit_params(params, rest_param.as_ref())?;
207
- self.writeln(&format!("{}function {} ({}) {{", async_prefix, escaped, params_str));
214
+ self.writeln(&format!(
215
+ "{}function {} ({}) {{",
216
+ async_prefix, escaped, params_str
217
+ ));
208
218
  self.indent += 1;
209
219
  if *async_ {
210
220
  self.in_async = true;
@@ -337,9 +347,7 @@ impl Codegen {
337
347
  let parts: Vec<String> = elements
338
348
  .iter()
339
349
  .map(|el| match el {
340
- Some(DestructElement::Ident(n)) => {
341
- Ok(Self::escape_ident(n.as_ref()))
342
- }
350
+ Some(DestructElement::Ident(n)) => Ok(Self::escape_ident(n.as_ref())),
343
351
  Some(DestructElement::Pattern(p)) => self.emit_destruct_pattern(p),
344
352
  Some(DestructElement::Rest(n)) => {
345
353
  Ok(format!("...{}", Self::escape_ident(n.as_ref())))
@@ -385,7 +393,9 @@ impl Codegen {
385
393
  Literal::Null => "null".to_string(),
386
394
  },
387
395
  Expr::Ident { name, .. } => Self::escape_ident(name.as_ref()),
388
- Expr::Binary { left, op, right, .. } => {
396
+ Expr::Binary {
397
+ left, op, right, ..
398
+ } => {
389
399
  let l = self.emit_expr(left)?;
390
400
  let r = self.emit_expr(right)?;
391
401
  let op_str = match op {
@@ -451,7 +461,9 @@ impl Codegen {
451
461
  let obj = self.emit_expr(object)?;
452
462
  let expr = match prop {
453
463
  MemberProp::Name(p) => {
454
- if p.parse::<u32>().is_ok() || !p.chars().all(|c| c.is_alphanumeric() || c == '_') {
464
+ if p.parse::<u32>().is_ok()
465
+ || !p.chars().all(|c| c.is_alphanumeric() || c == '_')
466
+ {
455
467
  format!("{}[{:?}]", obj, p.as_ref())
456
468
  } else {
457
469
  let sep = if *optional { "?." } else { "." };
@@ -548,7 +560,9 @@ impl Codegen {
548
560
  Expr::PrefixDec { name, .. } => {
549
561
  format!("--{}", Self::escape_ident(name.as_ref()))
550
562
  }
551
- Expr::CompoundAssign { name, op, value, .. } => {
563
+ Expr::CompoundAssign {
564
+ name, op, value, ..
565
+ } => {
552
566
  let n = Self::escape_ident(name.as_ref());
553
567
  let v = self.emit_expr(value)?;
554
568
  let op_str = match op {
@@ -560,7 +574,9 @@ impl Codegen {
560
574
  };
561
575
  format!("({} {} {})", n, op_str, v)
562
576
  }
563
- Expr::LogicalAssign { name, op, value, .. } => {
577
+ Expr::LogicalAssign {
578
+ name, op, value, ..
579
+ } => {
564
580
  let n = Self::escape_ident(name.as_ref());
565
581
  let v = self.emit_expr(value)?;
566
582
  let op_str = match op {
@@ -570,7 +586,12 @@ impl Codegen {
570
586
  };
571
587
  format!("({} {} {})", n, op_str, v)
572
588
  }
573
- Expr::MemberAssign { object, prop, value, .. } => {
589
+ Expr::MemberAssign {
590
+ object,
591
+ prop,
592
+ value,
593
+ ..
594
+ } => {
574
595
  let obj = self.emit_expr(object)?;
575
596
  let val = self.emit_expr(value)?;
576
597
  format!("({}.{} = {})", obj, prop.as_ref(), val)
@@ -652,7 +673,6 @@ impl Codegen {
652
673
  CallArg::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
653
674
  }
654
675
  }
655
-
656
676
  }
657
677
 
658
678
  /// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
@@ -679,7 +699,8 @@ pub fn compile_project_with_jsx(
679
699
  .map_err(|e| CompileError { message: e })?;
680
700
  tishlang_compile::detect_cycles(&modules).map_err(|e| CompileError { message: e })?;
681
701
  let program = {
682
- let prog = tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
702
+ let prog =
703
+ tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
683
704
  if optimize {
684
705
  tishlang_opt::optimize(&prog)
685
706
  } else {
@@ -11,7 +11,11 @@ mod tests {
11
11
  let src = r#"fn X() { return <div class="a">{"hi"}</div> }"#;
12
12
  let program = parse(src).unwrap();
13
13
  let js = compile_with_jsx(&program, false).unwrap();
14
- assert!(js.contains("h(\"div\", { class: \"a\" }, [\"hi\"])"), "{}", js);
14
+ assert!(
15
+ js.contains("h(\"div\", { class: \"a\" }, [\"hi\"])"),
16
+ "{}",
17
+ js
18
+ );
15
19
  assert!(!js.contains("function __h("));
16
20
  }
17
21
 
@@ -57,7 +61,11 @@ mod tests {
57
61
  let src = r#"fn X() { return <p>work!</p> }"#;
58
62
  let program = parse(src).unwrap();
59
63
  let js = compile_with_jsx(&program, false).unwrap();
60
- assert!(js.contains(r#""work!""#), "expected 'work!', got: {}", &js[..400.min(js.len())]);
64
+ assert!(
65
+ js.contains(r#""work!""#),
66
+ "expected 'work!', got: {}",
67
+ &js[..400.min(js.len())]
68
+ );
61
69
  }
62
70
 
63
71
  #[test]
@@ -65,7 +73,11 @@ mod tests {
65
73
  let src = r#"fn X() { return <p>hello 😔</p> }"#;
66
74
  let program = parse(src).unwrap();
67
75
  let js = compile_with_jsx(&program, false).unwrap();
68
- assert!(js.contains("😔"), "expected emoji, got: {}", &js[..400.min(js.len())]);
76
+ assert!(
77
+ js.contains("😔"),
78
+ "expected emoji, got: {}",
79
+ &js[..400.min(js.len())]
80
+ );
69
81
  }
70
82
 
71
83
  #[test]
@@ -93,10 +105,22 @@ mod tests {
93
105
  let src = r#"fn X() { return <div class="x">{"a"}</div> }"#;
94
106
  let program = parse(src).unwrap();
95
107
  let js = compile_with_jsx(&program, false).unwrap();
96
- assert!(js.contains("h(\"div\", { class: \"x\" }"), "{}", &js[..500.min(js.len())]);
108
+ assert!(
109
+ js.contains("h(\"div\", { class: \"x\" }"),
110
+ "{}",
111
+ &js[..500.min(js.len())]
112
+ );
97
113
  assert!(!js.contains("__vdom_h"), "{}", &js[..600.min(js.len())]);
98
- assert!(!js.contains("window.__LATTISH_JSX_VDOM"), "{}", &js[..600.min(js.len())]);
99
- assert!(!js.contains("__lattishVdomPatch"), "{}", &js[..600.min(js.len())]);
114
+ assert!(
115
+ !js.contains("window.__LATTISH_JSX_VDOM"),
116
+ "{}",
117
+ &js[..600.min(js.len())]
118
+ );
119
+ assert!(
120
+ !js.contains("__lattishVdomPatch"),
121
+ "{}",
122
+ &js[..600.min(js.len())]
123
+ );
100
124
  }
101
125
 
102
126
  /// Component calls like {Panel()} return DOM elements. Wrapping in String() produces [object HTMLDivElement].
@@ -14,29 +14,34 @@ use wasm_bindgen::prelude::*;
14
14
 
15
15
  #[wasm_bindgen]
16
16
  pub fn compile_to_bytecode(source: &str) -> Result<String, JsValue> {
17
- let program = tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
17
+ let program =
18
+ tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
18
19
  let program = tishlang_opt::optimize(&program);
19
- let chunk = tishlang_bytecode::compile(&program).map_err(|e| JsValue::from_str(&e.to_string()))?;
20
+ let chunk =
21
+ tishlang_bytecode::compile(&program).map_err(|e| JsValue::from_str(&e.to_string()))?;
20
22
  Ok(base64::engine::general_purpose::STANDARD.encode(tishlang_bytecode::serialize(&chunk)))
21
23
  }
22
24
 
23
25
  #[wasm_bindgen]
24
26
  pub fn compile_to_js(source: &str) -> Result<String, JsValue> {
25
- let program = tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
26
- tishlang_compile_js::compile_with_jsx(&program, true)
27
- .map_err(|e| JsValue::from_str(&e.message))
27
+ let program =
28
+ tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
29
+ tishlang_compile_js::compile_with_jsx(&program, true).map_err(|e| JsValue::from_str(&e.message))
28
30
  }
29
31
 
30
32
  #[wasm_bindgen]
31
- pub fn compile_to_bytecode_with_imports(entry_path: &str, files_json: &str) -> Result<String, JsValue> {
33
+ pub fn compile_to_bytecode_with_imports(
34
+ entry_path: &str,
35
+ files_json: &str,
36
+ ) -> Result<String, JsValue> {
32
37
  let files: HashMap<String, String> = serde_json::from_str(files_json)
33
38
  .map_err(|e| JsValue::from_str(&format!("Invalid files JSON: {}", e)))?;
34
- let modules = resolve_virtual(entry_path, &files)
35
- .map_err(|e| JsValue::from_str(&e))?;
39
+ let modules = resolve_virtual(entry_path, &files).map_err(|e| JsValue::from_str(&e))?;
36
40
  detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
37
41
  let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
38
42
  let program = tishlang_opt::optimize(&program);
39
- let chunk = tishlang_bytecode::compile(&program).map_err(|e| JsValue::from_str(&e.to_string()))?;
43
+ let chunk =
44
+ tishlang_bytecode::compile(&program).map_err(|e| JsValue::from_str(&e.to_string()))?;
40
45
  Ok(base64::engine::general_purpose::STANDARD.encode(tishlang_bytecode::serialize(&chunk)))
41
46
  }
42
47
 
@@ -44,11 +49,9 @@ pub fn compile_to_bytecode_with_imports(entry_path: &str, files_json: &str) -> R
44
49
  pub fn compile_to_js_with_imports(entry_path: &str, files_json: &str) -> Result<String, JsValue> {
45
50
  let files: HashMap<String, String> = serde_json::from_str(files_json)
46
51
  .map_err(|e| JsValue::from_str(&format!("Invalid files JSON: {}", e)))?;
47
- let modules = resolve_virtual(entry_path, &files)
48
- .map_err(|e| JsValue::from_str(&e))?;
52
+ let modules = resolve_virtual(entry_path, &files).map_err(|e| JsValue::from_str(&e))?;
49
53
  detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
50
54
  let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
51
55
  let program = tishlang_opt::optimize(&program);
52
- tishlang_compile_js::compile_with_jsx(&program, true)
53
- .map_err(|e| JsValue::from_str(&e.message))
56
+ tishlang_compile_js::compile_with_jsx(&program, true).map_err(|e| JsValue::from_str(&e.message))
54
57
  }
@@ -33,6 +33,7 @@ fn normalize_builtin_spec(spec: &str) -> Option<String> {
33
33
 
34
34
  fn is_native_import(spec: &str) -> bool {
35
35
  spec.starts_with("tish:")
36
+ || spec.starts_with("cargo:")
36
37
  || spec.starts_with('@')
37
38
  || matches!(spec, "fs" | "http" | "process" | "ws")
38
39
  }
@@ -108,10 +109,16 @@ pub fn resolve_virtual(
108
109
  if files.contains_key(&with_ext) {
109
110
  with_ext
110
111
  } else {
111
- return Err(format!("Entry file '{}' not in virtual file map", entry_path));
112
+ return Err(format!(
113
+ "Entry file '{}' not in virtual file map",
114
+ entry_path
115
+ ));
112
116
  }
113
117
  } else {
114
- return Err(format!("Entry file '{}' not in virtual file map", entry_path));
118
+ return Err(format!(
119
+ "Entry file '{}' not in virtual file map",
120
+ entry_path
121
+ ));
115
122
  };
116
123
 
117
124
  let mut visited = HashSet::new();
@@ -147,9 +154,9 @@ fn load_module_recursive(
147
154
  }
148
155
  visited.insert(module_path.to_string());
149
156
 
150
- let source = files.get(module_path).ok_or_else(|| {
151
- format!("Module '{}' not in virtual file map", module_path)
152
- })?;
157
+ let source = files
158
+ .get(module_path)
159
+ .ok_or_else(|| format!("Module '{}' not in virtual file map", module_path))?;
153
160
  let program = tishlang_parser::parse(source.trim())
154
161
  .map_err(|e| format!("Parse error in {}: {}", module_path, e))?;
155
162
 
@@ -161,13 +168,7 @@ fn load_module_recursive(
161
168
  }
162
169
  let dep_key = resolve_import_to_key(from, from_dir, files)?;
163
170
  if !path_to_module.contains_key(&dep_key) {
164
- load_module_recursive(
165
- &dep_key,
166
- files,
167
- visited,
168
- path_to_module,
169
- load_order,
170
- )?;
171
+ load_module_recursive(&dep_key, files, visited, path_to_module, load_order)?;
171
172
  }
172
173
  }
173
174
  }
@@ -196,11 +197,11 @@ pub fn detect_cycles_virtual(modules: &[VirtualModule]) -> Result<(), String> {
196
197
  &mut stack,
197
198
  &mut HashSet::new(),
198
199
  )? {
199
- let path_names: Vec<_> = stack
200
- .iter()
201
- .map(|&i| modules[i].path.clone())
202
- .collect();
203
- return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
200
+ let path_names: Vec<_> = stack.iter().map(|&i| modules[i].path.clone()).collect();
201
+ return Err(format!(
202
+ "Circular import detected: {}",
203
+ path_names.join(" -> ")
204
+ ));
204
205
  }
205
206
  }
206
207
  Ok(())
@@ -230,14 +231,8 @@ fn has_cycle_from(
230
231
  stack.push(dep_idx);
231
232
  let dep = &modules[dep_idx];
232
233
  let dep_dir = parent_dir(&dep.path);
233
- if has_cycle_from(
234
- dep_dir,
235
- &dep.program,
236
- path_to_idx,
237
- modules,
238
- stack,
239
- visiting,
240
- )? {
234
+ if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
235
+ {
241
236
  return Ok(true);
242
237
  }
243
238
  stack.pop();
@@ -306,7 +301,11 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
306
301
  let dir = parent_dir(&module.path);
307
302
  for stmt in &module.program.statements {
308
303
  match stmt {
309
- Statement::Import { specifiers, from, span } => {
304
+ Statement::Import {
305
+ specifiers,
306
+ from,
307
+ span,
308
+ } => {
310
309
  if is_native_import(from) {
311
310
  let canonical_spec =
312
311
  normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
@@ -386,18 +385,13 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
386
385
  name: ns.clone(),
387
386
  mutable: false,
388
387
  type_ann: None,
389
- init: Some(Expr::Object {
390
- props,
391
- span: *span,
392
- }),
388
+ init: Some(Expr::Object { props, span: *span }),
393
389
  span: *span,
394
390
  });
395
391
  }
396
392
  ImportSpecifier::Default(bind) => {
397
- let source = dep_exports
398
- .get("default")
399
- .cloned()
400
- .ok_or_else(|| {
393
+ let source =
394
+ dep_exports.get("default").cloned().ok_or_else(|| {
401
395
  format!("Module '{}' has no default export", from)
402
396
  })?;
403
397
  statements.push(Statement::VarDecl {
@@ -414,21 +408,19 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
414
408
  }
415
409
  }
416
410
  }
417
- Statement::Export { declaration, .. } => {
418
- match declaration.as_ref() {
419
- ExportDeclaration::Named(s) => statements.push(*s.clone()),
420
- ExportDeclaration::Default(e) => {
421
- let default_name = format!("__default_{}", idx);
422
- statements.push(Statement::VarDecl {
423
- name: Arc::from(default_name),
424
- mutable: false,
425
- type_ann: None,
426
- init: Some((*e).clone()),
427
- span: e.span(),
428
- });
429
- }
411
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
412
+ ExportDeclaration::Named(s) => statements.push(*s.clone()),
413
+ ExportDeclaration::Default(e) => {
414
+ let default_name = format!("__default_{}", idx);
415
+ statements.push(Statement::VarDecl {
416
+ name: Arc::from(default_name),
417
+ mutable: false,
418
+ type_ann: None,
419
+ init: Some((*e).clone()),
420
+ span: e.span(),
421
+ });
430
422
  }
431
- }
423
+ },
432
424
  _ => statements.push(stmt.clone()),
433
425
  }
434
426
  }
@@ -228,7 +228,7 @@ fn parse_number(input: &str) -> Result<(Value, &str), String> {
228
228
 
229
229
  let num_str: String = chars[..end].iter().collect();
230
230
  let byte_len: usize = chars[..end].iter().map(|c| c.len_utf8()).sum();
231
-
231
+
232
232
  num_str
233
233
  .parse::<f64>()
234
234
  .map(|n| (Value::Number(n), &input[byte_len..]))
@@ -306,7 +306,9 @@ mod tests {
306
306
  assert!(matches!(json_parse("true").unwrap(), Value::Bool(true)));
307
307
  assert!(matches!(json_parse("false").unwrap(), Value::Bool(false)));
308
308
  assert!(matches!(json_parse("42").unwrap(), Value::Number(n) if n == 42.0));
309
- assert!(matches!(json_parse("\"hello\"").unwrap(), Value::String(s) if s.as_ref() == "hello"));
309
+ assert!(
310
+ matches!(json_parse("\"hello\"").unwrap(), Value::String(s) if s.as_ref() == "hello")
311
+ );
310
312
  }
311
313
 
312
314
  #[test]
@@ -315,7 +317,7 @@ mod tests {
315
317
  let value = json_parse(original).unwrap();
316
318
  let stringified = json_stringify(&value);
317
319
  let reparsed = json_parse(&stringified).unwrap();
318
-
320
+
319
321
  match (&value, &reparsed) {
320
322
  (Value::Object(a), Value::Object(b)) => {
321
323
  assert_eq!(a.borrow().len(), b.borrow().len());
@@ -10,6 +10,6 @@ mod uri;
10
10
  mod value;
11
11
 
12
12
  pub use console_style::{format_value_styled, format_values_for_console, use_console_colors};
13
- pub use value::*;
14
13
  pub use json::{json_parse, json_stringify};
15
14
  pub use uri::{percent_decode, percent_encode};
15
+ pub use value::*;
@@ -10,13 +10,13 @@ pub fn percent_decode(input: &str) -> Result<String, String> {
10
10
  "%2F", "%2f", // /
11
11
  "%3F", "%3f", // ?
12
12
  "%3A", "%3a", // :
13
- "%40", // @
14
- "%26", // &
13
+ "%40", // @
14
+ "%26", // &
15
15
  "%3D", "%3d", // =
16
16
  "%2B", "%2b", // +
17
- "%24", // $
17
+ "%24", // $
18
18
  "%2C", "%2c", // ,
19
- "%23", // #
19
+ "%23", // #
20
20
  ];
21
21
 
22
22
  let mut result = String::with_capacity(input.len());
@@ -46,11 +46,14 @@ pub fn percent_decode(input: &str) -> Result<String, String> {
46
46
  None => return Err("URIError: malformed URI sequence".to_string()),
47
47
  }
48
48
  }
49
-
49
+
50
50
  if hex.len() == 2 {
51
51
  let encoded = format!("%{}", hex);
52
52
  // Check if this is a reserved character that should NOT be decoded
53
- if RESERVED_ENCODED.iter().any(|r| r.eq_ignore_ascii_case(&encoded)) {
53
+ if RESERVED_ENCODED
54
+ .iter()
55
+ .any(|r| r.eq_ignore_ascii_case(&encoded))
56
+ {
54
57
  result.push_str(&encoded);
55
58
  } else if let Ok(byte) = u8::from_str_radix(&hex, 16) {
56
59
  result.push(byte as char);