@tishlang/tish 1.4.2 → 1.6.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 (39) 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/cargo_example_compile.rs +65 -0
  7. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  8. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  9. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  10. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  11. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  12. package/crates/tish/tests/integration_test.rs +48 -0
  13. package/crates/tish_build_utils/src/lib.rs +204 -1
  14. package/crates/tish_builtins/src/string.rs +248 -0
  15. package/crates/tish_bytecode/Cargo.toml +1 -0
  16. package/crates/tish_bytecode/src/compiler.rs +289 -66
  17. package/crates/tish_bytecode/src/opcode.rs +13 -3
  18. package/crates/tish_bytecode/src/peephole.rs +21 -16
  19. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  20. package/crates/tish_compile/Cargo.toml +1 -0
  21. package/crates/tish_compile/src/codegen.rs +277 -93
  22. package/crates/tish_compile/src/lib.rs +7 -4
  23. package/crates/tish_compile/src/resolve.rs +418 -40
  24. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  25. package/crates/tish_core/src/value.rs +1 -0
  26. package/crates/tish_eval/src/eval.rs +49 -1
  27. package/crates/tish_eval/src/lib.rs +1 -1
  28. package/crates/tish_native/src/build.rs +86 -17
  29. package/crates/tish_native/src/lib.rs +36 -16
  30. package/crates/tish_runtime/src/lib.rs +4 -0
  31. package/crates/tish_vm/src/lib.rs +1 -1
  32. package/crates/tish_vm/src/vm.rs +165 -19
  33. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  34. package/package.json +1 -1
  35. package/platform/darwin-arm64/tish +0 -0
  36. package/platform/darwin-x64/tish +0 -0
  37. package/platform/linux-arm64/tish +0 -0
  38. package/platform/linux-x64/tish +0 -0
  39. package/platform/win32-x64/tish.exe +0 -0
@@ -15,6 +15,123 @@ fn is_tish_workspace_root(root: &Path) -> bool {
15
15
  root.join("crates").join("tish_runtime").is_dir()
16
16
  }
17
17
 
18
+ /// True if `line` (trimmed) opens a Cargo.toml table whose body may contain path dependencies.
19
+ fn cargo_section_may_contain_path_deps(header: &str) -> bool {
20
+ let h = header.trim();
21
+ if h == "dependencies"
22
+ || h == "dev-dependencies"
23
+ || h == "build-dependencies"
24
+ || h == "workspace.dependencies"
25
+ {
26
+ return true;
27
+ }
28
+ h.starts_with("dependencies.")
29
+ || h.starts_with("dev-dependencies.")
30
+ || h.starts_with("build-dependencies.")
31
+ || h.starts_with("workspace.dependencies.")
32
+ || h.starts_with("patch.")
33
+ }
34
+
35
+ /// Collect `path = "..."` / `path = '...'` strings from lines in dependency-related sections.
36
+ fn path_values_from_cargo_toml(content: &str) -> Vec<String> {
37
+ let mut out = Vec::new();
38
+ let mut in_section = false;
39
+ for line in content.lines() {
40
+ let trimmed = line.trim();
41
+ if let Some(rest) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
42
+ in_section = cargo_section_may_contain_path_deps(rest);
43
+ continue;
44
+ }
45
+ if !in_section {
46
+ continue;
47
+ }
48
+ extract_path_assignments_from_line(trimmed, &mut out);
49
+ }
50
+ out
51
+ }
52
+
53
+ fn extract_path_assignments_from_line(line: &str, out: &mut Vec<String>) {
54
+ let mut rest = line;
55
+ while let Some(idx) = rest.find("path") {
56
+ let after = rest[idx + 4..].trim_start();
57
+ let after = match after.strip_prefix('=') {
58
+ Some(a) => a.trim_start(),
59
+ None => {
60
+ rest = &rest[idx + 4..];
61
+ continue;
62
+ }
63
+ };
64
+ let quote = match after.chars().next() {
65
+ Some('"') => '"',
66
+ Some('\'') => '\'',
67
+ _ => {
68
+ rest = &rest[idx + 4..];
69
+ continue;
70
+ }
71
+ };
72
+ let after = &after[quote.len_utf8()..];
73
+ let end = after.find(quote);
74
+ let Some(end) = end else {
75
+ rest = &rest[idx + 4..];
76
+ continue;
77
+ };
78
+ out.push(after[..end].to_string());
79
+ rest = &after[end + quote.len_utf8()..];
80
+ }
81
+ }
82
+
83
+ /// Starting from a filesystem path (crate dir or file), walk ancestors for `crates/tish_runtime`.
84
+ fn tish_root_from_path_hint(start: &Path) -> Option<PathBuf> {
85
+ let mut dir = if start.is_file() {
86
+ start.parent()?.to_path_buf()
87
+ } else {
88
+ start.to_path_buf()
89
+ };
90
+ dir = fs::canonicalize(&dir).unwrap_or(dir);
91
+ let mut cur = dir.as_path();
92
+ for _ in 0..32 {
93
+ if is_tish_workspace_root(cur) {
94
+ return Some(cur.to_path_buf());
95
+ }
96
+ cur = cur.parent()?;
97
+ }
98
+ None
99
+ }
100
+
101
+ /// Scan `dir/Cargo.toml` for path dependencies; if any resolves inside a Tish workspace, return that root.
102
+ fn tish_root_from_cargo_manifest_dir(dir: &Path) -> Option<PathBuf> {
103
+ let cargo_toml = dir.join("Cargo.toml");
104
+ if !cargo_toml.is_file() {
105
+ return None;
106
+ }
107
+ let content = fs::read_to_string(&cargo_toml).ok()?;
108
+ let base = dir;
109
+ for rel in path_values_from_cargo_toml(&content) {
110
+ let joined = base.join(&rel);
111
+ let resolved = match joined.canonicalize() {
112
+ Ok(p) => p,
113
+ Err(_) => continue,
114
+ };
115
+ if let Some(root) = tish_root_from_path_hint(&resolved) {
116
+ return Some(root);
117
+ }
118
+ }
119
+ None
120
+ }
121
+
122
+ /// Walk from `start` upward; at each directory try [`tish_root_from_cargo_manifest_dir`].
123
+ fn tish_root_from_project_cargo_files(mut start: PathBuf) -> Option<PathBuf> {
124
+ for _ in 0..32 {
125
+ if let Some(root) = tish_root_from_cargo_manifest_dir(&start) {
126
+ return Some(root);
127
+ }
128
+ if !start.pop() {
129
+ break;
130
+ }
131
+ }
132
+ None
133
+ }
134
+
18
135
  /// Find the Tish workspace root using multiple strategies.
19
136
  ///
20
137
  /// Returns the directory containing the workspace Cargo.toml (with [workspace]).
@@ -30,6 +147,10 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
30
147
  return Ok(root_buf);
31
148
  }
32
149
  }
150
+ // Consumer workspace: manifest is the app crate; path deps point at Tish checkout.
151
+ if let Some(root) = tish_root_from_project_cargo_files(path.clone()) {
152
+ return Ok(root);
153
+ }
33
154
  }
34
155
 
35
156
  // Strategy 2: Walk from current executable (e.g. target/debug/tish)
@@ -49,7 +170,14 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
49
170
  }
50
171
  }
51
172
 
52
- // Strategy 3: Walk from current working directory
173
+ // Strategy 3: Walk from current working directory (path deps on a consumer crate)
174
+ if let Ok(cwd) = std::env::current_dir() {
175
+ if let Some(root) = tish_root_from_project_cargo_files(cwd.clone()) {
176
+ return Ok(root);
177
+ }
178
+ }
179
+
180
+ // Strategy 4: Walk from current working directory
53
181
  if let Ok(mut current) = std::env::current_dir() {
54
182
  for _ in 0..15 {
55
183
  let cargo_toml = current.join("Cargo.toml");
@@ -74,6 +202,21 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
74
202
  Err("Cannot find Tish workspace root. Run from workspace root or use cargo run.".to_string())
75
203
  }
76
204
 
205
+ /// Path to `crates/tish_runtime` inside a locally installed `@tishlang/tish` npm package.
206
+ pub fn npm_package_runtime_path(project_root: &Path) -> Option<PathBuf> {
207
+ let p = project_root
208
+ .join("node_modules")
209
+ .join("@tishlang")
210
+ .join("tish")
211
+ .join("crates")
212
+ .join("tish_runtime");
213
+ if p.is_dir() {
214
+ Some(p)
215
+ } else {
216
+ None
217
+ }
218
+ }
219
+
77
220
  /// Find the path to the tishlang_runtime crate.
78
221
  ///
79
222
  /// Returns a canonical path string suitable for Cargo.toml path dependencies.
@@ -89,6 +232,24 @@ pub fn find_runtime_path() -> Result<String, String> {
89
232
  .map(|p| p.display().to_string().replace('\\', "/"))
90
233
  }
91
234
 
235
+ /// Resolve `tishlang_runtime` for a Cargo build, preferring the npm install under `project_root`.
236
+ ///
237
+ /// When a Tish app lives next to a checkout of the language repo (e.g. `…/tish/tish-cargo-example`),
238
+ /// [`find_workspace_root`] can return the checkout while `rustDependencies` point at
239
+ /// `node_modules/@tishlang/tish/crates/tish_core`. Using the npm tree for **both** runtime and shim
240
+ /// avoids Cargo lockfile "package collision" for the same crate name/version at two paths.
241
+ pub fn find_runtime_path_for_project(project_root: Option<&Path>) -> Result<String, String> {
242
+ if let Some(root) = project_root {
243
+ if let Some(rt) = npm_package_runtime_path(root) {
244
+ return rt
245
+ .canonicalize()
246
+ .map_err(|e| format!("Cannot canonicalize tishlang_runtime (npm): {}", e))
247
+ .map(|p| p.display().to_string().replace('\\', "/"));
248
+ }
249
+ }
250
+ find_runtime_path()
251
+ }
252
+
92
253
  /// Crate package name -> directory name (directories kept as tish_* for historical reasons).
93
254
  const CRATE_NAME_TO_DIR: &[(&str, &str)] = &[
94
255
  ("tishlang_runtime", "tish_runtime"),
@@ -193,3 +354,45 @@ pub fn copy_binary_to_output(binary: &Path, output_path: &Path) -> Result<(), St
193
354
  fs::copy(binary, output_path).map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
194
355
  Ok(())
195
356
  }
357
+
358
+ #[cfg(test)]
359
+ mod tests {
360
+ use super::*;
361
+
362
+ #[test]
363
+ fn path_values_dependencies_section_only() {
364
+ let toml = r#"
365
+ [package]
366
+ name = "app"
367
+ path = "ignored-outside-deps"
368
+
369
+ [dependencies]
370
+ tishlang_runtime = { path = "../tish/crates/tish_runtime" }
371
+
372
+ [metadata]
373
+ path = "also-ignored"
374
+ "#;
375
+ let paths = path_values_from_cargo_toml(toml);
376
+ assert_eq!(paths, vec!["../tish/crates/tish_runtime"]);
377
+ }
378
+
379
+ #[test]
380
+ fn path_values_workspace_dependencies() {
381
+ let toml = r#"
382
+ [workspace.dependencies]
383
+ tishlang_runtime = { path = "../../tish/tish/crates/tish_runtime" }
384
+ "#;
385
+ let paths = path_values_from_cargo_toml(toml);
386
+ assert_eq!(paths, vec!["../../tish/tish/crates/tish_runtime"]);
387
+ }
388
+
389
+ #[test]
390
+ fn path_values_patch_section() {
391
+ let toml = r#"
392
+ [patch.crates-io]
393
+ tishlang_runtime = { path = "../vendor/tish_runtime" }
394
+ "#;
395
+ let paths = path_values_from_cargo_toml(toml);
396
+ assert_eq!(paths, vec!["../vendor/tish_runtime"]);
397
+ }
398
+ }
@@ -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" }