@tishlang/tish 1.0.28 → 1.0.33

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 (65) hide show
  1. package/Cargo.toml +1 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +15 -6
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/main.rs +8 -55
  5. package/crates/tish/tests/integration_test.rs +4 -3
  6. package/crates/tish_ast/src/ast.rs +65 -2
  7. package/crates/tish_build_utils/src/lib.rs +10 -2
  8. package/crates/tish_builtins/src/construct.rs +177 -0
  9. package/crates/tish_builtins/src/globals.rs +3 -5
  10. package/crates/tish_builtins/src/helpers.rs +2 -3
  11. package/crates/tish_builtins/src/lib.rs +1 -0
  12. package/crates/tish_builtins/src/object.rs +3 -4
  13. package/crates/tish_bytecode/src/compiler.rs +85 -11
  14. package/crates/tish_bytecode/src/opcode.rs +7 -3
  15. package/crates/tish_compile/Cargo.toml +1 -0
  16. package/crates/tish_compile/src/codegen.rs +233 -71
  17. package/crates/tish_compile/src/lib.rs +35 -0
  18. package/crates/tish_compile_js/Cargo.toml +1 -1
  19. package/crates/tish_compile_js/src/codegen.rs +43 -147
  20. package/crates/tish_compile_js/src/lib.rs +4 -7
  21. package/crates/tish_compile_js/src/tests_jsx.rs +89 -19
  22. package/crates/tish_compiler_wasm/src/lib.rs +2 -3
  23. package/crates/tish_core/Cargo.toml +4 -0
  24. package/crates/tish_core/src/console_style.rs +7 -1
  25. package/crates/tish_core/src/json.rs +1 -2
  26. package/crates/tish_core/src/macros.rs +2 -3
  27. package/crates/tish_core/src/value.rs +10 -5
  28. package/crates/tish_eval/Cargo.toml +2 -0
  29. package/crates/tish_eval/src/eval.rs +149 -72
  30. package/crates/tish_eval/src/http.rs +3 -4
  31. package/crates/tish_eval/src/regex.rs +3 -2
  32. package/crates/tish_eval/src/value.rs +11 -13
  33. package/crates/tish_eval/src/value_convert.rs +4 -8
  34. package/crates/tish_fmt/src/lib.rs +49 -10
  35. package/crates/tish_jsx_web/Cargo.toml +1 -1
  36. package/crates/tish_jsx_web/README.md +3 -16
  37. package/crates/tish_jsx_web/src/lib.rs +2 -157
  38. package/crates/tish_lexer/src/token.rs +2 -0
  39. package/crates/tish_lint/src/lib.rs +9 -0
  40. package/crates/tish_lsp/README.md +1 -1
  41. package/crates/tish_native/src/build.rs +16 -2
  42. package/crates/tish_opt/src/lib.rs +15 -0
  43. package/crates/tish_parser/src/lib.rs +101 -1
  44. package/crates/tish_parser/src/parser.rs +161 -50
  45. package/crates/tish_runtime/src/http.rs +4 -5
  46. package/crates/tish_runtime/src/http_fetch.rs +9 -10
  47. package/crates/tish_runtime/src/lib.rs +9 -2
  48. package/crates/tish_runtime/src/promise.rs +2 -3
  49. package/crates/tish_runtime/src/promise_io.rs +2 -3
  50. package/crates/tish_runtime/src/ws.rs +7 -7
  51. package/crates/tish_ui/Cargo.toml +17 -0
  52. package/crates/tish_ui/src/jsx.rs +390 -0
  53. package/crates/tish_ui/src/lib.rs +16 -0
  54. package/crates/tish_ui/src/runtime/hooks.rs +122 -0
  55. package/crates/tish_ui/src/runtime/mod.rs +173 -0
  56. package/crates/tish_vm/src/vm.rs +121 -27
  57. package/justfile +3 -3
  58. package/package.json +1 -1
  59. package/platform/darwin-arm64/tish +0 -0
  60. package/platform/darwin-x64/tish +0 -0
  61. package/platform/linux-arm64/tish +0 -0
  62. package/platform/linux-x64/tish +0 -0
  63. package/platform/win32-x64/tish.exe +0 -0
  64. package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
  65. package/crates/tish_jsx_web/vendor/Lattish.tish +0 -362
@@ -10,8 +10,8 @@ repository = { workspace = true }
10
10
  default = []
11
11
 
12
12
  [dependencies]
13
- tishlang_jsx_web = { path = "../tish_jsx_web", version = ">=0.1" }
14
13
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
15
14
  tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
16
15
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
17
16
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
17
+ tishlang_ui = { path = "../tish_ui", default-features = false, features = ["compiler"] }
@@ -2,30 +2,15 @@
2
2
 
3
3
  use tishlang_ast::{
4
4
  ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
5
- JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program,
6
- Statement, UnaryOp,
5
+ FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, UnaryOp,
7
6
  };
8
7
 
9
8
  use crate::error::CompileError;
10
- use crate::js_intrinsics::{JsIntrinsic, JsIntrinsics};
11
-
12
- /// How JSX lowers for `--target js`. Native targets never use this.
13
- #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14
- pub enum JsxMode {
15
- /// `h(tag, props, [children])` + `Fragment` (Lattish.tish). Import `h` / `Fragment` from lattish.
16
- #[default]
17
- LattishH,
18
- /// Vnode `__vdom_h` + `window.__lattishVdomPatch` (Lattish createRoot must use patch).
19
- Vdom,
20
- }
21
9
 
22
10
  struct Codegen {
23
11
  output: String,
24
12
  indent: usize,
25
13
  in_async: bool,
26
- uses_jsx: bool,
27
- intrinsics: JsIntrinsics,
28
- jsx_mode: JsxMode,
29
14
  }
30
15
 
31
16
  fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
@@ -36,14 +21,11 @@ fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
36
21
  }
37
22
 
38
23
  impl Codegen {
39
- fn new(jsx_mode: JsxMode) -> Self {
24
+ fn new() -> Self {
40
25
  Self {
41
26
  output: String::new(),
42
27
  indent: 0,
43
28
  in_async: false,
44
- uses_jsx: false,
45
- intrinsics: JsIntrinsics::new(),
46
- jsx_mode,
47
29
  }
48
30
  }
49
31
 
@@ -72,25 +54,9 @@ impl Codegen {
72
54
 
73
55
  fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
74
56
  self.write("// Generated by tishlang_compile_js\n");
75
- // First pass: check if JSX is used (we'll set uses_jsx during emit)
76
57
  for stmt in &program.statements {
77
58
  self.emit_statement(stmt)?;
78
59
  }
79
- self.output = self
80
- .intrinsics
81
- .prepend_runtime_preamble(std::mem::take(&mut self.output));
82
- if self.uses_jsx {
83
- match self.jsx_mode {
84
- JsxMode::Vdom => {
85
- self.output = format!(
86
- "{}\n{}",
87
- tishlang_jsx_web::VDOM_PRELUDE,
88
- self.output
89
- );
90
- }
91
- JsxMode::LattishH => {}
92
- }
93
- }
94
60
  Ok(())
95
61
  }
96
62
 
@@ -331,20 +297,34 @@ impl Codegen {
331
297
 
332
298
  fn emit_params(
333
299
  &mut self,
334
- params: &[tishlang_ast::TypedParam],
300
+ params: &[FunParam],
335
301
  rest_param: Option<&tishlang_ast::TypedParam>,
336
302
  ) -> Result<String, CompileError> {
337
- let mut parts: Vec<String> = params
338
- .iter()
339
- .map(|p| {
340
- let n = Self::escape_ident(p.name.as_ref());
341
- if let Some(ref d) = p.default {
342
- format!("{} = {}", n, self.emit_expr(d).unwrap())
343
- } else {
344
- n
303
+ let mut parts: Vec<String> = Vec::new();
304
+ for p in params {
305
+ match p {
306
+ FunParam::Simple(tp) => {
307
+ let n = Self::escape_ident(tp.name.as_ref());
308
+ let s = if let Some(ref d) = tp.default {
309
+ format!("{} = {}", n, self.emit_expr(d)?)
310
+ } else {
311
+ n
312
+ };
313
+ parts.push(s);
345
314
  }
346
- })
347
- .collect();
315
+ FunParam::Destructure {
316
+ pattern,
317
+ type_ann: _,
318
+ default,
319
+ } => {
320
+ let mut s = self.emit_destruct_pattern(pattern)?;
321
+ if let Some(ref d) = default {
322
+ s = format!("{} = {}", s, self.emit_expr(d)?);
323
+ }
324
+ parts.push(s);
325
+ }
326
+ }
327
+ }
348
328
  if let Some(rest) = rest_param {
349
329
  parts.push(format!("...{}", Self::escape_ident(rest.name.as_ref())));
350
330
  }
@@ -448,16 +428,6 @@ impl Codegen {
448
428
  }
449
429
  }
450
430
  Expr::Call { callee, args, .. } => {
451
- if let Some(kind) =
452
- JsIntrinsics::classify_call(callee.as_ref(), args)?
453
- {
454
- self.intrinsics.mark(kind);
455
- if kind == JsIntrinsic::Uint8Array {
456
- let n = self.emit_call_arg(&args[0])?;
457
- return Ok(JsIntrinsics::emit_expr(kind, &n));
458
- }
459
- return Ok(JsIntrinsics::emit_expr(kind, ""));
460
- }
461
431
  let c = self.emit_expr(callee)?;
462
432
  let arg_strs: Result<Vec<_>, _> =
463
433
  args.iter().map(|a| self.emit_call_arg(a)).collect();
@@ -465,6 +435,13 @@ impl Codegen {
465
435
  // Tish uses null for undefined (e.g. empty array pop/shift)
466
436
  format!("({}({}) ?? null)", c, arg_strs)
467
437
  }
438
+ Expr::New { callee, args, .. } => {
439
+ let c = self.emit_expr(callee)?;
440
+ let arg_strs: Result<Vec<_>, _> =
441
+ args.iter().map(|a| self.emit_call_arg(a)).collect();
442
+ let arg_strs = arg_strs?.join(", ");
443
+ format!("(new {}({}) ?? null)", c, arg_strs)
444
+ }
468
445
  Expr::Member {
469
446
  object,
470
447
  prop,
@@ -652,39 +629,11 @@ impl Codegen {
652
629
  let o = self.emit_expr(operand)?;
653
630
  format!("(await {})", o)
654
631
  }
655
- Expr::JsxElement { tag, props, children, .. } => {
656
- self.uses_jsx = true;
657
- let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
658
- tag.as_ref().to_string()
659
- } else {
660
- format!("{:?}", tag.as_ref())
661
- };
662
- let props_str = self.emit_jsx_props(props)?;
663
- let children_strs: Result<Vec<_>, _> =
664
- children.iter().map(|c| self.emit_jsx_child(c)).collect();
665
- let children_str = children_strs?.join(", ");
666
- match self.jsx_mode {
667
- JsxMode::LattishH => {
668
- format!("h({}, {}, [{}])", tag_str, props_str, children_str)
669
- }
670
- JsxMode::Vdom => {
671
- format!("__vdom_h({}, {}, [{}])", tag_str, props_str, children_str)
672
- }
673
- }
674
- }
675
- Expr::JsxFragment { children, .. } => {
676
- self.uses_jsx = true;
677
- let children_strs: Result<Vec<_>, _> =
678
- children.iter().map(|c| self.emit_jsx_child(c)).collect();
679
- let children_str = children_strs?.join(", ");
680
- match self.jsx_mode {
681
- JsxMode::LattishH => {
682
- format!("h(Fragment, null, [{}])", children_str)
683
- }
684
- JsxMode::Vdom => {
685
- format!("__vdom_h(__Fragment, null, [{}])", children_str)
686
- }
687
- }
632
+ Expr::JsxElement { .. } | Expr::JsxFragment { .. } => {
633
+ tishlang_ui::jsx::emit_jsx_js(expr, &mut |e| {
634
+ self.emit_expr(e).map_err(|ce| ce.message)
635
+ })
636
+ .map_err(|m| CompileError { message: m })?
688
637
  }
689
638
  Expr::NativeModuleLoad { spec, .. } => {
690
639
  return Err(CompileError {
@@ -704,68 +653,16 @@ impl Codegen {
704
653
  }
705
654
  }
706
655
 
707
- fn emit_jsx_props(&mut self, props: &[JsxProp]) -> Result<String, CompileError> {
708
- if props.is_empty() {
709
- return Ok("null".to_string());
710
- }
711
- let parts: Result<Vec<_>, _> = props
712
- .iter()
713
- .map(|p| match p {
714
- JsxProp::Attr { name, value } => {
715
- let val = match value {
716
- JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
717
- JsxAttrValue::Expr(e) => self.emit_expr(e)?,
718
- JsxAttrValue::ImplicitTrue => "true".to_string(),
719
- };
720
- let key = name.as_ref();
721
- Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
722
- format!("{}: {}", key, val)
723
- } else {
724
- format!("{:?}: {}", key, val)
725
- })
726
- }
727
- JsxProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
728
- })
729
- .collect();
730
- Ok(format!("{{ {} }}", parts?.join(", ")))
731
- }
732
-
733
- fn emit_jsx_child(&mut self, child: &JsxChild) -> Result<String, CompileError> {
734
- match child {
735
- JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
736
- JsxChild::Expr(e) => {
737
- let inner = self.emit_expr(e)?;
738
- // Only wrap literals we know are primitives (number, bool, null). Never wrap:
739
- // string/template (already strings), JSX (elements), Call (components), Array/Ident (may hold elements).
740
- let needs_string = matches!(
741
- e,
742
- Expr::Literal {
743
- value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
744
- ..
745
- }
746
- );
747
- Ok(if needs_string {
748
- format!("String({})", inner)
749
- } else {
750
- inner
751
- })
752
- }
753
- }
754
- }
755
656
  }
756
657
 
757
- /// Compile a single program (no imports) to JavaScript.
758
- pub fn compile_with_jsx(
759
- program: &Program,
760
- optimize: bool,
761
- jsx_mode: JsxMode,
762
- ) -> Result<String, CompileError> {
658
+ /// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
659
+ pub fn compile_with_jsx(program: &Program, optimize: bool) -> Result<String, CompileError> {
763
660
  let program = if optimize {
764
661
  tishlang_opt::optimize(program)
765
662
  } else {
766
663
  program.clone()
767
664
  };
768
- let mut g = Codegen::new(jsx_mode);
665
+ let mut g = Codegen::new();
769
666
  g.emit_program(&program)?;
770
667
  Ok(g.output)
771
668
  }
@@ -776,7 +673,6 @@ pub fn compile_project_with_jsx(
776
673
  entry_path: &std::path::Path,
777
674
  project_root: Option<&std::path::Path>,
778
675
  optimize: bool,
779
- jsx_mode: JsxMode,
780
676
  ) -> Result<String, CompileError> {
781
677
  use tishlang_ast::Statement;
782
678
  let modules = tishlang_compile::resolve_project(entry_path, project_root)
@@ -802,7 +698,7 @@ pub fn compile_project_with_jsx(
802
698
  None
803
699
  }
804
700
  });
805
- let mut js = compile_with_jsx(&program, optimize, jsx_mode)?;
701
+ let mut js = compile_with_jsx(&program, optimize)?;
806
702
  if let Some(name) = default_export {
807
703
  js.push_str(&format!("\nexport default {};\n", name));
808
704
  }
@@ -3,19 +3,16 @@
3
3
 
4
4
  mod codegen;
5
5
  mod error;
6
- mod js_intrinsics;
7
6
 
8
7
  #[cfg(test)]
9
8
  mod tests_jsx;
10
9
 
11
- pub use codegen::{compile_project_with_jsx, compile_with_jsx, JsxMode};
10
+ pub use codegen::{compile_project_with_jsx, compile_with_jsx};
12
11
  pub use error::CompileError;
13
12
 
14
- /// Default JSX mode lowers to Lattish-style calls (implementation detail). Import what you use from
15
- /// `lattish` (e.g. `useState`, `createRoot`); the merged bundle includes the JSX runtime from that
16
- /// module. JSX-only files can use `import {} from "lattish"` to pull it in without bindings.
13
+ /// JSX lowers to `h` / `Fragment`; merge the `lattish` runtime for hooks and DOM.
17
14
  pub fn compile(program: &tishlang_ast::Program, optimize: bool) -> Result<String, CompileError> {
18
- compile_with_jsx(program, optimize, JsxMode::LattishH)
15
+ compile_with_jsx(program, optimize)
19
16
  }
20
17
 
21
18
  pub fn compile_project(
@@ -23,5 +20,5 @@ pub fn compile_project(
23
20
  project_root: Option<&std::path::Path>,
24
21
  optimize: bool,
25
22
  ) -> Result<String, CompileError> {
26
- compile_project_with_jsx(entry_path, project_root, optimize, JsxMode::LattishH)
23
+ compile_project_with_jsx(entry_path, project_root, optimize)
27
24
  }
@@ -4,13 +4,13 @@ mod tests {
4
4
 
5
5
  use tishlang_parser::parse;
6
6
 
7
- use crate::{compile_project_with_jsx, compile_with_jsx, JsxMode};
7
+ use crate::{compile_project_with_jsx, compile_with_jsx};
8
8
 
9
9
  #[test]
10
10
  fn lattish_jsx_emits_h_with_children_array() {
11
11
  let src = r#"fn X() { return <div class="a">{"hi"}</div> }"#;
12
12
  let program = parse(src).unwrap();
13
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
13
+ let js = compile_with_jsx(&program, false).unwrap();
14
14
  assert!(js.contains("h(\"div\", { class: \"a\" }, [\"hi\"])"), "{}", js);
15
15
  assert!(!js.contains("function __h("));
16
16
  }
@@ -19,7 +19,7 @@ mod tests {
19
19
  fn fragment_lattish_uses_fragment_symbol() {
20
20
  let src = "fn X() { return <><b>{\"1\"}</b></> }";
21
21
  let program = parse(src).unwrap();
22
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
22
+ let js = compile_with_jsx(&program, false).unwrap();
23
23
  assert!(js.contains("h(Fragment, null, ["));
24
24
  }
25
25
 
@@ -27,7 +27,7 @@ mod tests {
27
27
  fn jsx_text_whitespace_coalesced() {
28
28
  let src = r#"fn X() { return <p>First paragraph</p> }"#;
29
29
  let program = parse(src).unwrap();
30
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
30
+ let js = compile_with_jsx(&program, false).unwrap();
31
31
  assert!(
32
32
  js.contains("\"First paragraph\""),
33
33
  "expected \"First paragraph\" in output, got: {}",
@@ -43,7 +43,7 @@ mod tests {
43
43
  fn jsx_text_whitespace_coalesced_multiline() {
44
44
  let src = "fn App() {\n return <p>First paragraph</p>\n}";
45
45
  let program = parse(src).unwrap();
46
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
46
+ let js = compile_with_jsx(&program, false).unwrap();
47
47
  assert!(
48
48
  js.contains("\"First paragraph\""),
49
49
  "multiline: expected \"First paragraph\", got: {}",
@@ -56,7 +56,7 @@ mod tests {
56
56
  // Punctuation (e.g. !) concatenates without space: "work!" not "work !"
57
57
  let src = r#"fn X() { return <p>work!</p> }"#;
58
58
  let program = parse(src).unwrap();
59
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
59
+ let js = compile_with_jsx(&program, false).unwrap();
60
60
  assert!(js.contains(r#""work!""#), "expected 'work!', got: {}", &js[..400.min(js.len())]);
61
61
  }
62
62
 
@@ -64,7 +64,7 @@ mod tests {
64
64
  fn jsx_text_emojis() {
65
65
  let src = r#"fn X() { return <p>hello 😔</p> }"#;
66
66
  let program = parse(src).unwrap();
67
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
67
+ let js = compile_with_jsx(&program, false).unwrap();
68
68
  assert!(js.contains("😔"), "expected emoji, got: {}", &js[..400.min(js.len())]);
69
69
  }
70
70
 
@@ -78,7 +78,7 @@ mod tests {
78
78
  f.write_all(src.as_bytes()).unwrap();
79
79
  f.sync_all().unwrap();
80
80
  drop(f);
81
- let js = compile_project_with_jsx(&path, Some(&dir), false, JsxMode::LattishH)
81
+ let js = compile_project_with_jsx(&path, Some(&dir), false)
82
82
  .expect("compile_project_with_jsx failed");
83
83
  assert!(
84
84
  js.contains("\"First paragraph\""),
@@ -89,12 +89,14 @@ mod tests {
89
89
  }
90
90
 
91
91
  #[test]
92
- fn vdom_emits_vdom_h() {
93
- let src = r#"fn X() { return <p/> }"#;
92
+ fn jsx_never_emits_vdom_helpers_or_prelude_flags() {
93
+ let src = r#"fn X() { return <div class="x">{"a"}</div> }"#;
94
94
  let program = parse(src).unwrap();
95
- let js = compile_with_jsx(&program, false, JsxMode::Vdom).unwrap();
96
- assert!(js.contains("__vdom_h(\"p\", null, [])"), "{}", &js[..600.min(js.len())]);
97
- assert!(js.contains("__lattishVdomPatch"));
95
+ let js = compile_with_jsx(&program, false).unwrap();
96
+ assert!(js.contains("h(\"div\", { class: \"x\" }"), "{}", &js[..500.min(js.len())]);
97
+ 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())]);
98
100
  }
99
101
 
100
102
  /// Component calls like {Panel()} return DOM elements. Wrapping in String() produces [object HTMLDivElement].
@@ -105,7 +107,7 @@ fn Panel() { return <div class="p">content</div> }
105
107
  fn App() { return <div>{Panel()}</div> }
106
108
  "#;
107
109
  let program = parse(src).unwrap();
108
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
110
+ let js = compile_with_jsx(&program, false).unwrap();
109
111
  assert!(
110
112
  js.contains("Panel()"),
111
113
  "component call should appear as Panel(), got: {}",
@@ -123,7 +125,7 @@ fn App() { return <div>{Panel()}</div> }
123
125
  fn jsx_nested_element_not_wrapped_in_string() {
124
126
  let src = r#"fn X() { return <div><span>inner</span></div> }"#;
125
127
  let program = parse(src).unwrap();
126
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
128
+ let js = compile_with_jsx(&program, false).unwrap();
127
129
  assert!(
128
130
  !js.contains("String(h("),
129
131
  "nested JSX elements must NOT be wrapped in String(). got: {}",
@@ -136,7 +138,7 @@ fn App() { return <div>{Panel()}</div> }
136
138
  fn jsx_literal_number_wrapped_in_string() {
137
139
  let src = r#"fn X() { return <span>{42}</span> }"#;
138
140
  let program = parse(src).unwrap();
139
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
141
+ let js = compile_with_jsx(&program, false).unwrap();
140
142
  assert!(
141
143
  js.contains("String(42)"),
142
144
  "literal number in JSX should be wrapped in String(). got: {}",
@@ -156,7 +158,7 @@ fn FileList() {
156
158
  }
157
159
  "#;
158
160
  let program = parse(src).unwrap();
159
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
161
+ let js = compile_with_jsx(&program, false).unwrap();
160
162
  assert!(
161
163
  !js.contains("String(items)"),
162
164
  "array/ident in JSX must NOT be wrapped in String() - causes [object HTMLButtonElement]. got: {}",
@@ -177,7 +179,7 @@ fn FileList() {
177
179
  >{"ok"}</button>
178
180
  }"#;
179
181
  let program = parse(src).expect("parse multi-line JSX with > comparison in attr");
180
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).expect("compile");
182
+ let js = compile_with_jsx(&program, false).expect("compile");
181
183
  assert!(
182
184
  js.contains("length > 0") || js.contains("length>0"),
183
185
  "expected compiled JS to preserve greater-than comparison, got: {}",
@@ -197,7 +199,7 @@ fn FileList() {
197
199
  >{"outer"}</button>
198
200
  }"#;
199
201
  let program = parse(src).expect("parse nested JSX inside onclick");
200
- let js = compile_with_jsx(&program, false, JsxMode::LattishH).expect("compile");
202
+ let js = compile_with_jsx(&program, false).expect("compile");
201
203
  assert!(
202
204
  js.contains("\"inner\""),
203
205
  "expected nested span text in output, got: {}",
@@ -209,4 +211,72 @@ fn FileList() {
209
211
  &js[..900.min(js.len())]
210
212
  );
211
213
  }
214
+
215
+ #[test]
216
+ fn new_date_global_emits_valid_js_with_and_without_optimize() {
217
+ let src = "let epoch = new Date(0)\nconsole.log(epoch.getTime())";
218
+ let program = parse(src).expect("parse");
219
+ for optimize in [false, true] {
220
+ let js = compile_with_jsx(&program, optimize).expect("compile");
221
+ assert!(
222
+ js.contains("new Date(0)"),
223
+ "optimize={optimize}: expected `new Date(0)` in JS output:\n{js}"
224
+ );
225
+ assert!(
226
+ !js.contains("let epoch = new;"),
227
+ "optimize={optimize}: broken `new` emission (missing constructor):\n{js}"
228
+ );
229
+ }
230
+ }
231
+
232
+ #[test]
233
+ fn new_uint8array_emits_direct_new_no_preamble() {
234
+ let src = "fn f(n) { return new Uint8Array(n) }";
235
+ let program = parse(src).expect("parse");
236
+ let js = compile_with_jsx(&program, false).expect("compile");
237
+ assert!(
238
+ js.contains("new Uint8Array("),
239
+ "expected direct new Uint8Array, got: {}",
240
+ &js[..500.min(js.len())]
241
+ );
242
+ assert!(
243
+ !js.contains("__tishUint8Array"),
244
+ "should not emit legacy intrinsic helper"
245
+ );
246
+ }
247
+
248
+ #[test]
249
+ fn new_audio_context_emits_direct_new_no_preamble() {
250
+ let src = "fn f() { return new AudioContext() }";
251
+ let program = parse(src).expect("parse");
252
+ let js = compile_with_jsx(&program, false).expect("compile");
253
+ assert!(
254
+ js.contains("new AudioContext("),
255
+ "expected new AudioContext, got: {}",
256
+ &js[..500.min(js.len())]
257
+ );
258
+ assert!(
259
+ !js.contains("__tishWebAudioCreateContext"),
260
+ "should not emit legacy intrinsic helper"
261
+ );
262
+ }
263
+
264
+ #[test]
265
+ fn new_class_name_emits_direct_new_js() {
266
+ let src = r#"
267
+ fn ClassName(x) {
268
+ return x
269
+ }
270
+ fn factory() {
271
+ return new ClassName(42)
272
+ }
273
+ "#;
274
+ let program = parse(src).expect("parse");
275
+ let js = compile_with_jsx(&program, false).expect("compile");
276
+ assert!(
277
+ js.contains("new ClassName("),
278
+ "expected new ClassName( in JS output, got: {}",
279
+ &js[..800.min(js.len())]
280
+ );
281
+ }
212
282
  }
@@ -10,7 +10,6 @@ mod resolve_virtual;
10
10
  use base64::Engine;
11
11
  use resolve_virtual::{detect_cycles_virtual, merge_modules_virtual, resolve_virtual};
12
12
  use std::collections::HashMap;
13
- use tishlang_compile_js::JsxMode;
14
13
  use wasm_bindgen::prelude::*;
15
14
 
16
15
  #[wasm_bindgen]
@@ -24,7 +23,7 @@ pub fn compile_to_bytecode(source: &str) -> Result<String, JsValue> {
24
23
  #[wasm_bindgen]
25
24
  pub fn compile_to_js(source: &str) -> Result<String, JsValue> {
26
25
  let program = tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
27
- tishlang_compile_js::compile_with_jsx(&program, true, JsxMode::LattishH)
26
+ tishlang_compile_js::compile_with_jsx(&program, true)
28
27
  .map_err(|e| JsValue::from_str(&e.message))
29
28
  }
30
29
 
@@ -50,6 +49,6 @@ pub fn compile_to_js_with_imports(entry_path: &str, files_json: &str) -> Result<
50
49
  detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
51
50
  let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
52
51
  let program = tishlang_opt::optimize(&program);
53
- tishlang_compile_js::compile_with_jsx(&program, true, JsxMode::LattishH)
52
+ tishlang_compile_js::compile_with_jsx(&program, true)
54
53
  .map_err(|e| JsValue::from_str(&e.message))
55
54
  }
@@ -11,4 +11,8 @@ default = []
11
11
  regex = ["dep:fancy-regex"]
12
12
 
13
13
  [dependencies]
14
+ ahash = "0.8.11"
14
15
  fancy-regex = { version = "0.17.0", optional = true }
16
+
17
+ [target.wasm32-unknown-unknown.dependencies]
18
+ getrandom = { version = "0.3", features = ["wasm_js"] }
@@ -4,9 +4,12 @@
4
4
  //! booleans, null, and object structure are easier to scan.
5
5
 
6
6
  use std::io::IsTerminal;
7
+ use std::sync::OnceLock;
7
8
 
8
9
  use crate::Value;
9
10
 
11
+ static CONSOLE_USES_COLORS: OnceLock<bool> = OnceLock::new();
12
+
10
13
  /// ANSI escape codes (standard 4-bit + bright black for dim).
11
14
  const RESET: &str = "\x1b[0m";
12
15
  /// Number: yellow (Node-style)
@@ -25,8 +28,11 @@ const PUNCT: &str = "\x1b[90m";
25
28
  const SPECIAL: &str = "\x1b[90m";
26
29
 
27
30
  /// Returns whether console output should use colors (stdout is a TTY).
31
+ ///
32
+ /// Cached for the process lifetime. `is_terminal()` is a syscall; benchmarks and
33
+ /// scripts with many `console.log` calls must not pay it on every line.
28
34
  pub fn use_console_colors() -> bool {
29
- std::io::stdout().is_terminal()
35
+ *CONSOLE_USES_COLORS.get_or_init(|| std::io::stdout().is_terminal())
30
36
  }
31
37
 
32
38
  /// Format a single value for console with optional ANSI colors (Node/Bun-style).
@@ -2,7 +2,6 @@
2
2
 
3
3
  use crate::Value;
4
4
  use std::cell::RefCell;
5
- use std::collections::HashMap;
6
5
  use std::rc::Rc;
7
6
  use std::sync::Arc;
8
7
 
@@ -260,7 +259,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
260
259
 
261
260
  fn parse_object(input: &str) -> Result<(Value, &str), String> {
262
261
  let mut input = &input[1..]; // skip '{'
263
- let mut map = HashMap::new();
262
+ let mut map = crate::ObjectMap::default();
264
263
 
265
264
  input = input.trim_start();
266
265
  if let Some(rest) = input.strip_prefix('}') {
@@ -24,11 +24,10 @@
24
24
  macro_rules! tish_module {
25
25
  ($($name:expr => $fn:expr),* $(,)?) => {{
26
26
  use std::cell::RefCell;
27
- use std::collections::HashMap;
28
27
  use std::rc::Rc;
29
28
  use std::sync::Arc;
30
- use $crate::Value;
31
- let mut map = HashMap::new();
29
+ use $crate::{ObjectMap, Value};
30
+ let mut map = ObjectMap::default();
32
31
  $(
33
32
  map.insert(Arc::from($name), Value::Function(Rc::new($fn)));
34
33
  )*
@@ -1,10 +1,15 @@
1
1
  //! Unified Value type for Tish runtime values.
2
2
 
3
3
  use std::cell::RefCell;
4
- use std::collections::HashMap;
5
4
  use std::rc::Rc;
6
5
  use std::sync::Arc;
7
6
 
7
+ use ahash::AHashMap;
8
+
9
+ /// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
10
+ /// Uses a faster hasher than `std::collections::HashMap` for string-heavy workloads.
11
+ pub type ObjectMap = AHashMap<Arc<str>, Value>;
12
+
8
13
  #[cfg(feature = "regex")]
9
14
  use fancy_regex::Regex;
10
15
 
@@ -147,7 +152,7 @@ pub enum Value {
147
152
  Bool(bool),
148
153
  Null,
149
154
  Array(Rc<RefCell<Vec<Value>>>),
150
- Object(Rc<RefCell<HashMap<Arc<str>, Value>>>),
155
+ Object(Rc<RefCell<ObjectMap>>),
151
156
  Function(NativeFn),
152
157
  #[cfg(feature = "regex")]
153
158
  RegExp(Rc<RefCell<TishRegExp>>),
@@ -256,8 +261,8 @@ impl Value {
256
261
  Value::Array(Rc::new(RefCell::new(items)))
257
262
  }
258
263
 
259
- /// Create a new object Value from a HashMap.
260
- pub fn object(map: HashMap<Arc<str>, Value>) -> Self {
264
+ /// Create a new object Value from a property map.
265
+ pub fn object(map: ObjectMap) -> Self {
261
266
  Value::Object(Rc::new(RefCell::new(map)))
262
267
  }
263
268
 
@@ -268,7 +273,7 @@ impl Value {
268
273
 
269
274
  /// Create an empty object Value.
270
275
  pub fn empty_object() -> Self {
271
- Value::Object(Rc::new(RefCell::new(HashMap::new())))
276
+ Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
272
277
  }
273
278
 
274
279
  /// Extract the number value, if this is a Number.