@tishlang/tish-format 2.16.13 → 2.35.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 (50) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fs_parity.rs +103 -0
  7. package/crates/tish/tests/shortcircuit.rs +3 -3
  8. package/crates/tish_ast/src/ast.rs +77 -3
  9. package/crates/tish_builtins/Cargo.toml +1 -0
  10. package/crates/tish_builtins/src/array.rs +195 -16
  11. package/crates/tish_builtins/src/collections.rs +37 -2
  12. package/crates/tish_builtins/src/globals.rs +215 -25
  13. package/crates/tish_builtins/src/math.rs +65 -7
  14. package/crates/tish_builtins/src/number.rs +173 -0
  15. package/crates/tish_builtins/src/string.rs +168 -15
  16. package/crates/tish_bytecode/src/compiler.rs +56 -4
  17. package/crates/tish_bytecode/src/opcode.rs +4 -2
  18. package/crates/tish_compile/src/codegen.rs +513 -44
  19. package/crates/tish_compile/src/infer.rs +26 -11
  20. package/crates/tish_compile/src/resolve.rs +87 -10
  21. package/crates/tish_compile_js/src/codegen.rs +82 -24
  22. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  23. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  24. package/crates/tish_core/src/json.rs +5 -1
  25. package/crates/tish_core/src/lib.rs +48 -2
  26. package/crates/tish_core/src/uri.rs +45 -0
  27. package/crates/tish_core/src/value.rs +6 -0
  28. package/crates/tish_eval/Cargo.toml +2 -1
  29. package/crates/tish_eval/src/eval.rs +655 -27
  30. package/crates/tish_eval/src/natives.rs +264 -18
  31. package/crates/tish_eval/src/regex.rs +47 -1
  32. package/crates/tish_fmt/Cargo.toml +1 -1
  33. package/crates/tish_fmt/src/lib.rs +23 -4
  34. package/crates/tish_opt/src/lib.rs +20 -1
  35. package/crates/tish_parser/src/lib.rs +41 -0
  36. package/crates/tish_parser/src/parser.rs +180 -11
  37. package/crates/tish_resolve/src/lib.rs +66 -5
  38. package/crates/tish_runtime/Cargo.toml +5 -1
  39. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  40. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  41. package/crates/tish_runtime/src/lib.rs +128 -9
  42. package/crates/tish_ui/src/jsx.rs +18 -3
  43. package/crates/tish_vm/src/jit.rs +1 -1
  44. package/crates/tish_vm/src/vm.rs +270 -60
  45. package/package.json +1 -1
  46. package/platform/darwin-arm64/tish-fmt +0 -0
  47. package/platform/darwin-x64/tish-fmt +0 -0
  48. package/platform/linux-arm64/tish-fmt +0 -0
  49. package/platform/linux-x64/tish-fmt +0 -0
  50. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -1290,6 +1290,21 @@ impl<'a> Parser<'a> {
1290
1290
  span: self.span_end(span_start),
1291
1291
  });
1292
1292
  }
1293
+ // `for (let k in obj)` — after a declared loop variable, `in` is for-in enumeration, not
1294
+ // the binary `in` operator (#413). Iterates the object's own enumerable keys.
1295
+ if matches!(self.peek_kind(), Some(TokenKind::In)) {
1296
+ self.advance();
1297
+ let object = self.parse_expr()?;
1298
+ self.expect(TokenKind::RParen)?;
1299
+ let body = Box::new(self.parse_block_or_statement()?);
1300
+ return Ok(Statement::ForIn {
1301
+ name,
1302
+ name_span,
1303
+ object,
1304
+ body,
1305
+ span: self.span_end(span_start),
1306
+ });
1307
+ }
1293
1308
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
1294
1309
  self.advance();
1295
1310
  Some(self.parse_type_annotation()?)
@@ -1667,7 +1682,15 @@ impl<'a> Parser<'a> {
1667
1682
  }
1668
1683
  }
1669
1684
  self.expect(TokenKind::RBrace)?;
1670
- let from = self.parse_from_clause()?;
1685
+ // #415: the `from` clause is OPTIONAL. `export { a, b as c } from "./m"` re-exports;
1686
+ // `export { a, b as c }` (no `from`) is a LOCAL named export of already-declared bindings.
1687
+ let from = if matches!(self.peek_kind(), Some(TokenKind::Ident))
1688
+ && self.peek().and_then(|t| t.literal.as_deref()) == Some("from")
1689
+ {
1690
+ Some(self.parse_from_clause()?)
1691
+ } else {
1692
+ None
1693
+ };
1671
1694
  ExportDeclaration::ReExport {
1672
1695
  specifiers,
1673
1696
  all: false,
@@ -1675,9 +1698,9 @@ impl<'a> Parser<'a> {
1675
1698
  span: self.span_end(span_start),
1676
1699
  }
1677
1700
  } else if matches!(self.peek_kind(), Some(TokenKind::Star)) {
1678
- // #305: re-export all — `export * from "./m"`
1701
+ // #305: re-export all — `export * from "./m"` (always requires `from`).
1679
1702
  self.advance(); // *
1680
- let from = self.parse_from_clause()?;
1703
+ let from = Some(self.parse_from_clause()?);
1681
1704
  ExportDeclaration::ReExport {
1682
1705
  specifiers: Vec::new(),
1683
1706
  all: true,
@@ -1977,9 +2000,54 @@ impl<'a> Parser<'a> {
1977
2000
  binary_single_op!(parse_bit_and, parse_shift, BitAnd, BinOp::BitAnd);
1978
2001
 
1979
2002
  binary_multi_op!(parse_shift, parse_equality, Shl => BinOp::Shl, Shr => BinOp::Shr, UShr => BinOp::UShr);
1980
- binary_multi_op!(parse_equality, parse_comparison,
1981
- StrictEq => BinOp::StrictEq, StrictNe => BinOp::StrictNe,
1982
- Eq => BinOp::Eq, Ne => BinOp::Ne);
2003
+
2004
+ // Loose equality `==` / `!=` is rejected at parse, uniformly across every backend (#244). Its
2005
+ // coercion is a footgun, and leaving it accepted was a portability trap: interp/vm silently
2006
+ // treated `==` as strict, the `--target js` backend emitted true JS loose `==` (so `1 == "1"`
2007
+ // disagreed), and the native-rust backend refused to compile it. Only `===` / `!==` are accepted;
2008
+ // `Eq` / `Ne` produce an actionable error pointing at the strict form. `StrictEq` / `StrictNe`
2009
+ // parse exactly as the generic `binary_multi_op!` would.
2010
+ fn parse_equality(&mut self) -> Result<Expr, String> {
2011
+ let mut left = self.parse_comparison()?;
2012
+ loop {
2013
+ let op = match self.peek_kind() {
2014
+ Some(TokenKind::StrictEq) => BinOp::StrictEq,
2015
+ Some(TokenKind::StrictNe) => BinOp::StrictNe,
2016
+ Some(TokenKind::Eq) => {
2017
+ let at = self
2018
+ .peek()
2019
+ .map(|t| format!("{:?}", t.span))
2020
+ .unwrap_or_else(|| "EOF".to_string());
2021
+ return Err(format!(
2022
+ "Loose equality '==' is not supported — use strict '===' (at {})",
2023
+ at
2024
+ ));
2025
+ }
2026
+ Some(TokenKind::Ne) => {
2027
+ let at = self
2028
+ .peek()
2029
+ .map(|t| format!("{:?}", t.span))
2030
+ .unwrap_or_else(|| "EOF".to_string());
2031
+ return Err(format!(
2032
+ "Loose inequality '!=' is not supported — use strict '!==' (at {})",
2033
+ at
2034
+ ));
2035
+ }
2036
+ _ => break,
2037
+ };
2038
+ self.advance();
2039
+ let right = self.parse_comparison()?;
2040
+ let start = expr_span(&left).start;
2041
+ let end = expr_span(&right).end;
2042
+ left = Expr::Binary {
2043
+ left: Box::new(left),
2044
+ op,
2045
+ right: Box::new(right),
2046
+ span: Span { start, end },
2047
+ };
2048
+ }
2049
+ Ok(left)
2050
+ }
1983
2051
  binary_multi_op!(parse_comparison, parse_term,
1984
2052
  Lt => BinOp::Lt, Le => BinOp::Le, Gt => BinOp::Gt, Ge => BinOp::Ge, In => BinOp::In);
1985
2053
  binary_multi_op!(parse_term, parse_factor, Plus => BinOp::Add, Minus => BinOp::Sub);
@@ -2396,17 +2464,71 @@ impl<'a> Parser<'a> {
2396
2464
  value: Literal::Null,
2397
2465
  span,
2398
2466
  }),
2399
- // `fn` / `type` as a value reference or single-param arrow head (issue #55).
2400
- // `fn`/`type` function-expressions don't exist and statement-leading decls are
2401
- // handled in parse_statement, so treating them as identifiers here is additive.
2467
+ // `async` in expression position (#428): an async arrow — `async () => …` or
2468
+ // `async x => …`. The body runs in async context (`await` allowed). Async function
2469
+ // expressions / method shorthand need function-expression support, which tish lacks.
2470
+ TokenKind::Async => {
2471
+ if matches!(self.peek_kind(), Some(TokenKind::LParen)) {
2472
+ self.advance(); // consume `(`
2473
+ if let Some(arrow) = self.try_parse_arrow_function(&span, true)? {
2474
+ return Ok(arrow);
2475
+ }
2476
+ return Err("Expected an arrow function after `async (`".to_string());
2477
+ }
2478
+ if matches!(
2479
+ self.peek_kind(),
2480
+ Some(TokenKind::Ident | TokenKind::Fn | TokenKind::Type)
2481
+ ) {
2482
+ let name_tok = self.advance().ok_or("Expected parameter name")?;
2483
+ let name = name_tok.literal.clone().ok_or("Expected parameter name")?;
2484
+ let name_span = Span {
2485
+ start: name_tok.span.start,
2486
+ end: name_tok.span.end,
2487
+ };
2488
+ self.expect(TokenKind::Arrow)?;
2489
+ let body = self.parse_arrow_body()?;
2490
+ let end = self.previous_span_end();
2491
+ return Ok(Expr::ArrowFunction {
2492
+ async_: true,
2493
+ params: vec![FunParam::Simple(TypedParam {
2494
+ name,
2495
+ name_span,
2496
+ type_ann: None,
2497
+ default: None,
2498
+ })],
2499
+ body,
2500
+ span: Span {
2501
+ start: span.start,
2502
+ end,
2503
+ },
2504
+ });
2505
+ }
2506
+ Err(
2507
+ "`async` in expression position must be followed by an arrow function (`async () => …`)"
2508
+ .to_string(),
2509
+ )
2510
+ }
2511
+ // `fn` / `type` as a value reference or single-param arrow head (issue #55), OR a function
2512
+ // EXPRESSION `function (…) {…}` / named `function f(…) {…}` (#464). statement-leading
2513
+ // `function`/`type` decls are handled in parse_statement.
2402
2514
  TokenKind::Ident | TokenKind::Fn | TokenKind::Type => {
2403
2515
  let name = t.literal.clone().ok_or("Expected ident")?;
2516
+ let is_fn_kw = t.kind == TokenKind::Fn;
2517
+ // Function expression: `function` (`Fn`) followed by `(` (anonymous) or an identifier
2518
+ // (named) has no other valid meaning in value position. `t` is no longer read past
2519
+ // here, so borrowing `self` for the peek is sound.
2520
+ if is_fn_kw
2521
+ && matches!(self.peek_kind(), Some(TokenKind::LParen | TokenKind::Ident))
2522
+ {
2523
+ return self.parse_function_expr_tail(&span, false);
2524
+ }
2404
2525
  // Check if this is a single-param arrow function: x => ...
2405
2526
  if matches!(self.peek_kind(), Some(TokenKind::Arrow)) {
2406
2527
  self.advance(); // consume =>
2407
2528
  let body = self.parse_arrow_body()?;
2408
2529
  let end = self.previous_span_end();
2409
2530
  return Ok(Expr::ArrowFunction {
2531
+ async_: false,
2410
2532
  params: vec![FunParam::Simple(TypedParam {
2411
2533
  name: name.clone(),
2412
2534
  name_span: span,
@@ -2424,7 +2546,7 @@ impl<'a> Parser<'a> {
2424
2546
  }
2425
2547
  TokenKind::LParen => {
2426
2548
  // Check if this is an arrow function: (params) => ...
2427
- if let Some(arrow_fn) = self.try_parse_arrow_function(&span)? {
2549
+ if let Some(arrow_fn) = self.try_parse_arrow_function(&span, false)? {
2428
2550
  return Ok(arrow_fn);
2429
2551
  }
2430
2552
  // Otherwise it's a grouping expression
@@ -2584,7 +2706,11 @@ impl<'a> Parser<'a> {
2584
2706
 
2585
2707
  /// Try to parse an arrow function starting with '(' already consumed.
2586
2708
  /// Returns Some(Expr) if successful, None if it's not an arrow function.
2587
- fn try_parse_arrow_function(&mut self, start_span: &Span) -> Result<Option<Expr>, String> {
2709
+ fn try_parse_arrow_function(
2710
+ &mut self,
2711
+ start_span: &Span,
2712
+ async_: bool,
2713
+ ) -> Result<Option<Expr>, String> {
2588
2714
  // Save position for backtracking
2589
2715
  let saved_pos = self.pos;
2590
2716
 
@@ -2638,6 +2764,7 @@ impl<'a> Parser<'a> {
2638
2764
  let end = self.previous_span_end();
2639
2765
 
2640
2766
  Ok(Some(Expr::ArrowFunction {
2767
+ async_,
2641
2768
  params,
2642
2769
  body,
2643
2770
  span: Span {
@@ -2647,6 +2774,48 @@ impl<'a> Parser<'a> {
2647
2774
  }))
2648
2775
  }
2649
2776
 
2777
+ /// Parse a `function` EXPRESSION after its `function` token has been consumed (#464):
2778
+ /// `[name] (params) [: RetType] { body }`. Lowers to an `ArrowFunction` with a block body so every
2779
+ /// backend reuses the existing arrow path. An optional name is parsed for source fidelity but not
2780
+ /// bound in scope, so a named function expression cannot yet self-reference by that name (a
2781
+ /// documented follow-up). `this`/`arguments` follow arrow semantics — fine for the common callback
2782
+ /// / IIFE / assigned-callback uses.
2783
+ fn parse_function_expr_tail(
2784
+ &mut self,
2785
+ start_span: &Span,
2786
+ async_: bool,
2787
+ ) -> Result<Expr, String> {
2788
+ // Optional name (named function expression).
2789
+ if matches!(self.peek_kind(), Some(TokenKind::Ident)) {
2790
+ self.advance();
2791
+ }
2792
+ self.expect(TokenKind::LParen)?;
2793
+ let mut params = Vec::new();
2794
+ while !matches!(self.peek_kind(), Some(TokenKind::RParen)) {
2795
+ params.push(self.parse_fun_param()?);
2796
+ if !matches!(self.peek_kind(), Some(TokenKind::RParen)) {
2797
+ self.expect(TokenKind::Comma)?;
2798
+ }
2799
+ }
2800
+ self.expect(TokenKind::RParen)?;
2801
+ // Optional return-type annotation (`: T`) — parsed and discarded (ArrowFunction carries none).
2802
+ if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
2803
+ self.advance();
2804
+ self.parse_type_annotation()?;
2805
+ }
2806
+ let block = self.parse_block()?;
2807
+ let end = self.previous_span_end();
2808
+ Ok(Expr::ArrowFunction {
2809
+ async_,
2810
+ params,
2811
+ body: ArrowBody::Block(Box::new(block)),
2812
+ span: Span {
2813
+ start: start_span.start,
2814
+ end,
2815
+ },
2816
+ })
2817
+ }
2818
+
2650
2819
  /// Parse the body of an arrow function (either expression or block)
2651
2820
  fn parse_arrow_body(&mut self) -> Result<ArrowBody, String> {
2652
2821
  if matches!(self.peek_kind(), Some(TokenKind::LBrace)) {
@@ -177,6 +177,13 @@ fn collect_stmt(
177
177
  iterable,
178
178
  body,
179
179
  ..
180
+ }
181
+ | Statement::ForIn {
182
+ name,
183
+ name_span,
184
+ object: iterable,
185
+ body,
186
+ ..
180
187
  } => {
181
188
  consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
182
189
  collect_expr(iterable, source, lsp_line, lsp_char, best);
@@ -939,7 +946,12 @@ fn member_chain_collect_stmt(
939
946
  }
940
947
  member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
941
948
  }
942
- Statement::ForOf { iterable, body, .. } => {
949
+ Statement::ForOf { iterable, body, .. }
950
+ | Statement::ForIn {
951
+ object: iterable,
952
+ body,
953
+ ..
954
+ } => {
943
955
  member_chain_collect_expr(iterable, source, lsp_line, lsp_char, best);
944
956
  member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
945
957
  }
@@ -1414,6 +1426,13 @@ fn walk_stmt_resolve(
1414
1426
  iterable,
1415
1427
  body,
1416
1428
  ..
1429
+ }
1430
+ | Statement::ForIn {
1431
+ name,
1432
+ name_span,
1433
+ object: iterable,
1434
+ body,
1435
+ ..
1417
1436
  } => {
1418
1437
  if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1419
1438
  return Some(*name_span);
@@ -2028,6 +2047,13 @@ fn check_unresolved_stmt(
2028
2047
  iterable,
2029
2048
  body,
2030
2049
  ..
2050
+ }
2051
+ | Statement::ForIn {
2052
+ name,
2053
+ name_span,
2054
+ object: iterable,
2055
+ body,
2056
+ ..
2031
2057
  } => {
2032
2058
  check_unresolved_expr(iterable, scopes, out);
2033
2059
  scopes.push();
@@ -2556,6 +2582,13 @@ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>)
2556
2582
  iterable,
2557
2583
  body,
2558
2584
  ..
2585
+ }
2586
+ | Statement::ForIn {
2587
+ name,
2588
+ name_span,
2589
+ object: iterable,
2590
+ body,
2591
+ ..
2559
2592
  } => {
2560
2593
  out.push(BindingSite {
2561
2594
  name: name.clone(),
@@ -2731,7 +2764,12 @@ fn jsx_tags_stmt(s: &Statement, out: &mut std::collections::HashSet<Arc<str>>) {
2731
2764
  }
2732
2765
  jsx_tags_stmt(body, out);
2733
2766
  }
2734
- Statement::ForOf { iterable, body, .. } => {
2767
+ Statement::ForOf { iterable, body, .. }
2768
+ | Statement::ForIn {
2769
+ object: iterable,
2770
+ body,
2771
+ ..
2772
+ } => {
2735
2773
  jsx_tags_expr(iterable, out);
2736
2774
  jsx_tags_stmt(body, out);
2737
2775
  }
@@ -3057,6 +3095,13 @@ fn collect_block_locals(
3057
3095
  body,
3058
3096
  span,
3059
3097
  ..
3098
+ }
3099
+ | Statement::ForIn {
3100
+ name,
3101
+ name_span,
3102
+ body,
3103
+ span,
3104
+ ..
3060
3105
  } => {
3061
3106
  if pos::span_contains_lsp_position(source, span, lsp_line, lsp_char)
3062
3107
  && pos::span_contains_lsp_position(
@@ -3428,6 +3473,12 @@ fn walk_stmt_completion(
3428
3473
  iterable,
3429
3474
  body,
3430
3475
  ..
3476
+ }
3477
+ | Statement::ForIn {
3478
+ name,
3479
+ object: iterable,
3480
+ body,
3481
+ ..
3431
3482
  } => {
3432
3483
  walk_expr_completion(iterable, source, lsp_line, lsp_char, stack, best);
3433
3484
  let body_sp = body.as_ref().span();
@@ -3900,7 +3951,12 @@ fn tref_stmt(stmt: &Statement, out: &mut Vec<TypeOcc>) {
3900
3951
  }
3901
3952
  tref_stmt(body, out);
3902
3953
  }
3903
- Statement::ForOf { iterable, body, .. } => {
3954
+ Statement::ForOf { iterable, body, .. }
3955
+ | Statement::ForIn {
3956
+ object: iterable,
3957
+ body,
3958
+ ..
3959
+ } => {
3904
3960
  tref_expr(iterable, out);
3905
3961
  tref_stmt(body, out);
3906
3962
  }
@@ -4061,7 +4117,12 @@ fn refs_stmt(
4061
4117
  }
4062
4118
  refs_stmt(body, program, source, name, def_span, out);
4063
4119
  }
4064
- Statement::ForOf { iterable, body, .. } => {
4120
+ Statement::ForOf { iterable, body, .. }
4121
+ | Statement::ForIn {
4122
+ object: iterable,
4123
+ body,
4124
+ ..
4125
+ } => {
4065
4126
  refs_expr(iterable, program, source, name, def_span, out);
4066
4127
  refs_stmt(body, program, source, name, def_span, out);
4067
4128
  }
@@ -4657,7 +4718,7 @@ mod tests {
4657
4718
 
4658
4719
  #[test]
4659
4720
  fn mutual_recursion_not_unresolved_or_unused() {
4660
- let src = "fn even(n) { if (n == 0) { return true } return odd(n - 1) }\nfn odd(n) { if (n == 0) { return false } return even(n - 1) }\neven(10)\n";
4721
+ let src = "fn even(n) { if (n === 0) { return true } return odd(n - 1) }\nfn odd(n) { if (n === 0) { return false } return even(n - 1) }\neven(10)\n";
4661
4722
  let program = parse(src).expect("parse");
4662
4723
  assert!(
4663
4724
  collect_unresolved_identifiers(&program).is_empty(),
@@ -52,7 +52,9 @@ http-hyper = [
52
52
  "tokio/rt",
53
53
  "tokio/io-util",
54
54
  ]
55
- fs = []
55
+ # `fs/promises` returns settled promises, so fs pulls in the promise machinery.
56
+ # `mkdtemp` needs a random suffix (predictable/timestamp names are a temp-dir security smell).
57
+ fs = ["promise", "dep:rand"]
56
58
  process = []
57
59
  # Interactive terminal I/O (issue #101): raw mode, key/resize events, size, alt screen.
58
60
  tty = ["dep:crossterm"]
@@ -81,6 +83,8 @@ reqwest = { version = "0.13.2", default-features = false, features = ["rustls",
81
83
  futures = { version = "0.3.32", optional = true }
82
84
  bytes = { version = "1", optional = true }
83
85
  tiny_http = { version = "0.12.0", optional = true }
86
+ # fs feature only: random suffix for `mkdtemp`.
87
+ rand = { version = "0.10.1", optional = true }
84
88
  socket2 = { version = "0.5", features = ["all"], optional = true }
85
89
  libc = { version = "0.2", optional = true }
86
90
  arc-swap = { version = "1", optional = true }