@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.
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +2 -2
- package/crates/tish/src/cli_help.rs +504 -0
- package/crates/tish/src/main.rs +76 -90
- package/crates/tish/src/repl_completion.rs +1 -1
- package/crates/tish/tests/cargo_example_compile.rs +65 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/integration_test.rs +48 -0
- package/crates/tish_build_utils/src/lib.rs +204 -1
- package/crates/tish_builtins/src/string.rs +248 -0
- package/crates/tish_bytecode/Cargo.toml +1 -0
- package/crates/tish_bytecode/src/compiler.rs +289 -66
- package/crates/tish_bytecode/src/opcode.rs +13 -3
- package/crates/tish_bytecode/src/peephole.rs +21 -16
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +277 -93
- package/crates/tish_compile/src/lib.rs +7 -4
- package/crates/tish_compile/src/resolve.rs +418 -40
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/src/value.rs +1 -0
- package/crates/tish_eval/src/eval.rs +49 -1
- package/crates/tish_eval/src/lib.rs +1 -1
- package/crates/tish_native/src/build.rs +86 -17
- package/crates/tish_native/src/lib.rs +36 -16
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_vm/src/lib.rs +1 -1
- package/crates/tish_vm/src/vm.rs +165 -19
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- 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
|
@@ -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" }
|