@tishlang/tish 2.10.1 → 2.16.13

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 (75) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +16 -0
  5. package/crates/tish/src/main.rs +24 -4
  6. package/crates/tish/tests/integration_test.rs +149 -0
  7. package/crates/tish_ast/src/ast.rs +10 -0
  8. package/crates/tish_builtins/src/array.rs +529 -56
  9. package/crates/tish_builtins/src/collections.rs +114 -40
  10. package/crates/tish_builtins/src/string.rs +95 -8
  11. package/crates/tish_bytecode/src/chunk.rs +7 -0
  12. package/crates/tish_bytecode/src/compiler.rs +560 -64
  13. package/crates/tish_bytecode/src/lib.rs +1 -1
  14. package/crates/tish_bytecode/src/opcode.rs +154 -2
  15. package/crates/tish_bytecode/src/serialize.rs +2 -0
  16. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  17. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  18. package/crates/tish_compile/src/codegen.rs +17736 -5269
  19. package/crates/tish_compile/src/infer.rs +1707 -190
  20. package/crates/tish_compile/src/lib.rs +29 -4
  21. package/crates/tish_compile/src/resolve.rs +66 -5
  22. package/crates/tish_compile/src/types.rs +224 -28
  23. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  24. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  25. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  26. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  27. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  28. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  29. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  30. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  31. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  32. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  33. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  34. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  35. package/crates/tish_compile_js/src/codegen.rs +91 -4
  36. package/crates/tish_compile_js/src/lib.rs +5 -2
  37. package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
  38. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  39. package/crates/tish_core/Cargo.toml +4 -0
  40. package/crates/tish_core/src/json.rs +104 -5
  41. package/crates/tish_core/src/lib.rs +174 -0
  42. package/crates/tish_core/src/shape.rs +4 -2
  43. package/crates/tish_core/src/value.rs +565 -35
  44. package/crates/tish_core/src/vmref.rs +14 -0
  45. package/crates/tish_eval/src/eval.rs +675 -76
  46. package/crates/tish_eval/src/natives.rs +19 -0
  47. package/crates/tish_eval/src/value.rs +76 -21
  48. package/crates/tish_ffi/src/lib.rs +11 -1
  49. package/crates/tish_ffi/tests/double_free.rs +35 -0
  50. package/crates/tish_fmt/src/lib.rs +75 -1
  51. package/crates/tish_lexer/src/lib.rs +76 -0
  52. package/crates/tish_lexer/src/token.rs +4 -0
  53. package/crates/tish_lint/src/lib.rs +126 -0
  54. package/crates/tish_lsp/README.md +2 -1
  55. package/crates/tish_lsp/src/main.rs +378 -28
  56. package/crates/tish_native/src/build.rs +41 -0
  57. package/crates/tish_parser/Cargo.toml +4 -0
  58. package/crates/tish_parser/src/lib.rs +27 -0
  59. package/crates/tish_parser/src/parser.rs +302 -20
  60. package/crates/tish_resolve/src/lib.rs +9 -0
  61. package/crates/tish_runtime/Cargo.toml +5 -0
  62. package/crates/tish_runtime/src/http.rs +28 -10
  63. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  64. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  65. package/crates/tish_runtime/src/lib.rs +523 -42
  66. package/crates/tish_runtime/src/timers.rs +49 -2
  67. package/crates/tish_ui/src/jsx.rs +253 -0
  68. package/crates/tish_vm/src/jit.rs +2514 -117
  69. package/crates/tish_vm/src/vm.rs +1227 -182
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish +0 -0
  72. package/platform/darwin-x64/tish +0 -0
  73. package/platform/linux-arm64/tish +0 -0
  74. package/platform/linux-x64/tish +0 -0
  75. package/platform/win32-x64/tish.exe +0 -0
@@ -14,6 +14,26 @@ fn next_id() -> u64 {
14
14
  NEXT_ID.fetch_add(1, Ordering::SeqCst)
15
15
  }
16
16
 
17
+ /// Maximum number of LIVE (registered, not-yet-fired) timers per worker thread. A program that keeps
18
+ /// scheduling `setTimeout`/`setInterval` without them draining would otherwise grow the registry (and
19
+ /// its retained callbacks + closed-over data) without bound (#384). Past the cap a new timer is
20
+ /// dropped rather than registered. Override with `TISH_MAX_TIMERS`; default 100k — far above any real
21
+ /// timer workload, low enough to bound memory.
22
+ fn max_live_timers() -> usize {
23
+ // Read per call (timer registration is not a hot path, and the cap exists precisely to bound the
24
+ // pathological caller): keeps the limit overridable per test without a process-global cache.
25
+ std::env::var("TISH_MAX_TIMERS")
26
+ .ok()
27
+ .and_then(|v| v.parse().ok())
28
+ .filter(|&n| n > 0)
29
+ .unwrap_or(100_000)
30
+ }
31
+
32
+ #[cfg(test)]
33
+ fn registry_len() -> usize {
34
+ REGISTRY.with(|r| r.borrow().len())
35
+ }
36
+
17
37
  struct TimerEntry {
18
38
  due: Instant,
19
39
  callback: Value,
@@ -114,7 +134,12 @@ pub fn set_timeout(args: &[Value]) -> Value {
114
134
  let id = next_id();
115
135
  let due = Instant::now() + Duration::from_millis(delay_ms);
116
136
  REGISTRY.with(|r| {
117
- r.borrow_mut().insert(
137
+ let mut reg = r.borrow_mut();
138
+ // #384: bound the live-timer count so a runaway scheduler can't grow the registry unbounded.
139
+ if reg.len() >= max_live_timers() {
140
+ return;
141
+ }
142
+ reg.insert(
118
143
  id,
119
144
  TimerEntry {
120
145
  due,
@@ -138,7 +163,12 @@ pub fn set_interval(args: &[Value]) -> Value {
138
163
  let id = next_id();
139
164
  let due = Instant::now() + Duration::from_millis(interval_ms);
140
165
  REGISTRY.with(|r| {
141
- r.borrow_mut().insert(
166
+ let mut reg = r.borrow_mut();
167
+ // #384: bound the live-timer count (see set_timeout).
168
+ if reg.len() >= max_live_timers() {
169
+ return;
170
+ }
171
+ reg.insert(
142
172
  id,
143
173
  TimerEntry {
144
174
  due,
@@ -170,3 +200,20 @@ pub fn clear_timeout(args: &[Value]) -> Value {
170
200
  pub fn clear_interval(args: &[Value]) -> Value {
171
201
  clear_timeout(args)
172
202
  }
203
+
204
+ #[cfg(test)]
205
+ mod timer_cap_tests_384 {
206
+ use super::*;
207
+
208
+ #[test]
209
+ fn set_timeout_is_bounded_by_max_timers() {
210
+ // Each `#[test]` runs on its own thread, so REGISTRY (thread_local) starts empty here.
211
+ std::env::set_var("TISH_MAX_TIMERS", "5");
212
+ let cb = tishlang_core::native_fn(|_| Value::Null);
213
+ for _ in 0..20 {
214
+ let _ = set_timeout(&[Value::Function(cb.clone()), Value::Number(10_000.0)]);
215
+ }
216
+ assert_eq!(registry_len(), 5, "live timers must be capped at TISH_MAX_TIMERS");
217
+ std::env::remove_var("TISH_MAX_TIMERS");
218
+ }
219
+ }
@@ -240,6 +240,7 @@ fn collect_fun_decl_names_stmt(stmt: &Statement, names: &mut HashSet<String>) {
240
240
  Statement::Export { declaration, .. } => match declaration.as_ref() {
241
241
  ExportDeclaration::Named(inner) => collect_fun_decl_names_stmt(inner, names),
242
242
  ExportDeclaration::Default(e) => collect_fun_decl_names_expr(e, names),
243
+ ExportDeclaration::ReExport { .. } => {}
243
244
  },
244
245
  Statement::Import { .. }
245
246
  | Statement::Break { .. }
@@ -541,6 +542,257 @@ pub fn program_contains_jsx(program: &tishlang_ast::Program) -> bool {
541
542
  program.statements.iter().any(stmt_contains_jsx)
542
543
  }
543
544
 
545
+ /// Whether the program contains any JSX **fragment** (`<>…</>`). Fragments lower to
546
+ /// `h(Fragment, null, …)`, so they require the `Fragment` runtime binding in addition to `h`.
547
+ pub fn program_contains_jsx_fragment(program: &tishlang_ast::Program) -> bool {
548
+ program.statements.iter().any(stmt_contains_jsx_fragment)
549
+ }
550
+
551
+ /// Which JSX runtime bindings (`h`, `Fragment`) a module needs auto-imported, given which names are
552
+ /// already in module scope. Any JSX needs `h`; fragments additionally need `Fragment`. A binding is
553
+ /// omitted if it's already provided by a top-level import/declaration (see
554
+ /// [`collect_jsx_runtime_bindings_in_scope`]).
555
+ ///
556
+ /// Returns a subset of `["h", "Fragment"]` (in that order), or `[]` when the module has no JSX.
557
+ pub fn jsx_runtime_imports_needed(program: &tishlang_ast::Program) -> Vec<&'static str> {
558
+ if !program_contains_jsx(program) {
559
+ return Vec::new();
560
+ }
561
+ let in_scope = collect_jsx_runtime_bindings_in_scope(program);
562
+ let mut needed = Vec::new();
563
+ if !in_scope.contains("h") {
564
+ needed.push("h");
565
+ }
566
+ if program_contains_jsx_fragment(program) && !in_scope.contains("Fragment") {
567
+ needed.push("Fragment");
568
+ }
569
+ needed
570
+ }
571
+
572
+ /// Top-level names that already bind the JSX runtime symbols `h` / `Fragment`, so the auto-import
573
+ /// must not shadow or duplicate them. Covers named imports (local alias), top-level `const`/`let`,
574
+ /// and `fn` declarations, including those wrapped in `export`.
575
+ pub fn collect_jsx_runtime_bindings_in_scope(program: &tishlang_ast::Program) -> HashSet<String> {
576
+ use tishlang_ast::{ExportDeclaration, ImportSpecifier, Statement};
577
+ let mut names = HashSet::new();
578
+ let note = |n: &str, names: &mut HashSet<String>| {
579
+ if n == "h" || n == "Fragment" {
580
+ names.insert(n.to_string());
581
+ }
582
+ };
583
+ for stmt in &program.statements {
584
+ match stmt {
585
+ Statement::Import { specifiers, .. } => {
586
+ for s in specifiers {
587
+ if let ImportSpecifier::Named { name, alias, .. } = s {
588
+ let local = alias.as_deref().unwrap_or(name.as_ref());
589
+ note(local, &mut names);
590
+ }
591
+ }
592
+ }
593
+ Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
594
+ note(name.as_ref(), &mut names)
595
+ }
596
+ Statement::Export { declaration, .. } => {
597
+ if let ExportDeclaration::Named(inner) = declaration.as_ref() {
598
+ match inner.as_ref() {
599
+ Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
600
+ note(name.as_ref(), &mut names)
601
+ }
602
+ _ => {}
603
+ }
604
+ }
605
+ }
606
+ _ => {}
607
+ }
608
+ }
609
+ names
610
+ }
611
+
612
+ fn stmt_contains_jsx_fragment(stmt: &tishlang_ast::Statement) -> bool {
613
+ use tishlang_ast::{ExportDeclaration, Statement};
614
+ match stmt {
615
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
616
+ statements.iter().any(stmt_contains_jsx_fragment)
617
+ }
618
+ Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_contains_jsx_fragment),
619
+ Statement::VarDeclDestructure { init, .. } => expr_contains_jsx_fragment(init),
620
+ Statement::ExprStmt { expr, .. } => expr_contains_jsx_fragment(expr),
621
+ Statement::Return { value, .. } => value.as_ref().is_some_and(expr_contains_jsx_fragment),
622
+ Statement::If {
623
+ cond,
624
+ then_branch,
625
+ else_branch,
626
+ ..
627
+ } => {
628
+ expr_contains_jsx_fragment(cond)
629
+ || stmt_contains_jsx_fragment(then_branch)
630
+ || else_branch
631
+ .as_ref()
632
+ .is_some_and(|s| stmt_contains_jsx_fragment(s))
633
+ }
634
+ Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
635
+ expr_contains_jsx_fragment(cond) || stmt_contains_jsx_fragment(body)
636
+ }
637
+ Statement::For {
638
+ init,
639
+ cond,
640
+ update,
641
+ body,
642
+ ..
643
+ } => {
644
+ init.as_ref().is_some_and(|s| stmt_contains_jsx_fragment(s))
645
+ || cond.as_ref().is_some_and(expr_contains_jsx_fragment)
646
+ || update.as_ref().is_some_and(expr_contains_jsx_fragment)
647
+ || stmt_contains_jsx_fragment(body)
648
+ }
649
+ Statement::ForOf { iterable, body, .. } => {
650
+ expr_contains_jsx_fragment(iterable) || stmt_contains_jsx_fragment(body)
651
+ }
652
+ Statement::Switch {
653
+ expr,
654
+ cases,
655
+ default_body,
656
+ ..
657
+ } => {
658
+ expr_contains_jsx_fragment(expr)
659
+ || cases.iter().any(|(e, ss)| {
660
+ e.as_ref().is_some_and(expr_contains_jsx_fragment)
661
+ || ss.iter().any(stmt_contains_jsx_fragment)
662
+ })
663
+ || default_body
664
+ .as_ref()
665
+ .is_some_and(|ss| ss.iter().any(stmt_contains_jsx_fragment))
666
+ }
667
+ Statement::Try {
668
+ body,
669
+ catch_body,
670
+ finally_body,
671
+ ..
672
+ } => {
673
+ stmt_contains_jsx_fragment(body)
674
+ || catch_body
675
+ .as_ref()
676
+ .is_some_and(|s| stmt_contains_jsx_fragment(s))
677
+ || finally_body
678
+ .as_ref()
679
+ .is_some_and(|s| stmt_contains_jsx_fragment(s))
680
+ }
681
+ Statement::FunDecl { body, .. } => stmt_contains_jsx_fragment(body),
682
+ Statement::Throw { value, .. } => expr_contains_jsx_fragment(value),
683
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
684
+ ExportDeclaration::Named(inner) => stmt_contains_jsx_fragment(inner),
685
+ ExportDeclaration::Default(e) => expr_contains_jsx_fragment(e),
686
+ ExportDeclaration::ReExport { .. } => false,
687
+ },
688
+ Statement::Import { .. }
689
+ | Statement::Break { .. }
690
+ | Statement::Continue { .. }
691
+ | Statement::TypeAlias { .. }
692
+ | Statement::DeclareVar { .. }
693
+ | Statement::DeclareFun { .. } => false,
694
+ }
695
+ }
696
+
697
+ fn expr_contains_jsx_fragment(expr: &Expr) -> bool {
698
+ match expr {
699
+ Expr::JsxFragment { .. } => true,
700
+ Expr::JsxElement {
701
+ props, children, ..
702
+ } => {
703
+ props.iter().any(|p| match p {
704
+ JsxProp::Attr {
705
+ value: JsxAttrValue::Expr(e),
706
+ ..
707
+ }
708
+ | JsxProp::Spread(e) => expr_contains_jsx_fragment(e),
709
+ _ => false,
710
+ }) || children.iter().any(|c| match c {
711
+ JsxChild::Expr(e) => expr_contains_jsx_fragment(e),
712
+ JsxChild::Text(_) => false,
713
+ })
714
+ }
715
+ Expr::Binary { left, right, .. } => {
716
+ expr_contains_jsx_fragment(left) || expr_contains_jsx_fragment(right)
717
+ }
718
+ Expr::Unary { operand, .. } => expr_contains_jsx_fragment(operand),
719
+ Expr::Assign { value, .. } => expr_contains_jsx_fragment(value),
720
+ Expr::Call { callee, args, .. } => {
721
+ expr_contains_jsx_fragment(callee)
722
+ || args.iter().any(|a| match a {
723
+ tishlang_ast::CallArg::Expr(e) | tishlang_ast::CallArg::Spread(e) => {
724
+ expr_contains_jsx_fragment(e)
725
+ }
726
+ })
727
+ }
728
+ Expr::Member { object, prop, .. } => {
729
+ expr_contains_jsx_fragment(object)
730
+ || matches!(prop, tishlang_ast::MemberProp::Expr(e) if expr_contains_jsx_fragment(e))
731
+ }
732
+ Expr::Index { object, index, .. } => {
733
+ expr_contains_jsx_fragment(object) || expr_contains_jsx_fragment(index)
734
+ }
735
+ Expr::Conditional {
736
+ cond,
737
+ then_branch,
738
+ else_branch,
739
+ ..
740
+ } => {
741
+ expr_contains_jsx_fragment(cond)
742
+ || expr_contains_jsx_fragment(then_branch)
743
+ || expr_contains_jsx_fragment(else_branch)
744
+ }
745
+ Expr::Array { elements, .. } => elements.iter().any(|el| match el {
746
+ ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_contains_jsx_fragment(e),
747
+ }),
748
+ Expr::Object { props, .. } => props.iter().any(|p| match p {
749
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_contains_jsx_fragment(e),
750
+ }),
751
+ Expr::ArrowFunction { body, .. } => match body {
752
+ tishlang_ast::ArrowBody::Expr(e) => expr_contains_jsx_fragment(e),
753
+ tishlang_ast::ArrowBody::Block(s) => stmt_contains_jsx_fragment(s),
754
+ },
755
+ Expr::NullishCoalesce { left, right, .. } => {
756
+ expr_contains_jsx_fragment(left) || expr_contains_jsx_fragment(right)
757
+ }
758
+ Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(expr_contains_jsx_fragment),
759
+ Expr::Await { operand, .. } => expr_contains_jsx_fragment(operand),
760
+ Expr::TypeOf { operand, .. } => expr_contains_jsx_fragment(operand),
761
+ Expr::Delete { target, .. } => expr_contains_jsx_fragment(target),
762
+ Expr::CompoundAssign { value, .. } | Expr::LogicalAssign { value, .. } => {
763
+ expr_contains_jsx_fragment(value)
764
+ }
765
+ Expr::MemberAssign { object, value, .. } => {
766
+ expr_contains_jsx_fragment(object) || expr_contains_jsx_fragment(value)
767
+ }
768
+ Expr::IndexAssign {
769
+ object,
770
+ index,
771
+ value,
772
+ ..
773
+ } => {
774
+ expr_contains_jsx_fragment(object)
775
+ || expr_contains_jsx_fragment(index)
776
+ || expr_contains_jsx_fragment(value)
777
+ }
778
+ Expr::New { callee, args, .. } => {
779
+ expr_contains_jsx_fragment(callee)
780
+ || args.iter().any(|a| match a {
781
+ tishlang_ast::CallArg::Expr(e) | tishlang_ast::CallArg::Spread(e) => {
782
+ expr_contains_jsx_fragment(e)
783
+ }
784
+ })
785
+ }
786
+ Expr::PostfixInc { .. }
787
+ | Expr::PrefixInc { .. }
788
+ | Expr::PostfixDec { .. }
789
+ | Expr::PrefixDec { .. }
790
+ | Expr::Literal { .. }
791
+ | Expr::Ident { .. }
792
+ | Expr::NativeModuleLoad { .. } => false,
793
+ }
794
+ }
795
+
544
796
  fn stmt_contains_jsx(stmt: &tishlang_ast::Statement) -> bool {
545
797
  use tishlang_ast::{ExportDeclaration, Statement};
546
798
  match stmt {
@@ -607,6 +859,7 @@ fn stmt_contains_jsx(stmt: &tishlang_ast::Statement) -> bool {
607
859
  Statement::Export { declaration, .. } => match declaration.as_ref() {
608
860
  ExportDeclaration::Named(inner) => stmt_contains_jsx(inner),
609
861
  ExportDeclaration::Default(e) => expr_contains_jsx(e),
862
+ ExportDeclaration::ReExport { .. } => false,
610
863
  },
611
864
  Statement::Import { .. }
612
865
  | Statement::Break { .. }