@tishlang/tish 1.3.8 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/tish +0 -0
  2. package/crates/tish/Cargo.toml +2 -2
  3. package/crates/tish/src/cli_help.rs +504 -0
  4. package/crates/tish/src/main.rs +76 -90
  5. package/crates/tish/src/repl_completion.rs +1 -1
  6. package/crates/tish/tests/integration_test.rs +48 -0
  7. package/crates/tish_build_utils/src/lib.rs +171 -1
  8. package/crates/tish_builtins/src/string.rs +248 -0
  9. package/crates/tish_bytecode/Cargo.toml +1 -0
  10. package/crates/tish_bytecode/src/compiler.rs +289 -66
  11. package/crates/tish_bytecode/src/opcode.rs +13 -3
  12. package/crates/tish_bytecode/src/peephole.rs +21 -16
  13. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  14. package/crates/tish_compile/src/codegen.rs +214 -79
  15. package/crates/tish_compile/src/lib.rs +1 -1
  16. package/crates/tish_core/src/value.rs +1 -0
  17. package/crates/tish_eval/src/eval.rs +39 -1
  18. package/crates/tish_eval/src/lib.rs +1 -1
  19. package/crates/tish_lint/Cargo.toml +1 -0
  20. package/crates/tish_lint/src/bin/tish-lint.rs +141 -23
  21. package/crates/tish_lint/src/lib.rs +3 -1
  22. package/crates/tish_lsp/README.md +1 -1
  23. package/crates/tish_native/src/build.rs +48 -7
  24. package/crates/tish_native/src/lib.rs +8 -3
  25. package/crates/tish_runtime/src/lib.rs +4 -0
  26. package/crates/tish_vm/src/lib.rs +1 -1
  27. package/crates/tish_vm/src/vm.rs +155 -16
  28. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  29. package/package.json +1 -8
  30. package/platform/darwin-arm64/tish +0 -0
  31. package/platform/darwin-x64/tish +0 -0
  32. package/platform/linux-arm64/tish +0 -0
  33. package/platform/linux-x64/tish +0 -0
  34. package/platform/win32-x64/tish.exe +0 -0
@@ -37,6 +37,80 @@ pub fn len(s: &Value) -> Option<usize> {
37
37
  }
38
38
  }
39
39
 
40
+ /// JS `ToIntegerOrInfinity` then clamp for `lastIndexOf` `position` (character index).
41
+ fn last_index_of_position_to_start(position: &Value, len: usize) -> usize {
42
+ let pos = match position {
43
+ Value::Null => 0.0,
44
+ Value::Bool(false) => 0.0,
45
+ Value::Bool(true) => 1.0,
46
+ Value::Number(n) => {
47
+ if n.is_nan() || *n == 0.0 {
48
+ 0.0
49
+ } else if n.is_infinite() {
50
+ *n
51
+ } else {
52
+ n.trunc()
53
+ }
54
+ }
55
+ _ => 0.0,
56
+ };
57
+ if pos.is_infinite() {
58
+ if pos > 0.0 {
59
+ len
60
+ } else {
61
+ 0
62
+ }
63
+ } else if pos <= 0.0 {
64
+ 0
65
+ } else {
66
+ (pos as usize).min(len)
67
+ }
68
+ }
69
+
70
+ /// Character index of last occurrence of `needle` in `haystack`, or `-1`.
71
+ /// `position` is JS `lastIndexOf`'s second argument: use `Number(INFINITY)` when omitted;
72
+ /// `Null` is JS `null` → 0. Indices are Unicode scalar positions (same as `.length` / `indexOf`).
73
+ pub fn last_index_of_str(haystack: &str, needle: &str, position: &Value) -> Value {
74
+ let len = haystack.chars().count();
75
+ let start = last_index_of_position_to_start(position, len);
76
+ let hay: Vec<char> = haystack.chars().collect();
77
+ let needle_chars: Vec<char> = needle.chars().collect();
78
+ let search_len = needle_chars.len();
79
+ if search_len == 0 {
80
+ return Value::Number(start as f64);
81
+ }
82
+ if search_len > len {
83
+ return Value::Number(-1.0);
84
+ }
85
+ // Match must fit in the string and end at or before `start` (ECMA `lastIndexOf` position).
86
+ if start + 1 < search_len {
87
+ return Value::Number(-1.0);
88
+ }
89
+ let k_max_by_len = len - search_len;
90
+ let k_max_by_start = start + 1 - search_len;
91
+ let k_max = k_max_by_len.min(k_max_by_start);
92
+ let mut k = k_max;
93
+ loop {
94
+ if hay[k..k + search_len] == needle_chars[..] {
95
+ return Value::Number(k as f64);
96
+ }
97
+ if k == 0 {
98
+ break;
99
+ }
100
+ k -= 1;
101
+ }
102
+ Value::Number(-1.0)
103
+ }
104
+
105
+ /// Like [`last_index_of_str`] but takes string `Value`s; non-strings → `-1`.
106
+ pub fn last_index_of(s: &Value, search: &Value, position: &Value) -> Value {
107
+ if let (Value::String(h), Value::String(n)) = (s, search) {
108
+ last_index_of_str(h.as_ref(), n.as_ref(), position)
109
+ } else {
110
+ Value::Number(-1.0)
111
+ }
112
+ }
113
+
40
114
  /// Returns character index of first occurrence, or -1. Optional fromIndex (JS indexOf).
41
115
  pub fn index_of(s: &Value, search: &Value, from: Option<&Value>) -> Value {
42
116
  if let (Value::String(s), Value::String(search)) = (s, search) {
@@ -251,3 +325,177 @@ pub fn pad_start(s: &Value, target_len: &Value, pad: &Value) -> Value {
251
325
  pub fn pad_end(s: &Value, target_len: &Value, pad: &Value) -> Value {
252
326
  pad_impl(s, target_len, pad, false)
253
327
  }
328
+
329
+ #[cfg(test)]
330
+ mod tests {
331
+ use super::*;
332
+
333
+ fn s(x: &str) -> Value {
334
+ Value::String(x.into())
335
+ }
336
+
337
+ fn n(x: f64) -> Value {
338
+ Value::Number(x)
339
+ }
340
+
341
+ fn same(a: &Value, b: &Value) -> bool {
342
+ match (a, b) {
343
+ (Value::String(x), Value::String(y)) => x == y,
344
+ (Value::Number(x), Value::Number(y)) => {
345
+ if x.is_nan() && y.is_nan() {
346
+ true
347
+ } else {
348
+ x == y
349
+ }
350
+ }
351
+ (Value::Bool(x), Value::Bool(y)) => x == y,
352
+ (Value::Null, Value::Null) => true,
353
+ (Value::Array(ax), Value::Array(ay)) => {
354
+ let bx = ax.borrow();
355
+ let by = ay.borrow();
356
+ bx.len() == by.len() && bx.iter().zip(by.iter()).all(|(u, v)| same(u, v))
357
+ }
358
+ _ => false,
359
+ }
360
+ }
361
+
362
+ macro_rules! assert_same {
363
+ ($left:expr, $right:expr) => {
364
+ assert!(same(&$left, &$right), "left={:?} right={:?}", $left, $right);
365
+ };
366
+ }
367
+
368
+ #[test]
369
+ fn index_of_basic() {
370
+ assert_same!(index_of(&s("abc"), &s("b"), None), n(1.0));
371
+ assert_same!(index_of(&s("abc"), &s("x"), None), n(-1.0));
372
+ assert_same!(index_of(&s("abca"), &s("a"), Some(&n(1.0))), n(3.0));
373
+ }
374
+
375
+ #[test]
376
+ fn index_of_non_string() {
377
+ assert_same!(index_of(&n(1.0), &s("a"), None), n(-1.0));
378
+ assert_same!(index_of(&s("a"), &n(1.0), None), n(-1.0));
379
+ }
380
+
381
+ #[test]
382
+ fn includes_basic() {
383
+ assert_same!(includes(&s("hello"), &s("ll"), None), Value::Bool(true));
384
+ assert_same!(includes(&s("hello"), &s("x"), None), Value::Bool(false));
385
+ assert_same!(includes(&s("hello"), &s("l"), Some(&n(3.0))), Value::Bool(true));
386
+ assert_same!(includes(&s("hello"), &s("l"), Some(&n(4.0))), Value::Bool(false));
387
+ }
388
+
389
+ #[test]
390
+ fn includes_negative_from() {
391
+ assert_same!(includes(&s("hello"), &s("o"), Some(&n(-1.0))), Value::Bool(true));
392
+ assert_same!(includes(&s("hello"), &s("h"), Some(&n(-5.0))), Value::Bool(true));
393
+ // fromIndex -1 → start at len-1 = 1 ("i" only), "h" not found
394
+ assert_same!(includes(&s("hi"), &s("h"), Some(&n(-1.0))), Value::Bool(false));
395
+ }
396
+
397
+ #[test]
398
+ fn includes_non_string() {
399
+ assert_same!(includes(&n(1.0), &s("a"), None), Value::Bool(false));
400
+ }
401
+
402
+ #[test]
403
+ fn slice_substring() {
404
+ assert_same!(slice(&s("hello"), &n(1.0), &n(4.0)), s("ell"));
405
+ assert_same!(slice(&s("hello"), &n(-3.0), &Value::Null), s("llo"));
406
+ assert_same!(substring(&s("hello"), &n(4.0), &n(1.0)), s("ell"));
407
+ assert_same!(slice(&s("ab"), &n(1.0), &n(1.0)), s(""));
408
+ }
409
+
410
+ #[test]
411
+ fn slice_non_string() {
412
+ assert_same!(slice(&n(1.0), &n(0.0), &Value::Null), Value::Null);
413
+ }
414
+
415
+ #[test]
416
+ fn split_trim() {
417
+ let Value::Array(a) = split(&s("a,b"), &s(",")) else {
418
+ panic!();
419
+ };
420
+ assert_eq!(a.borrow().len(), 2);
421
+ assert_same!(
422
+ split(&s("x"), &n(1.0)),
423
+ Value::Array(Rc::new(RefCell::new(vec![s("x")])))
424
+ );
425
+ assert_same!(split(&n(1.0), &s(",")), Value::Null);
426
+ assert_same!(trim(&s(" x ")), s("x"));
427
+ assert_same!(trim(&n(1.0)), Value::Null);
428
+ }
429
+
430
+ #[test]
431
+ fn case_and_prefix_suffix() {
432
+ assert_same!(to_upper_case(&s("aB")), s("AB"));
433
+ assert_same!(to_lower_case(&s("aB")), s("ab"));
434
+ assert_same!(starts_with(&s("/api"), &s("/api")), Value::Bool(true));
435
+ assert_same!(ends_with(&s("x.js"), &s(".js")), Value::Bool(true));
436
+ assert_same!(starts_with(&n(1.0), &s("")), Value::Bool(false));
437
+ }
438
+
439
+ #[test]
440
+ fn replace_family() {
441
+ assert_same!(replace(&s("aa"), &s("a"), &s("b")), s("ba"));
442
+ assert_same!(replace_all(&s("aa"), &s("a"), &s("b")), s("bb"));
443
+ assert_same!(replace(&n(1.0), &s("a"), &s("b")), Value::Null);
444
+ }
445
+
446
+ #[test]
447
+ fn char_at_code() {
448
+ assert_same!(char_at(&s("ab"), &n(0.0)), s("a"));
449
+ assert_same!(char_at(&s("ab"), &n(99.0)), s(""));
450
+ if let Value::Number(x) = char_code_at(&s("A"), &n(0.0)) {
451
+ assert_eq!(x, 65.0);
452
+ } else {
453
+ panic!();
454
+ }
455
+ assert!(matches!(char_code_at(&s("x"), &n(9.0)), Value::Number(x) if x.is_nan()));
456
+ }
457
+
458
+ #[test]
459
+ fn repeat_pad() {
460
+ assert_same!(repeat(&s("ab"), &n(2.0)), s("abab"));
461
+ assert_same!(repeat(&s("x"), &n(0.0)), s(""));
462
+ assert_same!(pad_start(&s("5"), &n(3.0), &s("0")), s("005"));
463
+ assert_same!(pad_end(&s("hi"), &n(5.0), &s("!")), s("hi!!!"));
464
+ assert_same!(pad_start(&s("hello"), &n(3.0), &Value::Null), s("hello"));
465
+ }
466
+
467
+ #[test]
468
+ fn last_index_of_basic() {
469
+ assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)), n(3.0));
470
+ assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(2.0)), n(0.0));
471
+ assert_same!(last_index_of(&s("hello"), &s("l"), &n(3.0)), n(3.0));
472
+ assert_same!(last_index_of(&s("hello"), &s("l"), &n(1.0)), n(-1.0));
473
+ }
474
+
475
+ #[test]
476
+ fn last_index_of_omit_and_null() {
477
+ assert_same!(last_index_of(&s("aba"), &s("a"), &n(f64::INFINITY)), n(2.0));
478
+ assert_same!(last_index_of(&s("aba"), &s("a"), &Value::Null), n(0.0));
479
+ }
480
+
481
+ #[test]
482
+ fn last_index_of_empty_needle() {
483
+ assert_same!(last_index_of(&s("abc"), &s(""), &n(2.0)), n(2.0));
484
+ }
485
+
486
+ #[test]
487
+ fn last_index_of_nan_position() {
488
+ assert_same!(last_index_of(&s("aba"), &s("a"), &n(f64::NAN)), n(0.0));
489
+ }
490
+
491
+ #[test]
492
+ fn last_index_of_unicode() {
493
+ assert_same!(last_index_of(&s("😀a😀"), &s("a"), &n(f64::INFINITY)), n(1.0));
494
+ assert_same!(last_index_of(&s("😀a😀"), &s("😀"), &n(f64::INFINITY)), n(2.0));
495
+ }
496
+
497
+ #[test]
498
+ fn last_index_of_non_string() {
499
+ assert_same!(last_index_of(&n(1.0), &s("a"), &n(0.0)), n(-1.0));
500
+ }
501
+ }
@@ -14,3 +14,4 @@ tishlang_core = { path = "../tish_core", version = ">=0.1" }
14
14
  tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
15
15
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
16
16
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
17
+ tishlang_vm = { path = "../tish_vm", version = ">=0.1" }