@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
@@ -33,6 +33,33 @@ mod tests {
33
33
  use super::*;
34
34
  use tishlang_ast::{CallArg, Expr, ObjectProp, Statement};
35
35
 
36
+ /// #381: pathologically nested source must return a catchable `Err`, NOT overflow the native
37
+ /// stack and abort the process. Exercises expression nesting (`((((…`), array nesting (`[[[[…`),
38
+ /// and prefix-op nesting (`-!-!…`) — all funnel through the depth-guarded `parse_unary`.
39
+ #[test]
40
+ fn deeply_nested_source_errors_instead_of_aborting() {
41
+ let n = 50_000;
42
+ for (open, close) in [("(", ")"), ("[", "]")] {
43
+ let src = format!("let x = {}1{}", open.repeat(n), close.repeat(n));
44
+ let r = parse(&src);
45
+ assert!(r.is_err(), "{n}× nested {open} should be a catchable Err, not an abort");
46
+ assert!(
47
+ r.unwrap_err().contains("nesting too deep"),
48
+ "expected a depth-limit error"
49
+ );
50
+ }
51
+ let prefix = format!("let x = {}1", "-".repeat(n));
52
+ assert!(parse(&prefix).is_err(), "deep prefix-op chain should error, not abort");
53
+ }
54
+
55
+ /// The depth guard must not reject ordinary, modestly-nested code.
56
+ #[test]
57
+ fn normal_nesting_still_parses() {
58
+ assert!(parse("let x = ((((1 + 2))))").is_ok());
59
+ assert!(parse("let y = [[1, 2], [3, [4, [5]]]]").is_ok());
60
+ assert!(parse("fn f() { if (a) { if (b) { return 1 } } }").is_ok());
61
+ }
62
+
36
63
  #[test]
37
64
  fn test_async_fn_parse() {
38
65
  let program = parse("async fn foo() { }").expect("parse async fn");
@@ -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
 
@@ -1579,9 +1613,78 @@ impl<'a> Parser<'a> {
1579
1613
  })
1580
1614
  }
1581
1615
 
1616
+ /// #305 — consume a `from "..."` clause (`from` is a soft keyword: an `Ident` whose spelling is
1617
+ /// "from"), returning the module path string.
1618
+ fn parse_from_clause(&mut self) -> Result<Arc<str>, String> {
1619
+ let from_tok = self.expect(TokenKind::Ident)?;
1620
+ if from_tok.literal.as_deref() != Some("from") {
1621
+ return Err("Expected 'from' in re-export statement".to_string());
1622
+ }
1623
+ self.expect(TokenKind::String)?
1624
+ .literal
1625
+ .clone()
1626
+ .ok_or_else(|| "Expected string path in re-export".to_string())
1627
+ }
1628
+
1582
1629
  fn parse_export(&mut self) -> Result<Statement, String> {
1583
1630
  let span_start = self.expect(TokenKind::Export)?.span.start;
1584
- let declaration = if matches!(self.peek_kind(), Some(TokenKind::Default)) {
1631
+ let declaration = if matches!(self.peek_kind(), Some(TokenKind::LBrace)) {
1632
+ // #305: re-export named — `export { foo, bar as inc } from "./m"`
1633
+ self.advance(); // {
1634
+ let mut specifiers = Vec::new();
1635
+ while !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
1636
+ let name_tok = self.expect(TokenKind::Ident)?;
1637
+ let name_span = Span {
1638
+ start: name_tok.span.start,
1639
+ end: name_tok.span.end,
1640
+ };
1641
+ let name = name_tok
1642
+ .literal
1643
+ .clone()
1644
+ .ok_or("Expected identifier in export")?;
1645
+ let (alias, alias_span) = if matches!(self.peek_kind(), Some(TokenKind::As)) {
1646
+ self.advance(); // as
1647
+ let alias_tok = self.expect(TokenKind::Ident)?;
1648
+ let asp = Span {
1649
+ start: alias_tok.span.start,
1650
+ end: alias_tok.span.end,
1651
+ };
1652
+ (
1653
+ Some(alias_tok.literal.clone().ok_or("Expected alias after 'as'")?),
1654
+ Some(asp),
1655
+ )
1656
+ } else {
1657
+ (None, None)
1658
+ };
1659
+ specifiers.push(ImportSpecifier::Named {
1660
+ name,
1661
+ name_span,
1662
+ alias,
1663
+ alias_span,
1664
+ });
1665
+ if !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
1666
+ self.expect(TokenKind::Comma)?;
1667
+ }
1668
+ }
1669
+ self.expect(TokenKind::RBrace)?;
1670
+ let from = self.parse_from_clause()?;
1671
+ ExportDeclaration::ReExport {
1672
+ specifiers,
1673
+ all: false,
1674
+ from,
1675
+ span: self.span_end(span_start),
1676
+ }
1677
+ } else if matches!(self.peek_kind(), Some(TokenKind::Star)) {
1678
+ // #305: re-export all — `export * from "./m"`
1679
+ self.advance(); // *
1680
+ let from = self.parse_from_clause()?;
1681
+ ExportDeclaration::ReExport {
1682
+ specifiers: Vec::new(),
1683
+ all: true,
1684
+ from,
1685
+ span: self.span_end(span_start),
1686
+ }
1687
+ } else if matches!(self.peek_kind(), Some(TokenKind::Default)) {
1585
1688
  self.advance();
1586
1689
  let expr = self.parse_expr()?;
1587
1690
  ExportDeclaration::Default(expr)
@@ -1617,6 +1720,73 @@ impl<'a> Parser<'a> {
1617
1720
  self.parse_assign()
1618
1721
  }
1619
1722
 
1723
+ /// #304 — is `e` a side-effect-free l-value whose object/index sub-expressions can be safely
1724
+ /// DUPLICATED by a compound/inc-dec desugar (no Call/New/assignment/inc-dec inside)? The desugar
1725
+ /// reads and writes the same target, so duplicating a side-effecting sub-expression would run it
1726
+ /// twice; those targets keep the existing parse error rather than mis-evaluate.
1727
+ fn is_simple_lvalue(e: &Expr) -> bool {
1728
+ match e {
1729
+ Expr::Ident { .. } | Expr::Literal { .. } => true,
1730
+ Expr::Member { object, prop, .. } => {
1731
+ Self::is_simple_lvalue(object)
1732
+ && match prop {
1733
+ MemberProp::Name { .. } => true,
1734
+ MemberProp::Expr(pe) => Self::is_simple_lvalue(pe),
1735
+ }
1736
+ }
1737
+ Expr::Index { object, index, .. } => {
1738
+ Self::is_simple_lvalue(object) && Self::is_simple_lvalue(index)
1739
+ }
1740
+ _ => false,
1741
+ }
1742
+ }
1743
+
1744
+ /// #304 — does `e` name a member (`obj.x`) or index (`arr[i]`) target the read-modify-write
1745
+ /// desugar can handle (a simple l-value)?
1746
+ fn is_desugarable_member_target(e: &Expr) -> bool {
1747
+ Self::is_simple_lvalue(e)
1748
+ && matches!(
1749
+ e,
1750
+ Expr::Member { prop: MemberProp::Name { .. }, .. } | Expr::Index { .. }
1751
+ )
1752
+ }
1753
+
1754
+ fn compound_op_to_binop(op: CompoundOp) -> BinOp {
1755
+ match op {
1756
+ CompoundOp::Add => BinOp::Add,
1757
+ CompoundOp::Sub => BinOp::Sub,
1758
+ CompoundOp::Mul => BinOp::Mul,
1759
+ CompoundOp::Div => BinOp::Div,
1760
+ CompoundOp::Mod => BinOp::Mod,
1761
+ }
1762
+ }
1763
+
1764
+ /// #304 — desugar a read-modify-write on a member/index target into the existing
1765
+ /// `MemberAssign`/`IndexAssign` node (so every backend handles it for free). `make_value(read)`
1766
+ /// builds the stored value from a clone of the target read (e.g. `read + 1`, `read || rhs`).
1767
+ /// Caller must have checked [`Self::is_desugarable_member_target`].
1768
+ fn desugar_member_rmw(left: &Expr, span: Span, make_value: impl FnOnce(Expr) -> Expr) -> Expr {
1769
+ match left {
1770
+ Expr::Member {
1771
+ object,
1772
+ prop: MemberProp::Name { name, .. },
1773
+ ..
1774
+ } => Expr::MemberAssign {
1775
+ object: object.clone(),
1776
+ prop: Arc::clone(name),
1777
+ value: Box::new(make_value(left.clone())),
1778
+ span,
1779
+ },
1780
+ Expr::Index { object, index, .. } => Expr::IndexAssign {
1781
+ object: object.clone(),
1782
+ index: index.clone(),
1783
+ value: Box::new(make_value(left.clone())),
1784
+ span,
1785
+ },
1786
+ _ => unreachable!("desugar_member_rmw on non-member/index target"),
1787
+ }
1788
+ }
1789
+
1620
1790
  fn parse_assign(&mut self) -> Result<Expr, String> {
1621
1791
  let left = self.parse_conditional()?;
1622
1792
 
@@ -1686,18 +1856,29 @@ impl<'a> Parser<'a> {
1686
1856
  };
1687
1857
 
1688
1858
  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;
1859
+ let is_ident = matches!(&left, Expr::Ident { .. });
1860
+ if is_ident || Self::is_desugarable_member_target(&left) {
1861
+ let start = left.span().start;
1692
1862
  self.advance(); // consume the compound operator
1693
1863
  let value = self.parse_assign()?;
1694
1864
  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
- });
1865
+ let span = Span { start, end };
1866
+ if let Expr::Ident { name, .. } = &left {
1867
+ return Ok(Expr::CompoundAssign {
1868
+ name: Arc::clone(name),
1869
+ op,
1870
+ value: Box::new(value),
1871
+ span,
1872
+ });
1873
+ }
1874
+ // #304: `obj.x += e` / `arr[i] *= e` -> MemberAssign/IndexAssign(read <op> e).
1875
+ let binop = Self::compound_op_to_binop(op);
1876
+ return Ok(Self::desugar_member_rmw(&left, span, move |read| Expr::Binary {
1877
+ left: Box::new(read),
1878
+ op: binop,
1879
+ right: Box::new(value),
1880
+ span,
1881
+ }));
1701
1882
  }
1702
1883
  }
1703
1884
 
@@ -1710,18 +1891,42 @@ impl<'a> Parser<'a> {
1710
1891
  };
1711
1892
 
1712
1893
  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;
1894
+ let is_ident = matches!(&left, Expr::Ident { .. });
1895
+ if is_ident || Self::is_desugarable_member_target(&left) {
1896
+ let start = left.span().start;
1716
1897
  self.advance(); // consume the logical assign operator
1717
1898
  let value = self.parse_assign()?;
1718
1899
  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
- });
1900
+ let span = Span { start, end };
1901
+ if let Expr::Ident { name, .. } = &left {
1902
+ return Ok(Expr::LogicalAssign {
1903
+ name: Arc::clone(name),
1904
+ op,
1905
+ value: Box::new(value),
1906
+ span,
1907
+ });
1908
+ }
1909
+ // #304: `obj.x ||= e` -> MemberAssign/IndexAssign(read || e) (short-circuit preserved,
1910
+ // since `||`/`&&`/`??` evaluate the RHS lazily).
1911
+ return Ok(Self::desugar_member_rmw(&left, span, move |read| match op {
1912
+ LogicalAssignOp::OrOr => Expr::Binary {
1913
+ left: Box::new(read),
1914
+ op: BinOp::Or,
1915
+ right: Box::new(value),
1916
+ span,
1917
+ },
1918
+ LogicalAssignOp::AndAnd => Expr::Binary {
1919
+ left: Box::new(read),
1920
+ op: BinOp::And,
1921
+ right: Box::new(value),
1922
+ span,
1923
+ },
1924
+ LogicalAssignOp::Nullish => Expr::NullishCoalesce {
1925
+ left: Box::new(read),
1926
+ right: Box::new(value),
1927
+ span,
1928
+ },
1929
+ }));
1725
1930
  }
1726
1931
  }
1727
1932
 
@@ -1797,7 +2002,19 @@ impl<'a> Parser<'a> {
1797
2002
  Ok(left)
1798
2003
  }
1799
2004
 
2005
+ /// Depth-guarded wrapper (#381): every expression operand and every nested `(…)`/`[…]`/`{…}`/
2006
+ /// prefix-op re-enters `parse_unary`, so bounding it here bounds all expression nesting. The
2007
+ /// `-= 1` runs on both the `Ok` and `Err` paths, keeping the counter balanced.
1800
2008
  fn parse_unary(&mut self) -> Result<Expr, String> {
2009
+ self.enter_depth()?;
2010
+ // Grow the stack on demand (each nesting level costs a whole precedence-chain of frames), so
2011
+ // we reach the MAX_PARSE_DEPTH bound and return a catchable error rather than overflowing.
2012
+ let r = stacker::maybe_grow(64 * 1024, 512 * 1024, || self.parse_unary_inner());
2013
+ self.depth -= 1;
2014
+ r
2015
+ }
2016
+
2017
+ fn parse_unary_inner(&mut self) -> Result<Expr, String> {
1801
2018
  // Handle prefix ++/-- (consolidated)
1802
2019
  if let Some(is_inc) = match self.peek_kind() {
1803
2020
  Some(TokenKind::PlusPlus) => Some(true),
@@ -1819,6 +2036,24 @@ impl<'a> Parser<'a> {
1819
2036
  Expr::PrefixDec { name, span }
1820
2037
  });
1821
2038
  }
2039
+ // #304: prefix `++obj.x` / `--arr[i]` on a member/index target. Desugars to a
2040
+ // read-modify-write, which yields the NEW value — exactly prefix semantics.
2041
+ if Self::is_desugarable_member_target(&operand) {
2042
+ let span = Span {
2043
+ start: span_start,
2044
+ end: operand.span().end,
2045
+ };
2046
+ let binop = if is_inc { BinOp::Add } else { BinOp::Sub };
2047
+ return Ok(Self::desugar_member_rmw(&operand, span, |read| Expr::Binary {
2048
+ left: Box::new(read),
2049
+ op: binop,
2050
+ right: Box::new(Expr::Literal {
2051
+ value: Literal::Number(1.0),
2052
+ span,
2053
+ }),
2054
+ span,
2055
+ }));
2056
+ }
1822
2057
  return Err(format!(
1823
2058
  "Prefix {} requires an identifier",
1824
2059
  if is_inc { "++" } else { "--" }
@@ -2049,13 +2284,13 @@ impl<'a> Parser<'a> {
2049
2284
  };
2050
2285
  }
2051
2286
  TokenKind::PlusPlus | TokenKind::MinusMinus => {
2287
+ let is_inc = kind == TokenKind::PlusPlus;
2052
2288
  if let Expr::Ident {
2053
2289
  name,
2054
2290
  span: ident_span,
2055
2291
  } = &expr
2056
2292
  {
2057
2293
  let name = Arc::clone(name);
2058
- let is_inc = kind == TokenKind::PlusPlus;
2059
2294
  let tok = self.advance().ok_or("Unexpected EOF")?;
2060
2295
  let span = Span {
2061
2296
  start: ident_span.start,
@@ -2066,6 +2301,26 @@ impl<'a> Parser<'a> {
2066
2301
  } else {
2067
2302
  Expr::PostfixDec { name, span }
2068
2303
  };
2304
+ } else if Self::is_desugarable_member_target(&expr) {
2305
+ // #304: postfix `obj.x++` / `arr[i]--` on a member/index target. Desugars to a
2306
+ // read-modify-write (statement-position semantics: yields the NEW value; the
2307
+ // old-value result in expression position is a documented follow-up).
2308
+ let start = expr.span().start;
2309
+ let tok = self.advance().ok_or("Unexpected EOF")?;
2310
+ let span = Span {
2311
+ start,
2312
+ end: tok.span.end,
2313
+ };
2314
+ let binop = if is_inc { BinOp::Add } else { BinOp::Sub };
2315
+ expr = Self::desugar_member_rmw(&expr, span, |read| Expr::Binary {
2316
+ left: Box::new(read),
2317
+ op: binop,
2318
+ right: Box::new(Expr::Literal {
2319
+ value: Literal::Number(1.0),
2320
+ span,
2321
+ }),
2322
+ span,
2323
+ });
2069
2324
  } else {
2070
2325
  break;
2071
2326
  }
@@ -2102,6 +2357,33 @@ impl<'a> Parser<'a> {
2102
2357
  span,
2103
2358
  })
2104
2359
  }
2360
+ TokenKind::Regex => {
2361
+ // #299: `/pattern/flags` desugars to `new RegExp("pattern", "flags")`. Every backend
2362
+ // already lowers `new RegExp(str, str)`, so no codegen change is needed. The lexer
2363
+ // encodes the literal as `pattern\0flags`; the pattern is kept verbatim (e.g. `\d+`).
2364
+ let lit = t.literal.clone().ok_or("Expected regex literal")?;
2365
+ let (pat, flags) = match lit.split_once('\u{0}') {
2366
+ Some((p, f)) => (Arc::from(p), Arc::from(f)),
2367
+ None => (Arc::clone(&lit), Arc::from("")),
2368
+ };
2369
+ Ok(Expr::New {
2370
+ callee: Box::new(Expr::Ident {
2371
+ name: Arc::from("RegExp"),
2372
+ span,
2373
+ }),
2374
+ args: vec![
2375
+ CallArg::Expr(Expr::Literal {
2376
+ value: Literal::String(pat),
2377
+ span,
2378
+ }),
2379
+ CallArg::Expr(Expr::Literal {
2380
+ value: Literal::String(flags),
2381
+ span,
2382
+ }),
2383
+ ],
2384
+ span,
2385
+ })
2386
+ }
2105
2387
  TokenKind::True => Ok(Expr::Literal {
2106
2388
  value: Literal::Bool(true),
2107
2389
  span,
@@ -286,6 +286,7 @@ fn collect_stmt(
286
286
  collect_stmt(inner, source, lsp_line, lsp_char, best)
287
287
  }
288
288
  ExportDeclaration::Default(e) => collect_expr(e, source, lsp_line, lsp_char, best),
289
+ ExportDeclaration::ReExport { .. } => {}
289
290
  },
290
291
  Statement::TypeAlias {
291
292
  name, name_span, ..
@@ -1009,6 +1010,7 @@ fn member_chain_collect_stmt(
1009
1010
  ExportDeclaration::Default(e) => {
1010
1011
  member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
1011
1012
  }
1013
+ ExportDeclaration::ReExport { .. } => {}
1012
1014
  },
1013
1015
  Statement::Import { .. }
1014
1016
  | Statement::Break { .. }
@@ -1598,6 +1600,7 @@ fn walk_stmt_resolve(
1598
1600
  Statement::Export { declaration, .. } => match declaration.as_ref() {
1599
1601
  ExportDeclaration::Named(inner) => walk_stmt_resolve(inner, scopes, target),
1600
1602
  ExportDeclaration::Default(e) => walk_expr_resolve(e, scopes, target),
1603
+ ExportDeclaration::ReExport { .. } => None,
1601
1604
  },
1602
1605
  Statement::TypeAlias {
1603
1606
  name, name_span, ..
@@ -2156,6 +2159,7 @@ fn check_unresolved_stmt(
2156
2159
  Statement::Export { declaration, .. } => match declaration.as_ref() {
2157
2160
  ExportDeclaration::Named(inner) => check_unresolved_stmt(inner, scopes, out),
2158
2161
  ExportDeclaration::Default(e) => check_unresolved_expr(e, scopes, out),
2162
+ ExportDeclaration::ReExport { .. } => {}
2159
2163
  },
2160
2164
  Statement::TypeAlias {
2161
2165
  name, name_span, ..
@@ -2489,6 +2493,7 @@ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>)
2489
2493
  Statement::Export { declaration, .. } => match declaration.as_ref() {
2490
2494
  ExportDeclaration::Named(inner) => enumerate_stmt(inner, true, out),
2491
2495
  ExportDeclaration::Default(e) => enumerate_expr(e, exported, out),
2496
+ ExportDeclaration::ReExport { .. } => {}
2492
2497
  },
2493
2498
  Statement::VarDecl {
2494
2499
  name,
@@ -2788,6 +2793,7 @@ fn jsx_tags_stmt(s: &Statement, out: &mut std::collections::HashSet<Arc<str>>) {
2788
2793
  Statement::Export { declaration, .. } => match declaration.as_ref() {
2789
2794
  ExportDeclaration::Named(inner) => jsx_tags_stmt(inner, out),
2790
2795
  ExportDeclaration::Default(e) => jsx_tags_expr(e, out),
2796
+ ExportDeclaration::ReExport { .. } => {}
2791
2797
  },
2792
2798
  Statement::Import { .. }
2793
2799
  | Statement::Break { .. }
@@ -3532,6 +3538,7 @@ fn walk_stmt_completion(
3532
3538
  ExportDeclaration::Default(e) => {
3533
3539
  walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
3534
3540
  }
3541
+ ExportDeclaration::ReExport { .. } => {}
3535
3542
  },
3536
3543
  }
3537
3544
  }
@@ -3945,6 +3952,7 @@ fn tref_stmt(stmt: &Statement, out: &mut Vec<TypeOcc>) {
3945
3952
  Statement::Export { declaration, .. } => match declaration.as_ref() {
3946
3953
  ExportDeclaration::Named(inner) => tref_stmt(inner, out),
3947
3954
  ExportDeclaration::Default(e) => tref_expr(e, out),
3955
+ ExportDeclaration::ReExport { .. } => {}
3948
3956
  },
3949
3957
  _ => {}
3950
3958
  }
@@ -4132,6 +4140,7 @@ fn refs_stmt(
4132
4140
  refs_stmt(inner, program, source, name, def_span, out)
4133
4141
  }
4134
4142
  ExportDeclaration::Default(e) => refs_expr(e, program, source, name, def_span, out),
4143
+ ExportDeclaration::ReExport { .. } => {}
4135
4144
  },
4136
4145
  Statement::Import { .. }
4137
4146
  | Statement::Break { .. }
@@ -94,3 +94,8 @@ core_affinity = { version = "0.8", optional = true }
94
94
  tokio-tungstenite = { version = "0.29", optional = true }
95
95
  futures-util = { version = "0.3", optional = true }
96
96
  lazy_static = { version = "1", optional = true }
97
+
98
+ # #381 native-codegen recursion guard: stack-pressure probe for guarded self-recursive fns.
99
+ # Non-wasm only — wasm traps on overflow inside its own sandbox, so the guard is a no-op there.
100
+ [target.'cfg(not(target_family = "wasm"))'.dependencies]
101
+ stacker = "0.1"
@@ -1110,16 +1110,30 @@ where
1110
1110
  drop(tx);
1111
1111
 
1112
1112
  let mut count = 0usize;
1113
- while let Ok((req_prim, resp_tx)) = rx.recv() {
1114
- let req_value = req_prim.into_value();
1115
- let response_value = handler.call(&[req_value]);
1116
- let resp_prim = ResponsePrimitive::from_value(&response_value);
1117
- let _ = resp_tx.send(resp_prim);
1118
-
1119
- count += 1;
1120
- if max_requests.map(|m| count >= m).unwrap_or(false) {
1121
- stop.store(true, Ordering::Relaxed);
1122
- break;
1113
+ // #384: `recv_timeout` (not a blocking `recv`) so a handler's scheduled timers still fire when the
1114
+ // server is idle. Handler calls all run on this single VM thread, so the timer registry is here.
1115
+ loop {
1116
+ match rx.recv_timeout(Duration::from_millis(250)) {
1117
+ Ok((req_prim, resp_tx)) => {
1118
+ let req_value = req_prim.into_value();
1119
+ let response_value = handler.call(&[req_value]);
1120
+ crate::timers::drain_timers();
1121
+ let resp_prim = ResponsePrimitive::from_value(&response_value);
1122
+ let _ = resp_tx.send(resp_prim);
1123
+
1124
+ count += 1;
1125
+ if max_requests.map(|m| count >= m).unwrap_or(false) {
1126
+ stop.store(true, Ordering::Relaxed);
1127
+ break;
1128
+ }
1129
+ }
1130
+ Err(mpsc::RecvTimeoutError::Timeout) => {
1131
+ crate::timers::drain_timers();
1132
+ if stop.load(Ordering::Relaxed) {
1133
+ break;
1134
+ }
1135
+ }
1136
+ Err(mpsc::RecvTimeoutError::Disconnected) => break,
1123
1137
  }
1124
1138
  }
1125
1139
 
@@ -1181,6 +1195,10 @@ fn worker_loop_direct(
1181
1195
  Ok(None) => {} // timeout; re-check stop flag
1182
1196
  Err(_) => break,
1183
1197
  }
1198
+ // #384: fire any timers a handler scheduled. The handler runs on this accept thread, so its
1199
+ // `setTimeout`/`setInterval` callbacks live in this thread's registry; the ≤250ms `recv_timeout`
1200
+ // above means they fire within that window even when no new requests arrive.
1201
+ crate::timers::drain_timers();
1184
1202
  }
1185
1203
  }
1186
1204