@tishlang/tish 1.13.1 → 2.0.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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +43 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -23,6 +23,10 @@ fn optimize_statement(stmt: &Statement) -> Statement {
23
23
  span: *span,
24
24
  }
25
25
  }
26
+ Statement::Multi { statements, span } => Statement::Multi {
27
+ statements: statements.iter().map(optimize_statement).collect(),
28
+ span: *span,
29
+ },
26
30
  Statement::VarDecl {
27
31
  name,
28
32
  name_span,
@@ -444,6 +448,10 @@ fn optimize_expr(expr: &Expr) -> Expr {
444
448
  operand: Box::new(optimize_expr(operand)),
445
449
  span: *span,
446
450
  },
451
+ Expr::Delete { target, span } => Expr::Delete {
452
+ target: Box::new(optimize_expr(target)),
453
+ span: *span,
454
+ },
447
455
  Expr::PostfixInc { .. }
448
456
  | Expr::PostfixDec { .. }
449
457
  | Expr::PrefixInc { .. }
@@ -554,19 +562,70 @@ fn literal_strict_eq(a: &Literal, b: &Literal) -> bool {
554
562
  }
555
563
  }
556
564
 
565
+ /// JS `Number.prototype.toString` (radix 10). **Kept byte-for-byte in sync with
566
+ /// `tishlang_core::js_number_to_string`** so a constant-folded `"" + n` here matches the
567
+ /// runtime conversion there; `tish_opt` deliberately does not depend on `tish_core` (it is a
568
+ /// lean AST pass), hence the small duplication of this fixed-spec algorithm. See that function
569
+ /// for the full commentary.
570
+ fn js_number_to_string(value: f64) -> String {
571
+ if value.is_nan() {
572
+ return "NaN".to_string();
573
+ }
574
+ if value == f64::INFINITY {
575
+ return "Infinity".to_string();
576
+ }
577
+ if value == f64::NEG_INFINITY {
578
+ return "-Infinity".to_string();
579
+ }
580
+ if value == 0.0 {
581
+ return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
582
+ }
583
+ let negative = value < 0.0;
584
+ let sci = format!("{:e}", value.abs());
585
+ let (mantissa, exp_str) = sci
586
+ .split_once('e')
587
+ .expect("LowerExp formatting always contains 'e'");
588
+ let exp: i32 = exp_str
589
+ .parse()
590
+ .expect("LowerExp exponent is a valid integer");
591
+ let digits: String = mantissa.chars().filter(|&c| c != '.').collect();
592
+ let k = digits.len() as i32;
593
+ let point = exp + 1;
594
+
595
+ let mut out = String::new();
596
+ if negative {
597
+ out.push('-');
598
+ }
599
+ if k <= point && point <= 21 {
600
+ out.push_str(&digits);
601
+ out.push_str(&"0".repeat((point - k) as usize));
602
+ } else if 0 < point && point <= 21 {
603
+ out.push_str(&digits[..point as usize]);
604
+ out.push('.');
605
+ out.push_str(&digits[point as usize..]);
606
+ } else if -6 < point && point <= 0 {
607
+ out.push_str("0.");
608
+ out.push_str(&"0".repeat((-point) as usize));
609
+ out.push_str(&digits);
610
+ } else {
611
+ let e = point - 1;
612
+ out.push_str(&digits[..1]);
613
+ if k > 1 {
614
+ out.push('.');
615
+ out.push_str(&digits[1..]);
616
+ }
617
+ out.push('e');
618
+ out.push(if e >= 0 { '+' } else { '-' });
619
+ out.push_str(&e.abs().to_string());
620
+ }
621
+ out
622
+ }
623
+
557
624
  fn literal_to_display_string(lit: &Literal) -> String {
558
625
  match lit {
559
- Literal::Number(n) => {
560
- if n.is_nan() {
561
- "NaN".to_string()
562
- } else if *n == f64::INFINITY {
563
- "Infinity".to_string()
564
- } else if *n == f64::NEG_INFINITY {
565
- "-Infinity".to_string()
566
- } else {
567
- n.to_string()
568
- }
569
- }
626
+ // Must match the runtime exactly so constant-folded `"" + n` agrees with the
627
+ // unfolded path (see `js_number_to_string` below).
628
+ Literal::Number(n) => js_number_to_string(*n),
570
629
  Literal::String(s) => s.to_string(),
571
630
  Literal::Bool(b) => b.to_string(),
572
631
  Literal::Null => "null".to_string(),
@@ -708,6 +767,41 @@ fn try_algebraic_simplify(
708
767
  None
709
768
  }
710
769
 
770
+ // JS ToInt32/ToUint32 for the constant folder. NaN/±Infinity → 0 (`f64 as i64` saturates, so a
771
+ // folded `(1e308 * 10) | 0` would otherwise give -1 while the runtime gives 0). `tish_opt` has no
772
+ // `tish_core` dep, so these mirror `tishlang_core::to_int32`/`to_uint32` (kept in sync, pure spec).
773
+ #[inline]
774
+ fn fold_to_int32(x: f64) -> i32 {
775
+ if x.is_finite() {
776
+ x as i64 as i32
777
+ } else {
778
+ 0
779
+ }
780
+ }
781
+ #[inline]
782
+ fn fold_to_uint32(x: f64) -> u32 {
783
+ if x.is_finite() {
784
+ x as i64 as u32
785
+ } else {
786
+ 0
787
+ }
788
+ }
789
+
790
+ /// Constant-fold a relational comparison (`<` `<=` `>` `>=`). Two string literals
791
+ /// compare lexicographically; otherwise the numeric coercions `ln`/`rn` are used.
792
+ /// `pred` maps the `Ordering` to a bool; a NaN-involved numeric comparison has no
793
+ /// ordering and is `false` — matching the VM/interp/native runtime exactly.
794
+ fn fold_relational<F>(left: &Literal, right: &Literal, ln: f64, rn: f64, pred: F) -> bool
795
+ where
796
+ F: FnOnce(std::cmp::Ordering) -> bool,
797
+ {
798
+ let ord = match (left, right) {
799
+ (Literal::String(a), Literal::String(b)) => Some(a.as_ref().cmp(b.as_ref())),
800
+ _ => ln.partial_cmp(&rn),
801
+ };
802
+ ord.map(pred).unwrap_or(false)
803
+ }
804
+
711
805
  fn try_fold_binop(left: &Literal, op: BinOp, right: &Literal) -> Option<Literal> {
712
806
  use BinOp::*;
713
807
  let ln = literal_as_number(left);
@@ -729,24 +823,33 @@ fn try_fold_binop(left: &Literal, op: BinOp, right: &Literal) -> Option<Literal>
729
823
  }
730
824
  Sub => Literal::Number(ln - rn),
731
825
  Mul => Literal::Number(ln * rn),
732
- Div => Literal::Number(if rn == 0.0 { f64::NAN } else { ln / rn }),
733
- Mod => Literal::Number(if rn == 0.0 { f64::NAN } else { ln % rn }),
826
+ // IEEE division/remainder, matching JS + the VM's `eval_binop` + interp + rust-AOT:
827
+ // `5/0` Infinity, `-5/0` → -Infinity, `0/0` NaN, `5%0` NaN. The former
828
+ // `if rn == 0.0 { NaN }` folded `5/0` to NaN at compile time, diverging from every runtime
829
+ // path (which all produce Infinity) — a constant-fold-vs-runtime inconsistency.
830
+ Div => Literal::Number(ln / rn),
831
+ Mod => Literal::Number(ln % rn),
734
832
  Pow => Literal::Number(ln.powf(rn)),
735
833
  Eq => Literal::Bool(literal_strict_eq(left, right)),
736
834
  Ne => Literal::Bool(!literal_strict_eq(left, right)),
737
835
  StrictEq => Literal::Bool(literal_strict_eq(left, right)),
738
836
  StrictNe => Literal::Bool(!literal_strict_eq(left, right)),
739
- Lt => Literal::Bool(ln < rn),
740
- Le => Literal::Bool(ln <= rn),
741
- Gt => Literal::Bool(ln > rn),
742
- Ge => Literal::Bool(ln >= rn),
837
+ // Relational ops fold lexicographically when BOTH operands are string
838
+ // literals (JS semantics — must match the VM/interp/native runtime), else
839
+ // numerically. A NaN-involved numeric comparison is always false.
840
+ Lt => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_lt())),
841
+ Le => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_le())),
842
+ Gt => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_gt())),
843
+ Ge => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_ge())),
743
844
  And => Literal::Bool(literal_is_truthy(left) && literal_is_truthy(right)),
744
845
  Or => Literal::Bool(literal_is_truthy(left) || literal_is_truthy(right)),
745
- BitAnd => Literal::Number((ln as i32 & rn as i32) as f64),
746
- BitOr => Literal::Number((ln as i32 | rn as i32) as f64),
747
- BitXor => Literal::Number((ln as i32 ^ rn as i32) as f64),
748
- Shl => Literal::Number(((ln as i32) << (rn as i32)) as f64),
749
- Shr => Literal::Number(((ln as i32) >> (rn as i32)) as f64),
846
+ // ToInt32/ToUint32 (modulo 2³², NaN/±Infinity 0), matching the VM/interp exactly.
847
+ BitAnd => Literal::Number((fold_to_int32(ln) & fold_to_int32(rn)) as f64),
848
+ BitOr => Literal::Number((fold_to_int32(ln) | fold_to_int32(rn)) as f64),
849
+ BitXor => Literal::Number((fold_to_int32(ln) ^ fold_to_int32(rn)) as f64),
850
+ Shl => Literal::Number(fold_to_int32(ln).wrapping_shl(fold_to_uint32(rn)) as f64),
851
+ Shr => Literal::Number(fold_to_int32(ln).wrapping_shr(fold_to_uint32(rn)) as f64),
852
+ UShr => Literal::Number(fold_to_uint32(ln).wrapping_shr(fold_to_uint32(rn)) as f64),
750
853
  In => return None, // Requires object/array on right
751
854
  };
752
855
  Some(result)
@@ -936,7 +1039,7 @@ fn try_fold_unary(op: UnaryOp, operand: &Literal) -> Option<Literal> {
936
1039
  Not => Literal::Bool(!literal_is_truthy(operand)),
937
1040
  Neg => Literal::Number(-literal_as_number(operand)),
938
1041
  Pos => Literal::Number(literal_as_number(operand)),
939
- BitNot => Literal::Number(!(literal_as_number(operand) as i32) as f64),
1042
+ BitNot => Literal::Number(!fold_to_int32(literal_as_number(operand)) as f64),
940
1043
  Void => Literal::Null,
941
1044
  };
942
1045
  Some(result)
@@ -6,9 +6,22 @@ use parser::Parser;
6
6
 
7
7
  use tishlang_ast::Program;
8
8
  use tishlang_lexer::Lexer;
9
+ pub use tishlang_lexer::LexerOptions;
9
10
 
11
+ /// Parse `source`, reading lexer options from the environment (e.g. `TISH_IGNORE_INDENT=1`
12
+ /// to ignore indentation syntax). Every backend funnels through here, so the env toggle
13
+ /// reaches run/build/dump-ast/fmt/lint/lsp uniformly.
10
14
  pub fn parse(source: &str) -> Result<Program, String> {
11
- let lexer = Lexer::new(source);
15
+ parse_with_options(source, LexerOptions::from_env())
16
+ }
17
+
18
+ /// Parse with explicit lexer options, bypassing the environment.
19
+ ///
20
+ /// With `LexerOptions { ignore_indent: true }`, indentation is treated as ordinary
21
+ /// whitespace and blocks must be brace-delimited — useful for debugging how nested
22
+ /// blocks transpile, since fully brace-delimited code parses identically either way.
23
+ pub fn parse_with_options(source: &str, options: LexerOptions) -> Result<Program, String> {
24
+ let lexer = Lexer::with_options(source, options);
12
25
  let tokens: Result<Vec<_>, _> = lexer.collect();
13
26
  let tokens = tokens?;
14
27
  let mut parser = Parser::new(&tokens);
@@ -329,4 +342,45 @@ mod tests {
329
342
  assert!(matches!(stmts[1], Statement::VarDecl { .. }));
330
343
  assert!(matches!(stmts[2], Statement::If { .. }));
331
344
  }
345
+
346
+ #[test]
347
+ fn ignore_indent_parses_brace_blocks_identically() {
348
+ // Fully brace-delimited code: braces are authoritative, indentation is decoration.
349
+ // Ignoring indentation must therefore produce an identical AST.
350
+ let src = "fn f() {\n let a = 1\n if (a) {\n let b = 2\n g(b)\n }\n}\n";
351
+ let normal = parse(src).expect("parse (indentation significant)");
352
+ let ignored = parse_with_options(src, LexerOptions { ignore_indent: true })
353
+ .expect("parse (indentation ignored)");
354
+ assert_eq!(
355
+ format!("{normal:#?}"),
356
+ format!("{ignored:#?}"),
357
+ "brace-delimited code must parse identically with indentation ignored"
358
+ );
359
+ }
360
+
361
+ #[test]
362
+ fn ignore_indent_drops_indentation_induced_block() {
363
+ // A leading-indented line makes the lexer open an indent level, so the parser wraps
364
+ // `a()` in a `Block` — the kind of stray, indentation-driven nesting that can give
365
+ // transpiled JS the wrong lexical scope. Ignoring indentation removes that wrapper.
366
+ let src = " a()\nb()\n";
367
+
368
+ let normal = parse(src).expect("parse normal");
369
+ assert!(
370
+ matches!(normal.statements.first(), Some(Statement::Block { .. })),
371
+ "indentation should wrap a() in a Block, got: {:?}",
372
+ normal.statements
373
+ );
374
+
375
+ let ignored = parse_with_options(src, LexerOptions { ignore_indent: true })
376
+ .expect("parse ignored");
377
+ assert!(
378
+ ignored
379
+ .statements
380
+ .iter()
381
+ .all(|s| matches!(s, Statement::ExprStmt { .. })),
382
+ "with indentation ignored, both calls are flat expression statements, got: {:?}",
383
+ ignored.statements
384
+ );
385
+ }
332
386
  }