@tishlang/tish 2.2.7 → 2.9.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 +2 -1
- package/crates/tish/src/cli_help.rs +15 -0
- package/crates/tish/src/main.rs +139 -12
- package/crates/tish/tests/integration_test.rs +65 -0
- 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 +3133 -582
- package/crates/tish_compile/src/infer.rs +1950 -56
- 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/Cargo.toml +3 -0
- package/crates/tish_compile_js/src/codegen.rs +365 -9
- package/crates/tish_compile_js/src/lib.rs +2 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +229 -1
- 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
|
@@ -215,15 +215,39 @@ pub fn substr(s: &Value, start: &Value, length: &Value) -> Value {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
pub fn split(s: &Value, sep: &Value) -> Value {
|
|
218
|
+
split_limit(s, sep, None)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// `String.prototype.split(sep, limit)` for a string separator. JS semantics: the string is split
|
|
222
|
+
/// completely on `sep`, then the result is truncated to `limit` elements (it does NOT keep the
|
|
223
|
+
/// unsplit remainder in the last slot, which is what Rust's `splitn` would do). `limit == 0` yields
|
|
224
|
+
/// an empty array. This is the single source of truth shared by the VM and rust/cranelift/wasi
|
|
225
|
+
/// backends; the interpreter mirrors it in `tish_eval::regex::string_split`.
|
|
226
|
+
pub fn split_limit(s: &Value, sep: &Value, limit: Option<usize>) -> Value {
|
|
218
227
|
if let Value::String(s) = s {
|
|
219
228
|
let separator = match sep {
|
|
220
229
|
Value::String(ss) => ss.as_str(),
|
|
221
230
|
_ => return Value::Array(VmRef::new(vec![Value::String(s.clone())])),
|
|
222
231
|
};
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
if limit == Some(0) {
|
|
233
|
+
return Value::Array(VmRef::new(Vec::new()));
|
|
234
|
+
}
|
|
235
|
+
// JS `split("")` is special: it yields the string's characters with NO surrounding empties
|
|
236
|
+
// (`"xyz".split("")` → `["x","y","z"]`, `"".split("")` → `[]`), unlike Rust's `str::split("")`
|
|
237
|
+
// which emits leading/trailing `""`. (tish splits on `char`s; lone-surrogate behavior for
|
|
238
|
+
// astral code points is out of scope.) #247
|
|
239
|
+
if separator.is_empty() {
|
|
240
|
+
let mut parts: Vec<Value> =
|
|
241
|
+
s.chars().map(|c| Value::String(c.to_string().into())).collect();
|
|
242
|
+
if let Some(max) = limit {
|
|
243
|
+
parts.truncate(max);
|
|
244
|
+
}
|
|
245
|
+
return Value::Array(VmRef::new(parts));
|
|
246
|
+
}
|
|
247
|
+
let mut parts: Vec<Value> = s.split(separator).map(|p| Value::String(p.into())).collect();
|
|
248
|
+
if let Some(max) = limit {
|
|
249
|
+
parts.truncate(max);
|
|
250
|
+
}
|
|
227
251
|
Value::Array(VmRef::new(parts))
|
|
228
252
|
} else {
|
|
229
253
|
Value::Null
|
|
@@ -364,6 +388,24 @@ pub fn char_at(s: &Value, idx: &Value) -> Value {
|
|
|
364
388
|
}
|
|
365
389
|
}
|
|
366
390
|
|
|
391
|
+
/// `String.prototype.at(index)` — like `charAt` but negative `index` counts from the end and an
|
|
392
|
+
/// out-of-range index yields `null` (JS `undefined`), not `""`. #247
|
|
393
|
+
pub fn at(s: &Value, index: &Value) -> Value {
|
|
394
|
+
if let Value::String(s) = s {
|
|
395
|
+
let i = match index {
|
|
396
|
+
Value::Number(n) => *n as i64,
|
|
397
|
+
_ => 0,
|
|
398
|
+
};
|
|
399
|
+
let chars: Vec<char> = s.chars().collect();
|
|
400
|
+
let len = chars.len() as i64;
|
|
401
|
+
let idx = if i < 0 { len + i } else { i };
|
|
402
|
+
if idx >= 0 && idx < len {
|
|
403
|
+
return Value::String(chars[idx as usize].to_string().into());
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
Value::Null
|
|
407
|
+
}
|
|
408
|
+
|
|
367
409
|
pub fn char_code_at(s: &Value, idx: &Value) -> Value {
|
|
368
410
|
if let Value::String(s) = s {
|
|
369
411
|
let idx = match idx {
|
|
@@ -541,6 +583,28 @@ mod tests {
|
|
|
541
583
|
assert_same!(trim(&n(1.0)), Value::Null);
|
|
542
584
|
}
|
|
543
585
|
|
|
586
|
+
#[test]
|
|
587
|
+
fn split_limit_js_semantics() {
|
|
588
|
+
let parts = |v: &Value| -> Vec<String> {
|
|
589
|
+
let Value::Array(a) = v else { panic!("not array") };
|
|
590
|
+
a.borrow()
|
|
591
|
+
.iter()
|
|
592
|
+
.map(|x| match x {
|
|
593
|
+
Value::String(s) => s.to_string(),
|
|
594
|
+
_ => panic!("not string"),
|
|
595
|
+
})
|
|
596
|
+
.collect()
|
|
597
|
+
};
|
|
598
|
+
// limit truncates to the first N pieces (does NOT keep the remainder, unlike `splitn`)
|
|
599
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), Some(2))), ["a", "b"]);
|
|
600
|
+
// limit 0 -> empty; limit beyond piece count -> full split; no limit -> full split
|
|
601
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), Some(0))).len(), 0);
|
|
602
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), Some(10))), ["a", "b", "c", "d"]);
|
|
603
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), None)), ["a", "b", "c", "d"]);
|
|
604
|
+
// split() delegates with no limit
|
|
605
|
+
assert_eq!(parts(&split(&s("one two"), &s(" "))), ["one", "two"]);
|
|
606
|
+
}
|
|
607
|
+
|
|
544
608
|
#[test]
|
|
545
609
|
fn case_and_prefix_suffix() {
|
|
546
610
|
assert_same!(to_upper_case(&s("aB")), s("AB"));
|
|
@@ -136,7 +136,14 @@ fn construct(kind: Kind, args: &[Value]) -> Value {
|
|
|
136
136
|
/// construction-only-coercion gap for this one view). Any op without a packed fast path materialises
|
|
137
137
|
/// it back to a boxed array, so every array method keeps working.
|
|
138
138
|
pub fn float64_array_packed(args: &[Value]) -> Value {
|
|
139
|
-
|
|
139
|
+
float64_array_with(Value::packed_arrays_enabled(), args)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// `float64_array_packed` with the packed/boxed choice made explicit, so tests exercise both paths
|
|
143
|
+
/// without toggling the now-cached process env flag (#166). `packed = false` is byte-identical to
|
|
144
|
+
/// the generic boxed `Value::Array` constructor.
|
|
145
|
+
fn float64_array_with(packed: bool, args: &[Value]) -> Value {
|
|
146
|
+
if !packed {
|
|
140
147
|
// Byte-identical fallback to the generic boxed `Value::Array` backing.
|
|
141
148
|
return construct(Kind::F64, args);
|
|
142
149
|
}
|
|
@@ -261,25 +268,26 @@ mod tests {
|
|
|
261
268
|
assert_eq!(nums(&v), vec![0.0, 1.0]);
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
271
|
+
// The packed/boxed selection is passed explicitly via `float64_array_with`, so this exercises
|
|
272
|
+
// both paths without touching the now-cached process env flag (#166) — no env mutation, no
|
|
273
|
+
// mid-test toggle race, parallel-safe.
|
|
267
274
|
#[test]
|
|
268
275
|
fn float64_packed_respects_flag() {
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
let boxed = float64_array_packed(&[Value::Number(3.0)]);
|
|
276
|
+
// Packed off (the default): byte-identical boxed `Value::Array` fallback.
|
|
277
|
+
let boxed = float64_array_with(false, &[Value::Number(3.0)]);
|
|
272
278
|
assert!(matches!(boxed, Value::Array(_)), "packed-off must return boxed Array");
|
|
273
279
|
assert_eq!(nums(&boxed), vec![0.0, 0.0, 0.0]);
|
|
274
280
|
|
|
275
|
-
//
|
|
281
|
+
// Packed on: `Value::NumberArray`. F64 needs no coercion (exact), non-numeric → NaN
|
|
276
282
|
// (matching the boxed `from_values(F64, …)`), and the length form zero-fills.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Value::
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
let packed = float64_array_with(
|
|
284
|
+
true,
|
|
285
|
+
&[Value::Array(VmRef::new(vec![
|
|
286
|
+
Value::Number(1.1),
|
|
287
|
+
Value::Number(2.2),
|
|
288
|
+
Value::Null,
|
|
289
|
+
]))],
|
|
290
|
+
);
|
|
283
291
|
match &packed {
|
|
284
292
|
Value::NumberArray(a) => {
|
|
285
293
|
let v = a.borrow();
|
|
@@ -290,9 +298,8 @@ mod tests {
|
|
|
290
298
|
_ => panic!("packed-on must return NumberArray"),
|
|
291
299
|
}
|
|
292
300
|
assert!(matches!(
|
|
293
|
-
|
|
301
|
+
float64_array_with(true, &[Value::Number(2.0)]),
|
|
294
302
|
Value::NumberArray(_)
|
|
295
303
|
));
|
|
296
|
-
std::env::remove_var("TISH_PACKED_ARRAYS");
|
|
297
304
|
}
|
|
298
305
|
}
|
|
@@ -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
|
+
}
|