@tishlang/tish 2.2.7 → 2.8.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/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- package/crates/tish_ast/src/ast.rs +6 -2
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +1 -1
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +4 -4
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +17 -0
- package/crates/tish_compile_js/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +45 -1
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +6 -0
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +856 -140
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +15 -26
- package/crates/tish_resolve/src/lib.rs +188 -18
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -157,7 +157,7 @@ fn expr_is_param_only(e: &Expr, params: &HashSet<&str>) -> bool {
|
|
|
157
157
|
ArrayElement::Spread(_) => false,
|
|
158
158
|
}),
|
|
159
159
|
Expr::Object { props, .. } => props.iter().all(|p| match p {
|
|
160
|
-
ObjectProp::KeyValue(_, x) => expr_is_param_only(x, params),
|
|
160
|
+
ObjectProp::KeyValue(_, x, _) => expr_is_param_only(x, params),
|
|
161
161
|
ObjectProp::Spread(_) => false,
|
|
162
162
|
}),
|
|
163
163
|
Expr::TemplateLiteral { exprs, .. } => {
|
|
@@ -332,7 +332,7 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
|
332
332
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
|
|
333
333
|
}),
|
|
334
334
|
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
335
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_rebinds(e, name),
|
|
335
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_rebinds(e, name),
|
|
336
336
|
}),
|
|
337
337
|
Expr::MemberAssign { object, value, .. } => expr_rebinds(object, name) || expr_rebinds(value, name),
|
|
338
338
|
Expr::IndexAssign { object, index, value, .. } => {
|
|
@@ -452,7 +452,7 @@ impl SlotScan {
|
|
|
452
452
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
|
|
453
453
|
}),
|
|
454
454
|
Expr::Object { props, .. } => props.iter().all(|p| match p {
|
|
455
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => self.expr(e, in_closure),
|
|
455
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => self.expr(e, in_closure),
|
|
456
456
|
}),
|
|
457
457
|
Expr::Assign { name, value, .. }
|
|
458
458
|
| Expr::CompoundAssign { name, value, .. }
|
|
@@ -836,6 +836,53 @@ impl<'a> Compiler<'a> {
|
|
|
836
836
|
self.chunk.code[patch_pos + 1] = bytes[1];
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
+
/// Compile `cond` in CONDITION position (if / while / for / do-while). The operand stack is EMPTY
|
|
840
|
+
/// at every emitted `JumpIfFalse` — `&&` / `||` lower to pure control flow rather than the
|
|
841
|
+
/// value-producing `Dup` + `BinOp` form, so a hot numeric loop whose only disqualifier was the
|
|
842
|
+
/// condition shape stays eligible for the cranelift JIT (#167). Returns the `JumpIfFalse` patch
|
|
843
|
+
/// sites the caller must point at its "condition is false" target. The condition value itself is
|
|
844
|
+
/// never observed here, so truthiness-only lowering is exact; short-circuit order is preserved.
|
|
845
|
+
fn compile_condition_jump_if_false(
|
|
846
|
+
&mut self,
|
|
847
|
+
cond: &Expr,
|
|
848
|
+
) -> Result<Vec<usize>, CompileError> {
|
|
849
|
+
match cond {
|
|
850
|
+
// `a && b` is false if EITHER operand is false — test each in order; both exit to the
|
|
851
|
+
// same false-target, so concatenate their patch sites.
|
|
852
|
+
Expr::Binary {
|
|
853
|
+
left,
|
|
854
|
+
op: BinOp::And,
|
|
855
|
+
right,
|
|
856
|
+
..
|
|
857
|
+
} => {
|
|
858
|
+
let mut patches = self.compile_condition_jump_if_false(left)?;
|
|
859
|
+
patches.extend(self.compile_condition_jump_if_false(right)?);
|
|
860
|
+
Ok(patches)
|
|
861
|
+
}
|
|
862
|
+
// `a || b`: a truthy left makes the condition hold (skip the right); only the right's
|
|
863
|
+
// falsiness exits.
|
|
864
|
+
Expr::Binary {
|
|
865
|
+
left,
|
|
866
|
+
op: BinOp::Or,
|
|
867
|
+
right,
|
|
868
|
+
..
|
|
869
|
+
} => {
|
|
870
|
+
self.compile_expr(left)?;
|
|
871
|
+
let take_right = self.emit_jump(Opcode::JumpIfFalse); // left falsy → evaluate right
|
|
872
|
+
let done = self.emit_jump(Opcode::Jump); // left truthy → condition holds
|
|
873
|
+
self.patch_jump(take_right, self.chunk.code.len());
|
|
874
|
+
let patches = self.compile_condition_jump_if_false(right)?;
|
|
875
|
+
self.patch_jump(done, self.chunk.code.len());
|
|
876
|
+
Ok(patches)
|
|
877
|
+
}
|
|
878
|
+
// Any other expression: evaluate it; one JumpIfFalse consumes the value.
|
|
879
|
+
_ => {
|
|
880
|
+
self.compile_expr(cond)?;
|
|
881
|
+
Ok(vec![self.emit_jump(Opcode::JumpIfFalse)])
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
839
886
|
/// Patch a JumpBack operand: distance from the IP after this insn back to `target`.
|
|
840
887
|
/// `patch_pos` is the first byte of the u16 operand (same as [`Self::emit_jump_back`]'s return value).
|
|
841
888
|
fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
|
|
@@ -1194,11 +1241,13 @@ impl<'a> Compiler<'a> {
|
|
|
1194
1241
|
else_branch,
|
|
1195
1242
|
..
|
|
1196
1243
|
} => {
|
|
1197
|
-
self.
|
|
1198
|
-
let jump_else = self.emit_jump(Opcode::JumpIfFalse);
|
|
1244
|
+
let jump_else_sites = self.compile_condition_jump_if_false(cond)?;
|
|
1199
1245
|
self.compile_statement(then_branch)?;
|
|
1200
1246
|
let jump_end = self.emit_jump(Opcode::Jump);
|
|
1201
|
-
|
|
1247
|
+
let else_target = self.chunk.code.len();
|
|
1248
|
+
for site in &jump_else_sites {
|
|
1249
|
+
self.patch_jump(*site, else_target);
|
|
1250
|
+
}
|
|
1202
1251
|
if let Some(else_s) = else_branch {
|
|
1203
1252
|
self.compile_statement(else_s)?;
|
|
1204
1253
|
}
|
|
@@ -1222,14 +1271,16 @@ impl<'a> Compiler<'a> {
|
|
|
1222
1271
|
self.breakable_stack.push(Breakable::Loop {
|
|
1223
1272
|
unwind_depth: self.block_depth,
|
|
1224
1273
|
});
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1274
|
+
// Condition-position lowering: empty stack at each JumpIfFalse keeps the loop
|
|
1275
|
+
// JIT-eligible (#167). Each returned site exits to `end`.
|
|
1276
|
+
let jump_out_sites = self.compile_condition_jump_if_false(cond)?;
|
|
1228
1277
|
self.compile_statement(body)?;
|
|
1229
1278
|
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
|
|
1230
1279
|
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1231
1280
|
let end = self.chunk.code.len();
|
|
1232
|
-
|
|
1281
|
+
for site in &jump_out_sites {
|
|
1282
|
+
self.patch_jump(*site, end);
|
|
1283
|
+
}
|
|
1233
1284
|
let info = self.loop_stack.pop().unwrap();
|
|
1234
1285
|
self.breakable_stack.pop();
|
|
1235
1286
|
for p in info.continue_patches {
|
|
@@ -1266,14 +1317,16 @@ impl<'a> Compiler<'a> {
|
|
|
1266
1317
|
self.emit_u16(Opcode::LoopVarsBegin, idx);
|
|
1267
1318
|
}
|
|
1268
1319
|
let cond_start = self.chunk.code.len();
|
|
1269
|
-
|
|
1270
|
-
|
|
1320
|
+
// Condition-position lowering keeps the loop JIT-eligible (#167). The absent-condition
|
|
1321
|
+
// `for (;;)` keeps its `true` constant + single exit jump.
|
|
1322
|
+
let jump_out_sites = if let Some(c) = cond {
|
|
1323
|
+
self.compile_condition_jump_if_false(c)?
|
|
1271
1324
|
} else {
|
|
1272
1325
|
let idx = self.constant_idx(Constant::Bool(true));
|
|
1273
1326
|
self.emit(Opcode::LoadConst);
|
|
1274
1327
|
self.chunk.write_u16(idx);
|
|
1275
|
-
|
|
1276
|
-
|
|
1328
|
+
vec![self.emit_jump(Opcode::JumpIfFalse)]
|
|
1329
|
+
};
|
|
1277
1330
|
self.loop_stack.push(LoopInfo {
|
|
1278
1331
|
break_patches: Vec::new(),
|
|
1279
1332
|
continue_patches: Vec::new(),
|
|
@@ -1295,7 +1348,9 @@ impl<'a> Compiler<'a> {
|
|
|
1295
1348
|
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
|
|
1296
1349
|
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1297
1350
|
let end = self.chunk.code.len();
|
|
1298
|
-
|
|
1351
|
+
for site in &jump_out_sites {
|
|
1352
|
+
self.patch_jump(*site, end);
|
|
1353
|
+
}
|
|
1299
1354
|
for p in info.break_patches {
|
|
1300
1355
|
self.patch_jump(p, end);
|
|
1301
1356
|
}
|
|
@@ -1561,23 +1616,32 @@ impl<'a> Compiler<'a> {
|
|
|
1561
1616
|
self.loop_stack.push(LoopInfo {
|
|
1562
1617
|
break_patches: Vec::new(),
|
|
1563
1618
|
continue_patches: Vec::new(),
|
|
1564
|
-
|
|
1619
|
+
// `continue` jumps to the condition test, which is emitted AFTER the body — a
|
|
1620
|
+
// FORWARD jump. (A backward JumpBack would patch to dist = 0 via saturating_sub
|
|
1621
|
+
// of a forward target, becoming a no-op; execution then fell through and re-ran
|
|
1622
|
+
// the body's already-unwound ExitBlock on an empty block stack — the
|
|
1623
|
+
// "ExitBlock without matching EnterBlock" crash.)
|
|
1624
|
+
continue_is_forward_jump: true,
|
|
1565
1625
|
});
|
|
1566
1626
|
self.breakable_stack.push(Breakable::Loop {
|
|
1567
1627
|
unwind_depth: self.block_depth,
|
|
1568
1628
|
});
|
|
1569
1629
|
self.compile_statement(body)?;
|
|
1570
1630
|
let cond_start = self.chunk.code.len();
|
|
1571
|
-
|
|
1572
|
-
|
|
1631
|
+
// Condition-position lowering: a false condition exits to `end`, a true one falls
|
|
1632
|
+
// through to the JumpBack — JIT-eligible, no value left on the stack (#167).
|
|
1633
|
+
let exit_sites = self.compile_condition_jump_if_false(cond)?;
|
|
1573
1634
|
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
|
|
1574
1635
|
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1575
1636
|
let end = self.chunk.code.len();
|
|
1576
|
-
|
|
1637
|
+
for site in &exit_sites {
|
|
1638
|
+
self.patch_jump(*site, end);
|
|
1639
|
+
}
|
|
1577
1640
|
let info = self.loop_stack.pop().unwrap();
|
|
1578
1641
|
self.breakable_stack.pop();
|
|
1579
1642
|
for p in info.continue_patches {
|
|
1580
|
-
|
|
1643
|
+
// Forward jump to the condition (see continue_is_forward_jump above).
|
|
1644
|
+
self.patch_jump(p, cond_start);
|
|
1581
1645
|
}
|
|
1582
1646
|
for p in info.break_patches {
|
|
1583
1647
|
self.patch_jump(p, end);
|
|
@@ -1859,14 +1923,16 @@ impl<'a> Compiler<'a> {
|
|
|
1859
1923
|
} => {
|
|
1860
1924
|
match op {
|
|
1861
1925
|
BinOp::And => {
|
|
1862
|
-
// Short-circuit: a && b
|
|
1926
|
+
// Short-circuit + value-returning (JS): `a && b` is `a` when `a` is falsy,
|
|
1927
|
+
// else `b` — NOT a coerced boolean (#240). Mirror the `||` lowering below:
|
|
1928
|
+
// keep the left operand on a falsy short-circuit, discard it and evaluate the
|
|
1929
|
+
// right otherwise. (The old `BinOp(And)` here coerced a truthy-left result to
|
|
1930
|
+
// a Bool, so `five() && 7` yielded `true` instead of `7`.)
|
|
1863
1931
|
self.compile_expr(left)?;
|
|
1864
1932
|
self.emit(Opcode::Dup);
|
|
1865
|
-
let
|
|
1866
|
-
self.
|
|
1867
|
-
self.
|
|
1868
|
-
let jump_end = self.emit_jump(Opcode::Jump);
|
|
1869
|
-
self.patch_jump(jump_shortcut, self.chunk.code.len());
|
|
1933
|
+
let jump_end = self.emit_jump(Opcode::JumpIfFalse); // a falsy → keep a
|
|
1934
|
+
self.emit(Opcode::Pop); // a truthy → discard a …
|
|
1935
|
+
self.compile_expr(right)?; // … and yield b
|
|
1870
1936
|
self.patch_jump(jump_end, self.chunk.code.len());
|
|
1871
1937
|
}
|
|
1872
1938
|
BinOp::Or => {
|
|
@@ -2123,7 +2189,7 @@ impl<'a> Compiler<'a> {
|
|
|
2123
2189
|
self.emit_u16(Opcode::NewObject, 0); // start with {}
|
|
2124
2190
|
for prop in props {
|
|
2125
2191
|
match prop {
|
|
2126
|
-
ObjectProp::KeyValue(k, v) => {
|
|
2192
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
2127
2193
|
let idx = self.constant_idx(Constant::String(Arc::clone(k)));
|
|
2128
2194
|
self.emit(Opcode::LoadConst);
|
|
2129
2195
|
self.chunk.write_u16(idx);
|
|
@@ -2139,7 +2205,7 @@ impl<'a> Compiler<'a> {
|
|
|
2139
2205
|
}
|
|
2140
2206
|
} else {
|
|
2141
2207
|
for prop in props {
|
|
2142
|
-
if let ObjectProp::KeyValue(k, v) = prop {
|
|
2208
|
+
if let ObjectProp::KeyValue(k, v, _) = prop {
|
|
2143
2209
|
let idx = self.constant_idx(Constant::String(Arc::clone(k)));
|
|
2144
2210
|
self.emit(Opcode::LoadConst);
|
|
2145
2211
|
self.chunk.write_u16(idx);
|
|
@@ -42,3 +42,22 @@ fn mutation_vm_ast_opt_with_peephole() {
|
|
|
42
42
|
let chunk = compile(&prog).expect("compile");
|
|
43
43
|
run(&chunk).expect("VM");
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
// Regression: `continue` inside a `do { } while (cond)` targets the condition test, which is emitted
|
|
47
|
+
// AFTER the body — a FORWARD jump. It used to be lowered as a backward `JumpBack`, which `patch_jump_back`
|
|
48
|
+
// resolved to distance 0 (saturating_sub of a forward target) — a no-op. Execution then fell through into
|
|
49
|
+
// the body block's already-unwound `ExitBlock`, crashing the VM with "ExitBlock without matching
|
|
50
|
+
// EnterBlock". The `continue` here unwinds the body block AND the `if` then-block, so it exercises the
|
|
51
|
+
// multi-ExitBlock unwind path specifically.
|
|
52
|
+
#[test]
|
|
53
|
+
fn do_while_continue_does_not_crash_vm() {
|
|
54
|
+
let src = "let d = 0\n\
|
|
55
|
+
do {\n\
|
|
56
|
+
d = d + 1\n\
|
|
57
|
+
if (d === 2) { continue }\n\
|
|
58
|
+
} while (d < 5)\n";
|
|
59
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
60
|
+
// Both compile paths must run clean (the crash happened regardless of peephole optimization).
|
|
61
|
+
run(&compile_unoptimized(&prog).expect("compile (unopt)")).expect("VM run unoptimized");
|
|
62
|
+
run(&compile(&prog).expect("compile (opt)")).expect("VM run optimized");
|
|
63
|
+
}
|