@tishlang/tish-lsp 2.12.0 → 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 (88) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish-lsp +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  4. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  5. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  6. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/integration_test.rs +80 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +84 -0
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +724 -72
  13. package/crates/tish_builtins/src/collections.rs +149 -40
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +263 -23
  18. package/crates/tish_bytecode/src/chunk.rs +7 -0
  19. package/crates/tish_bytecode/src/compiler.rs +611 -63
  20. package/crates/tish_bytecode/src/lib.rs +1 -1
  21. package/crates/tish_bytecode/src/opcode.rs +156 -2
  22. package/crates/tish_bytecode/src/serialize.rs +2 -0
  23. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  24. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  25. package/crates/tish_compile/src/codegen.rs +18219 -5283
  26. package/crates/tish_compile/src/infer.rs +1562 -30
  27. package/crates/tish_compile/src/lib.rs +29 -4
  28. package/crates/tish_compile/src/resolve.rs +151 -13
  29. package/crates/tish_compile/src/types.rs +224 -28
  30. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  31. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  32. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  33. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  34. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  35. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  36. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  37. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  38. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  39. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  40. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  41. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  42. package/crates/tish_compile_js/src/codegen.rs +94 -3
  43. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  44. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
  45. package/crates/tish_core/Cargo.toml +4 -0
  46. package/crates/tish_core/src/json.rs +109 -6
  47. package/crates/tish_core/src/lib.rs +222 -2
  48. package/crates/tish_core/src/shape.rs +4 -2
  49. package/crates/tish_core/src/uri.rs +45 -0
  50. package/crates/tish_core/src/value.rs +571 -35
  51. package/crates/tish_core/src/vmref.rs +14 -0
  52. package/crates/tish_eval/Cargo.toml +2 -1
  53. package/crates/tish_eval/src/eval.rs +1328 -101
  54. package/crates/tish_eval/src/natives.rs +283 -18
  55. package/crates/tish_eval/src/regex.rs +47 -1
  56. package/crates/tish_eval/src/value.rs +76 -21
  57. package/crates/tish_ffi/src/lib.rs +11 -1
  58. package/crates/tish_ffi/tests/double_free.rs +35 -0
  59. package/crates/tish_fmt/src/lib.rs +94 -1
  60. package/crates/tish_lexer/src/lib.rs +76 -0
  61. package/crates/tish_lexer/src/token.rs +4 -0
  62. package/crates/tish_lint/src/lib.rs +126 -0
  63. package/crates/tish_lsp/Cargo.toml +1 -1
  64. package/crates/tish_lsp/README.md +2 -1
  65. package/crates/tish_lsp/src/main.rs +378 -28
  66. package/crates/tish_native/src/build.rs +41 -0
  67. package/crates/tish_opt/src/lib.rs +20 -1
  68. package/crates/tish_parser/Cargo.toml +4 -0
  69. package/crates/tish_parser/src/lib.rs +68 -0
  70. package/crates/tish_parser/src/parser.rs +479 -28
  71. package/crates/tish_resolve/src/lib.rs +75 -5
  72. package/crates/tish_runtime/Cargo.toml +10 -1
  73. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  74. package/crates/tish_runtime/src/http.rs +28 -10
  75. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  76. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  77. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  78. package/crates/tish_runtime/src/lib.rs +651 -51
  79. package/crates/tish_runtime/src/timers.rs +49 -2
  80. package/crates/tish_ui/src/jsx.rs +21 -3
  81. package/crates/tish_vm/src/jit.rs +2514 -117
  82. package/crates/tish_vm/src/vm.rs +1479 -224
  83. package/package.json +1 -1
  84. package/platform/darwin-arm64/tish-lsp +0 -0
  85. package/platform/darwin-x64/tish-lsp +0 -0
  86. package/platform/linux-arm64/tish-lsp +0 -0
  87. package/platform/linux-x64/tish-lsp +0 -0
  88. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -123,8 +123,17 @@ pub struct Parser<'a> {
123
123
  generic_specializations: Vec<Statement>,
124
124
  /// Names of specializations already generated (dedup).
125
125
  generic_done: HashSet<String>,
126
+ /// Current recursion depth of the expression/statement descent, bounded by [`MAX_PARSE_DEPTH`] so
127
+ /// pathologically nested untrusted source (`((((…`, `[[[[…`, `{{{{…`, `-!-!…`) returns a CATCHABLE
128
+ /// parse error instead of overflowing the native stack and aborting the whole process (#381).
129
+ depth: usize,
126
130
  }
127
131
 
132
+ /// Maximum expression/statement nesting the parser will descend before returning a catchable error.
133
+ /// Deep enough for any realistic hand-written or generated source; shallow enough that the descent
134
+ /// stays well within the OS thread stack. Mirrors `MAX_JSON_DEPTH` in the JSON parser.
135
+ const MAX_PARSE_DEPTH: usize = 1024;
136
+
128
137
  impl<'a> Parser<'a> {
129
138
  pub fn new(tokens: &'a [Token]) -> Self {
130
139
  Self {
@@ -134,9 +143,25 @@ impl<'a> Parser<'a> {
134
143
  generic_aliases: HashMap::new(),
135
144
  generic_specializations: Vec::new(),
136
145
  generic_done: HashSet::new(),
146
+ depth: 0,
137
147
  }
138
148
  }
139
149
 
150
+ /// Enter one level of recursive descent. Returns a catchable `Err` (never a panic/abort) once the
151
+ /// nesting exceeds [`MAX_PARSE_DEPTH`]. Pair every `Ok(())` return with a matching `self.depth -=
152
+ /// 1` on the way out (the `parse_unary`/`parse_statement` wrappers do this unconditionally). #381
153
+ fn enter_depth(&mut self) -> Result<(), String> {
154
+ self.depth += 1;
155
+ if self.depth > MAX_PARSE_DEPTH {
156
+ self.depth -= 1;
157
+ return Err(format!(
158
+ "parse error: nesting too deep (exceeded {} levels)",
159
+ MAX_PARSE_DEPTH
160
+ ));
161
+ }
162
+ Ok(())
163
+ }
164
+
140
165
  /// Close one generic-arg `>`: use a previously-split `>>` debt, consume a `>`, or split a
141
166
  /// `>>` (consuming it and owing one `>` to the parent). Returns false if there's no closer.
142
167
  fn try_close_angle(&mut self) -> bool {
@@ -259,7 +284,16 @@ impl<'a> Parser<'a> {
259
284
  Ok(Program { statements })
260
285
  }
261
286
 
287
+ /// Depth-guarded wrapper (#381): bounds block/statement nesting (`{{{{…`, `if(x){if(x){…`) so a
288
+ /// crafted deeply-nested program returns a catchable error instead of a stack-overflow abort.
262
289
  fn parse_statement(&mut self) -> Result<Statement, String> {
290
+ self.enter_depth()?;
291
+ let r = stacker::maybe_grow(64 * 1024, 512 * 1024, || self.parse_statement_inner());
292
+ self.depth -= 1;
293
+ r
294
+ }
295
+
296
+ fn parse_statement_inner(&mut self) -> Result<Statement, String> {
263
297
  let kind = self.peek_kind().ok_or("Unexpected EOF")?;
264
298
  let span_start = self.peek().map(|t| t.span.start).unwrap_or((0, 0));
265
299
 
@@ -1256,6 +1290,21 @@ impl<'a> Parser<'a> {
1256
1290
  span: self.span_end(span_start),
1257
1291
  });
1258
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
+ }
1259
1308
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
1260
1309
  self.advance();
1261
1310
  Some(self.parse_type_annotation()?)
@@ -1579,9 +1628,86 @@ impl<'a> Parser<'a> {
1579
1628
  })
1580
1629
  }
1581
1630
 
1631
+ /// #305 — consume a `from "..."` clause (`from` is a soft keyword: an `Ident` whose spelling is
1632
+ /// "from"), returning the module path string.
1633
+ fn parse_from_clause(&mut self) -> Result<Arc<str>, String> {
1634
+ let from_tok = self.expect(TokenKind::Ident)?;
1635
+ if from_tok.literal.as_deref() != Some("from") {
1636
+ return Err("Expected 'from' in re-export statement".to_string());
1637
+ }
1638
+ self.expect(TokenKind::String)?
1639
+ .literal
1640
+ .clone()
1641
+ .ok_or_else(|| "Expected string path in re-export".to_string())
1642
+ }
1643
+
1582
1644
  fn parse_export(&mut self) -> Result<Statement, String> {
1583
1645
  let span_start = self.expect(TokenKind::Export)?.span.start;
1584
- let declaration = if matches!(self.peek_kind(), Some(TokenKind::Default)) {
1646
+ let declaration = if matches!(self.peek_kind(), Some(TokenKind::LBrace)) {
1647
+ // #305: re-export named — `export { foo, bar as inc } from "./m"`
1648
+ self.advance(); // {
1649
+ let mut specifiers = Vec::new();
1650
+ while !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
1651
+ let name_tok = self.expect(TokenKind::Ident)?;
1652
+ let name_span = Span {
1653
+ start: name_tok.span.start,
1654
+ end: name_tok.span.end,
1655
+ };
1656
+ let name = name_tok
1657
+ .literal
1658
+ .clone()
1659
+ .ok_or("Expected identifier in export")?;
1660
+ let (alias, alias_span) = if matches!(self.peek_kind(), Some(TokenKind::As)) {
1661
+ self.advance(); // as
1662
+ let alias_tok = self.expect(TokenKind::Ident)?;
1663
+ let asp = Span {
1664
+ start: alias_tok.span.start,
1665
+ end: alias_tok.span.end,
1666
+ };
1667
+ (
1668
+ Some(alias_tok.literal.clone().ok_or("Expected alias after 'as'")?),
1669
+ Some(asp),
1670
+ )
1671
+ } else {
1672
+ (None, None)
1673
+ };
1674
+ specifiers.push(ImportSpecifier::Named {
1675
+ name,
1676
+ name_span,
1677
+ alias,
1678
+ alias_span,
1679
+ });
1680
+ if !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
1681
+ self.expect(TokenKind::Comma)?;
1682
+ }
1683
+ }
1684
+ self.expect(TokenKind::RBrace)?;
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
+ };
1694
+ ExportDeclaration::ReExport {
1695
+ specifiers,
1696
+ all: false,
1697
+ from,
1698
+ span: self.span_end(span_start),
1699
+ }
1700
+ } else if matches!(self.peek_kind(), Some(TokenKind::Star)) {
1701
+ // #305: re-export all — `export * from "./m"` (always requires `from`).
1702
+ self.advance(); // *
1703
+ let from = Some(self.parse_from_clause()?);
1704
+ ExportDeclaration::ReExport {
1705
+ specifiers: Vec::new(),
1706
+ all: true,
1707
+ from,
1708
+ span: self.span_end(span_start),
1709
+ }
1710
+ } else if matches!(self.peek_kind(), Some(TokenKind::Default)) {
1585
1711
  self.advance();
1586
1712
  let expr = self.parse_expr()?;
1587
1713
  ExportDeclaration::Default(expr)
@@ -1617,6 +1743,73 @@ impl<'a> Parser<'a> {
1617
1743
  self.parse_assign()
1618
1744
  }
1619
1745
 
1746
+ /// #304 — is `e` a side-effect-free l-value whose object/index sub-expressions can be safely
1747
+ /// DUPLICATED by a compound/inc-dec desugar (no Call/New/assignment/inc-dec inside)? The desugar
1748
+ /// reads and writes the same target, so duplicating a side-effecting sub-expression would run it
1749
+ /// twice; those targets keep the existing parse error rather than mis-evaluate.
1750
+ fn is_simple_lvalue(e: &Expr) -> bool {
1751
+ match e {
1752
+ Expr::Ident { .. } | Expr::Literal { .. } => true,
1753
+ Expr::Member { object, prop, .. } => {
1754
+ Self::is_simple_lvalue(object)
1755
+ && match prop {
1756
+ MemberProp::Name { .. } => true,
1757
+ MemberProp::Expr(pe) => Self::is_simple_lvalue(pe),
1758
+ }
1759
+ }
1760
+ Expr::Index { object, index, .. } => {
1761
+ Self::is_simple_lvalue(object) && Self::is_simple_lvalue(index)
1762
+ }
1763
+ _ => false,
1764
+ }
1765
+ }
1766
+
1767
+ /// #304 — does `e` name a member (`obj.x`) or index (`arr[i]`) target the read-modify-write
1768
+ /// desugar can handle (a simple l-value)?
1769
+ fn is_desugarable_member_target(e: &Expr) -> bool {
1770
+ Self::is_simple_lvalue(e)
1771
+ && matches!(
1772
+ e,
1773
+ Expr::Member { prop: MemberProp::Name { .. }, .. } | Expr::Index { .. }
1774
+ )
1775
+ }
1776
+
1777
+ fn compound_op_to_binop(op: CompoundOp) -> BinOp {
1778
+ match op {
1779
+ CompoundOp::Add => BinOp::Add,
1780
+ CompoundOp::Sub => BinOp::Sub,
1781
+ CompoundOp::Mul => BinOp::Mul,
1782
+ CompoundOp::Div => BinOp::Div,
1783
+ CompoundOp::Mod => BinOp::Mod,
1784
+ }
1785
+ }
1786
+
1787
+ /// #304 — desugar a read-modify-write on a member/index target into the existing
1788
+ /// `MemberAssign`/`IndexAssign` node (so every backend handles it for free). `make_value(read)`
1789
+ /// builds the stored value from a clone of the target read (e.g. `read + 1`, `read || rhs`).
1790
+ /// Caller must have checked [`Self::is_desugarable_member_target`].
1791
+ fn desugar_member_rmw(left: &Expr, span: Span, make_value: impl FnOnce(Expr) -> Expr) -> Expr {
1792
+ match left {
1793
+ Expr::Member {
1794
+ object,
1795
+ prop: MemberProp::Name { name, .. },
1796
+ ..
1797
+ } => Expr::MemberAssign {
1798
+ object: object.clone(),
1799
+ prop: Arc::clone(name),
1800
+ value: Box::new(make_value(left.clone())),
1801
+ span,
1802
+ },
1803
+ Expr::Index { object, index, .. } => Expr::IndexAssign {
1804
+ object: object.clone(),
1805
+ index: index.clone(),
1806
+ value: Box::new(make_value(left.clone())),
1807
+ span,
1808
+ },
1809
+ _ => unreachable!("desugar_member_rmw on non-member/index target"),
1810
+ }
1811
+ }
1812
+
1620
1813
  fn parse_assign(&mut self) -> Result<Expr, String> {
1621
1814
  let left = self.parse_conditional()?;
1622
1815
 
@@ -1686,18 +1879,29 @@ impl<'a> Parser<'a> {
1686
1879
  };
1687
1880
 
1688
1881
  if let Some(op) = compound_op {
1689
- if let Expr::Ident { name, span } = &left {
1690
- let name = Arc::clone(name);
1691
- let start = span.start;
1882
+ let is_ident = matches!(&left, Expr::Ident { .. });
1883
+ if is_ident || Self::is_desugarable_member_target(&left) {
1884
+ let start = left.span().start;
1692
1885
  self.advance(); // consume the compound operator
1693
1886
  let value = self.parse_assign()?;
1694
1887
  let end = value.span().end;
1695
- return Ok(Expr::CompoundAssign {
1696
- name,
1697
- op,
1698
- value: Box::new(value),
1699
- span: Span { start, end },
1700
- });
1888
+ let span = Span { start, end };
1889
+ if let Expr::Ident { name, .. } = &left {
1890
+ return Ok(Expr::CompoundAssign {
1891
+ name: Arc::clone(name),
1892
+ op,
1893
+ value: Box::new(value),
1894
+ span,
1895
+ });
1896
+ }
1897
+ // #304: `obj.x += e` / `arr[i] *= e` -> MemberAssign/IndexAssign(read <op> e).
1898
+ let binop = Self::compound_op_to_binop(op);
1899
+ return Ok(Self::desugar_member_rmw(&left, span, move |read| Expr::Binary {
1900
+ left: Box::new(read),
1901
+ op: binop,
1902
+ right: Box::new(value),
1903
+ span,
1904
+ }));
1701
1905
  }
1702
1906
  }
1703
1907
 
@@ -1710,18 +1914,42 @@ impl<'a> Parser<'a> {
1710
1914
  };
1711
1915
 
1712
1916
  if let Some(op) = logical_op {
1713
- if let Expr::Ident { name, span } = &left {
1714
- let name = Arc::clone(name);
1715
- let start = span.start;
1917
+ let is_ident = matches!(&left, Expr::Ident { .. });
1918
+ if is_ident || Self::is_desugarable_member_target(&left) {
1919
+ let start = left.span().start;
1716
1920
  self.advance(); // consume the logical assign operator
1717
1921
  let value = self.parse_assign()?;
1718
1922
  let end = value.span().end;
1719
- return Ok(Expr::LogicalAssign {
1720
- name,
1721
- op,
1722
- value: Box::new(value),
1723
- span: Span { start, end },
1724
- });
1923
+ let span = Span { start, end };
1924
+ if let Expr::Ident { name, .. } = &left {
1925
+ return Ok(Expr::LogicalAssign {
1926
+ name: Arc::clone(name),
1927
+ op,
1928
+ value: Box::new(value),
1929
+ span,
1930
+ });
1931
+ }
1932
+ // #304: `obj.x ||= e` -> MemberAssign/IndexAssign(read || e) (short-circuit preserved,
1933
+ // since `||`/`&&`/`??` evaluate the RHS lazily).
1934
+ return Ok(Self::desugar_member_rmw(&left, span, move |read| match op {
1935
+ LogicalAssignOp::OrOr => Expr::Binary {
1936
+ left: Box::new(read),
1937
+ op: BinOp::Or,
1938
+ right: Box::new(value),
1939
+ span,
1940
+ },
1941
+ LogicalAssignOp::AndAnd => Expr::Binary {
1942
+ left: Box::new(read),
1943
+ op: BinOp::And,
1944
+ right: Box::new(value),
1945
+ span,
1946
+ },
1947
+ LogicalAssignOp::Nullish => Expr::NullishCoalesce {
1948
+ left: Box::new(read),
1949
+ right: Box::new(value),
1950
+ span,
1951
+ },
1952
+ }));
1725
1953
  }
1726
1954
  }
1727
1955
 
@@ -1772,9 +2000,54 @@ impl<'a> Parser<'a> {
1772
2000
  binary_single_op!(parse_bit_and, parse_shift, BitAnd, BinOp::BitAnd);
1773
2001
 
1774
2002
  binary_multi_op!(parse_shift, parse_equality, Shl => BinOp::Shl, Shr => BinOp::Shr, UShr => BinOp::UShr);
1775
- binary_multi_op!(parse_equality, parse_comparison,
1776
- StrictEq => BinOp::StrictEq, StrictNe => BinOp::StrictNe,
1777
- 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
+ }
1778
2051
  binary_multi_op!(parse_comparison, parse_term,
1779
2052
  Lt => BinOp::Lt, Le => BinOp::Le, Gt => BinOp::Gt, Ge => BinOp::Ge, In => BinOp::In);
1780
2053
  binary_multi_op!(parse_term, parse_factor, Plus => BinOp::Add, Minus => BinOp::Sub);
@@ -1797,7 +2070,19 @@ impl<'a> Parser<'a> {
1797
2070
  Ok(left)
1798
2071
  }
1799
2072
 
2073
+ /// Depth-guarded wrapper (#381): every expression operand and every nested `(…)`/`[…]`/`{…}`/
2074
+ /// prefix-op re-enters `parse_unary`, so bounding it here bounds all expression nesting. The
2075
+ /// `-= 1` runs on both the `Ok` and `Err` paths, keeping the counter balanced.
1800
2076
  fn parse_unary(&mut self) -> Result<Expr, String> {
2077
+ self.enter_depth()?;
2078
+ // Grow the stack on demand (each nesting level costs a whole precedence-chain of frames), so
2079
+ // we reach the MAX_PARSE_DEPTH bound and return a catchable error rather than overflowing.
2080
+ let r = stacker::maybe_grow(64 * 1024, 512 * 1024, || self.parse_unary_inner());
2081
+ self.depth -= 1;
2082
+ r
2083
+ }
2084
+
2085
+ fn parse_unary_inner(&mut self) -> Result<Expr, String> {
1801
2086
  // Handle prefix ++/-- (consolidated)
1802
2087
  if let Some(is_inc) = match self.peek_kind() {
1803
2088
  Some(TokenKind::PlusPlus) => Some(true),
@@ -1819,6 +2104,24 @@ impl<'a> Parser<'a> {
1819
2104
  Expr::PrefixDec { name, span }
1820
2105
  });
1821
2106
  }
2107
+ // #304: prefix `++obj.x` / `--arr[i]` on a member/index target. Desugars to a
2108
+ // read-modify-write, which yields the NEW value — exactly prefix semantics.
2109
+ if Self::is_desugarable_member_target(&operand) {
2110
+ let span = Span {
2111
+ start: span_start,
2112
+ end: operand.span().end,
2113
+ };
2114
+ let binop = if is_inc { BinOp::Add } else { BinOp::Sub };
2115
+ return Ok(Self::desugar_member_rmw(&operand, span, |read| Expr::Binary {
2116
+ left: Box::new(read),
2117
+ op: binop,
2118
+ right: Box::new(Expr::Literal {
2119
+ value: Literal::Number(1.0),
2120
+ span,
2121
+ }),
2122
+ span,
2123
+ }));
2124
+ }
1822
2125
  return Err(format!(
1823
2126
  "Prefix {} requires an identifier",
1824
2127
  if is_inc { "++" } else { "--" }
@@ -2049,13 +2352,13 @@ impl<'a> Parser<'a> {
2049
2352
  };
2050
2353
  }
2051
2354
  TokenKind::PlusPlus | TokenKind::MinusMinus => {
2355
+ let is_inc = kind == TokenKind::PlusPlus;
2052
2356
  if let Expr::Ident {
2053
2357
  name,
2054
2358
  span: ident_span,
2055
2359
  } = &expr
2056
2360
  {
2057
2361
  let name = Arc::clone(name);
2058
- let is_inc = kind == TokenKind::PlusPlus;
2059
2362
  let tok = self.advance().ok_or("Unexpected EOF")?;
2060
2363
  let span = Span {
2061
2364
  start: ident_span.start,
@@ -2066,6 +2369,26 @@ impl<'a> Parser<'a> {
2066
2369
  } else {
2067
2370
  Expr::PostfixDec { name, span }
2068
2371
  };
2372
+ } else if Self::is_desugarable_member_target(&expr) {
2373
+ // #304: postfix `obj.x++` / `arr[i]--` on a member/index target. Desugars to a
2374
+ // read-modify-write (statement-position semantics: yields the NEW value; the
2375
+ // old-value result in expression position is a documented follow-up).
2376
+ let start = expr.span().start;
2377
+ let tok = self.advance().ok_or("Unexpected EOF")?;
2378
+ let span = Span {
2379
+ start,
2380
+ end: tok.span.end,
2381
+ };
2382
+ let binop = if is_inc { BinOp::Add } else { BinOp::Sub };
2383
+ expr = Self::desugar_member_rmw(&expr, span, |read| Expr::Binary {
2384
+ left: Box::new(read),
2385
+ op: binop,
2386
+ right: Box::new(Expr::Literal {
2387
+ value: Literal::Number(1.0),
2388
+ span,
2389
+ }),
2390
+ span,
2391
+ });
2069
2392
  } else {
2070
2393
  break;
2071
2394
  }
@@ -2102,6 +2425,33 @@ impl<'a> Parser<'a> {
2102
2425
  span,
2103
2426
  })
2104
2427
  }
2428
+ TokenKind::Regex => {
2429
+ // #299: `/pattern/flags` desugars to `new RegExp("pattern", "flags")`. Every backend
2430
+ // already lowers `new RegExp(str, str)`, so no codegen change is needed. The lexer
2431
+ // encodes the literal as `pattern\0flags`; the pattern is kept verbatim (e.g. `\d+`).
2432
+ let lit = t.literal.clone().ok_or("Expected regex literal")?;
2433
+ let (pat, flags) = match lit.split_once('\u{0}') {
2434
+ Some((p, f)) => (Arc::from(p), Arc::from(f)),
2435
+ None => (Arc::clone(&lit), Arc::from("")),
2436
+ };
2437
+ Ok(Expr::New {
2438
+ callee: Box::new(Expr::Ident {
2439
+ name: Arc::from("RegExp"),
2440
+ span,
2441
+ }),
2442
+ args: vec![
2443
+ CallArg::Expr(Expr::Literal {
2444
+ value: Literal::String(pat),
2445
+ span,
2446
+ }),
2447
+ CallArg::Expr(Expr::Literal {
2448
+ value: Literal::String(flags),
2449
+ span,
2450
+ }),
2451
+ ],
2452
+ span,
2453
+ })
2454
+ }
2105
2455
  TokenKind::True => Ok(Expr::Literal {
2106
2456
  value: Literal::Bool(true),
2107
2457
  span,
@@ -2114,17 +2464,71 @@ impl<'a> Parser<'a> {
2114
2464
  value: Literal::Null,
2115
2465
  span,
2116
2466
  }),
2117
- // `fn` / `type` as a value reference or single-param arrow head (issue #55).
2118
- // `fn`/`type` function-expressions don't exist and statement-leading decls are
2119
- // 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.
2120
2514
  TokenKind::Ident | TokenKind::Fn | TokenKind::Type => {
2121
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
+ }
2122
2525
  // Check if this is a single-param arrow function: x => ...
2123
2526
  if matches!(self.peek_kind(), Some(TokenKind::Arrow)) {
2124
2527
  self.advance(); // consume =>
2125
2528
  let body = self.parse_arrow_body()?;
2126
2529
  let end = self.previous_span_end();
2127
2530
  return Ok(Expr::ArrowFunction {
2531
+ async_: false,
2128
2532
  params: vec![FunParam::Simple(TypedParam {
2129
2533
  name: name.clone(),
2130
2534
  name_span: span,
@@ -2142,7 +2546,7 @@ impl<'a> Parser<'a> {
2142
2546
  }
2143
2547
  TokenKind::LParen => {
2144
2548
  // Check if this is an arrow function: (params) => ...
2145
- if let Some(arrow_fn) = self.try_parse_arrow_function(&span)? {
2549
+ if let Some(arrow_fn) = self.try_parse_arrow_function(&span, false)? {
2146
2550
  return Ok(arrow_fn);
2147
2551
  }
2148
2552
  // Otherwise it's a grouping expression
@@ -2302,7 +2706,11 @@ impl<'a> Parser<'a> {
2302
2706
 
2303
2707
  /// Try to parse an arrow function starting with '(' already consumed.
2304
2708
  /// Returns Some(Expr) if successful, None if it's not an arrow function.
2305
- 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> {
2306
2714
  // Save position for backtracking
2307
2715
  let saved_pos = self.pos;
2308
2716
 
@@ -2356,6 +2764,7 @@ impl<'a> Parser<'a> {
2356
2764
  let end = self.previous_span_end();
2357
2765
 
2358
2766
  Ok(Some(Expr::ArrowFunction {
2767
+ async_,
2359
2768
  params,
2360
2769
  body,
2361
2770
  span: Span {
@@ -2365,6 +2774,48 @@ impl<'a> Parser<'a> {
2365
2774
  }))
2366
2775
  }
2367
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
+
2368
2819
  /// Parse the body of an arrow function (either expression or block)
2369
2820
  fn parse_arrow_body(&mut self) -> Result<ArrowBody, String> {
2370
2821
  if matches!(self.peek_kind(), Some(TokenKind::LBrace)) {