@tishlang/tish-lint 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.
- package/Cargo.toml +3 -0
- package/bin/tish-lint +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.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::
|
|
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
|
-
|
|
1690
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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
|
-
// `
|
|
2118
|
-
// `
|
|
2119
|
-
//
|
|
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(
|
|
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)) {
|