@tishlang/tish-format 2.16.13 → 2.35.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-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +77 -3
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +195 -16
- package/crates/tish_builtins/src/collections.rs +37 -2
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +168 -15
- package/crates/tish_bytecode/src/compiler.rs +56 -4
- package/crates/tish_bytecode/src/opcode.rs +4 -2
- package/crates/tish_compile/src/codegen.rs +513 -44
- package/crates/tish_compile/src/infer.rs +26 -11
- package/crates/tish_compile/src/resolve.rs +87 -10
- package/crates/tish_compile_js/src/codegen.rs +82 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
- package/crates/tish_core/src/json.rs +5 -1
- package/crates/tish_core/src/lib.rs +48 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +6 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +655 -27
- package/crates/tish_eval/src/natives.rs +264 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +23 -4
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/src/lib.rs +41 -0
- package/crates/tish_parser/src/parser.rs +180 -11
- package/crates/tish_resolve/src/lib.rs +66 -5
- package/crates/tish_runtime/Cargo.toml +5 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/lib.rs +128 -9
- package/crates/tish_ui/src/jsx.rs +18 -3
- package/crates/tish_vm/src/jit.rs +1 -1
- package/crates/tish_vm/src/vm.rs +270 -60
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
package/bin/tish-format
CHANGED
|
Binary file
|
|
@@ -176,7 +176,12 @@ pub fn convert_expr(expr: &OxcExpr<'_>, ctx: &Ctx<'_>) -> Result<Expr, ConvertEr
|
|
|
176
176
|
span: span_util::oxc_span_to_tish(ctx.1, &*arrow.body),
|
|
177
177
|
}))
|
|
178
178
|
};
|
|
179
|
-
Ok(Expr::ArrowFunction {
|
|
179
|
+
Ok(Expr::ArrowFunction {
|
|
180
|
+
async_: arrow.r#async,
|
|
181
|
+
params,
|
|
182
|
+
body,
|
|
183
|
+
span,
|
|
184
|
+
})
|
|
180
185
|
}
|
|
181
186
|
OxcExpr::AwaitExpression(a) => Ok(Expr::Await {
|
|
182
187
|
operand: Box::new(convert_expr(&a.argument, ctx)?),
|
|
@@ -229,7 +234,12 @@ pub fn convert_expr(expr: &OxcExpr<'_>, ctx: &Ctx<'_>) -> Result<Expr, ConvertEr
|
|
|
229
234
|
}))
|
|
230
235
|
}
|
|
231
236
|
};
|
|
232
|
-
Ok(Expr::ArrowFunction {
|
|
237
|
+
Ok(Expr::ArrowFunction {
|
|
238
|
+
async_: f.r#async,
|
|
239
|
+
params,
|
|
240
|
+
body,
|
|
241
|
+
span,
|
|
242
|
+
})
|
|
233
243
|
}
|
|
234
244
|
_ => Err(ConvertError::new(ConvertErrorKind::Unsupported {
|
|
235
245
|
what: format!("expression: {:?}", std::mem::discriminant(expr)),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Issue #122: Node callback form on the VM — `fs.writeFile(path, data, (err) => …)` and
|
|
2
|
+
// `fs.readFile(path, (err, data) => …)`, with (err, data) semantics and a non-null err on a
|
|
3
|
+
// missing file. Operates under $FS_TMP.
|
|
4
|
+
import { writeFile, readFile, stat, unlink } from "node:fs"
|
|
5
|
+
import { process } from "tish:process"
|
|
6
|
+
|
|
7
|
+
let d = process.env.FS_TMP
|
|
8
|
+
writeFile(d + "/cb.txt", "callback data", (err) => {
|
|
9
|
+
console.log("write-err-null=" + String(err === null))
|
|
10
|
+
readFile(d + "/cb.txt", (err2, data) => {
|
|
11
|
+
console.log("read=" + data)
|
|
12
|
+
stat(d + "/cb.txt", (err3, st) => {
|
|
13
|
+
console.log("size=" + String(st.size) + ",file=" + String(st.isFile()))
|
|
14
|
+
unlink(d + "/cb.txt", (err4) => {
|
|
15
|
+
console.log("unlink-err-null=" + String(err4 === null))
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
readFile(d + "/missing.txt", (err, data) => {
|
|
21
|
+
console.log("missing-err-set=" + String(!(err === null)) + ",data-null=" + String(data === null))
|
|
22
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Issue #122: the async `node:fs/promises` surface — await on the same methods, and `access`
|
|
2
|
+
// rejects on a missing path. Operates under $FS_TMP.
|
|
3
|
+
import { writeFile, readFile, stat, mkdir, readdir, rm, access } from "node:fs/promises"
|
|
4
|
+
import { process } from "tish:process"
|
|
5
|
+
|
|
6
|
+
async fn main() {
|
|
7
|
+
let d = process.env.FS_TMP
|
|
8
|
+
await writeFile(d + "/p.txt", "async hello")
|
|
9
|
+
console.log("read=" + (await readFile(d + "/p.txt")))
|
|
10
|
+
let st = await stat(d + "/p.txt")
|
|
11
|
+
console.log("size=" + String(st.size))
|
|
12
|
+
await mkdir(d + "/pd")
|
|
13
|
+
await writeFile(d + "/pd/x", "x")
|
|
14
|
+
console.log("dir=" + (await readdir(d + "/pd")).join(","))
|
|
15
|
+
let rejected = false
|
|
16
|
+
try { await access(d + "/missing") } catch (e) { rejected = true }
|
|
17
|
+
console.log("access-missing-rejected=" + String(rejected))
|
|
18
|
+
await rm(d + "/pd", { recursive: true })
|
|
19
|
+
console.log("done")
|
|
20
|
+
}
|
|
21
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Issue #122: Node-compatible fs sync surface via `node:fs`. Operates under $FS_TMP.
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, statSync, mkdirSync, readdirSync, renameSync, rmSync, appendFileSync, copyFileSync, constants } from "node:fs"
|
|
3
|
+
import { process } from "tish:process"
|
|
4
|
+
|
|
5
|
+
let d = process.env.FS_TMP
|
|
6
|
+
writeFileSync(d + "/a.txt", "hi")
|
|
7
|
+
appendFileSync(d + "/a.txt", "!")
|
|
8
|
+
console.log("read=" + readFileSync(d + "/a.txt"))
|
|
9
|
+
console.log("exists=" + String(existsSync(d + "/a.txt")) + "," + String(existsSync(d + "/no")))
|
|
10
|
+
let st = statSync(d + "/a.txt")
|
|
11
|
+
console.log("size=" + String(st.size) + ",file=" + String(st.isFile()) + ",dir=" + String(st.isDirectory()))
|
|
12
|
+
mkdirSync(d + "/sub")
|
|
13
|
+
copyFileSync(d + "/a.txt", d + "/sub/x.txt")
|
|
14
|
+
console.log("dir=" + readdirSync(d + "/sub").join(","))
|
|
15
|
+
renameSync(d + "/a.txt", d + "/b.txt")
|
|
16
|
+
console.log("renamed=" + String(existsSync(d + "/a.txt")) + "," + String(existsSync(d + "/b.txt")))
|
|
17
|
+
rmSync(d + "/sub", { recursive: true })
|
|
18
|
+
console.log("rm=" + String(existsSync(d + "/sub")))
|
|
19
|
+
console.log("const=" + String(constants.R_OK))
|
|
20
|
+
console.log("done")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//! Issue #122: Node `fs` parity on the bytecode VM (`tish run` default backend) — the sync
|
|
2
|
+
//! surface via `node:fs` and the async `node:fs/promises` surface (await + rejection).
|
|
3
|
+
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
use std::process::Command;
|
|
6
|
+
|
|
7
|
+
fn run_on(backend: &str, fixture: &str, tmp: &str) -> String {
|
|
8
|
+
let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
|
|
9
|
+
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
10
|
+
.join("tests")
|
|
11
|
+
.join("fixtures")
|
|
12
|
+
.join(fixture);
|
|
13
|
+
let out = Command::new(&tish)
|
|
14
|
+
.args([
|
|
15
|
+
"run",
|
|
16
|
+
"--backend",
|
|
17
|
+
backend,
|
|
18
|
+
"--feature",
|
|
19
|
+
"fs,process",
|
|
20
|
+
path.to_str().unwrap(),
|
|
21
|
+
])
|
|
22
|
+
.env("FS_TMP", tmp)
|
|
23
|
+
.output()
|
|
24
|
+
.expect("spawn tish run");
|
|
25
|
+
assert!(
|
|
26
|
+
out.status.success(),
|
|
27
|
+
"tish run --backend {backend} {fixture} failed: {}",
|
|
28
|
+
String::from_utf8_lossy(&out.stderr)
|
|
29
|
+
);
|
|
30
|
+
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn tmp_dir(tag: &str) -> String {
|
|
34
|
+
let nanos = std::time::SystemTime::now()
|
|
35
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
36
|
+
.unwrap()
|
|
37
|
+
.as_nanos();
|
|
38
|
+
let d = std::env::temp_dir().join(format!("tish_fs_parity_{tag}_{nanos:x}"));
|
|
39
|
+
std::fs::create_dir_all(&d).unwrap();
|
|
40
|
+
d.to_str().unwrap().to_string()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SYNC_EXPECTED: &str = "read=hi!\n\
|
|
44
|
+
exists=true,false\n\
|
|
45
|
+
size=3,file=true,dir=false\n\
|
|
46
|
+
dir=x.txt\n\
|
|
47
|
+
renamed=false,true\n\
|
|
48
|
+
rm=false\n\
|
|
49
|
+
const=4\n\
|
|
50
|
+
done\n";
|
|
51
|
+
|
|
52
|
+
const PROMISES_EXPECTED: &str = "read=async hello\n\
|
|
53
|
+
size=11\n\
|
|
54
|
+
dir=x\n\
|
|
55
|
+
access-missing-rejected=true\n\
|
|
56
|
+
done\n";
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn node_fs_sync_surface_on_every_backend() {
|
|
60
|
+
for backend in ["vm", "interp"] {
|
|
61
|
+
let tmp = tmp_dir(&format!("sync_{backend}"));
|
|
62
|
+
assert_eq!(
|
|
63
|
+
run_on(backend, "fs_parity_sync.tish", &tmp),
|
|
64
|
+
SYNC_EXPECTED,
|
|
65
|
+
"sync surface mismatch on {backend}"
|
|
66
|
+
);
|
|
67
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn node_fs_callback_form_on_every_backend() {
|
|
73
|
+
// Node callback form `readFile(path, (err, data) => …)` on every backend (#122): native's
|
|
74
|
+
// multi-arg-closure-to-native lowering was fixed upstream, and the interpreter runs the callback
|
|
75
|
+
// at the eval level (call_func's Native arm) instead of the self-less core bridge.
|
|
76
|
+
let expected = "write-err-null=true\n\
|
|
77
|
+
read=callback data\n\
|
|
78
|
+
size=13,file=true\n\
|
|
79
|
+
unlink-err-null=true\n\
|
|
80
|
+
missing-err-set=true,data-null=true\n";
|
|
81
|
+
for backend in ["vm", "interp", "native"] {
|
|
82
|
+
let tmp = tmp_dir(&format!("cb_{backend}"));
|
|
83
|
+
assert_eq!(
|
|
84
|
+
run_on(backend, "fs_parity_callbacks.tish", &tmp),
|
|
85
|
+
expected,
|
|
86
|
+
"callback form mismatch on {backend}"
|
|
87
|
+
);
|
|
88
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn node_fs_promises_surface_on_every_backend() {
|
|
94
|
+
for backend in ["vm", "interp"] {
|
|
95
|
+
let tmp = tmp_dir(&format!("prom_{backend}"));
|
|
96
|
+
assert_eq!(
|
|
97
|
+
run_on(backend, "fs_parity_promises.tish", &tmp),
|
|
98
|
+
PROMISES_EXPECTED,
|
|
99
|
+
"promises surface mismatch on {backend}"
|
|
100
|
+
);
|
|
101
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -9,7 +9,7 @@ use tishlang_vm;
|
|
|
9
9
|
|
|
10
10
|
#[test]
|
|
11
11
|
fn test_and_shortcircuit_emits_jump() {
|
|
12
|
-
let source = "let x = null; let y = x
|
|
12
|
+
let source = "let x = null; let y = x !== null && x.foo;";
|
|
13
13
|
let program = parse(source).expect("parse");
|
|
14
14
|
let chunk = compile_unoptimized(&program).expect("compile");
|
|
15
15
|
let code = &chunk.code;
|
|
@@ -22,7 +22,7 @@ fn test_and_shortcircuit_emits_jump() {
|
|
|
22
22
|
|
|
23
23
|
#[test]
|
|
24
24
|
fn test_and_shortcircuit_runs_unoptimized() {
|
|
25
|
-
let source = "let x = null; let y = x
|
|
25
|
+
let source = "let x = null; let y = x !== null && x.foo;";
|
|
26
26
|
let program = parse(source).expect("parse");
|
|
27
27
|
let chunk = compile_unoptimized(&program).expect("compile");
|
|
28
28
|
let result = tishlang_vm::run(&chunk);
|
|
@@ -35,7 +35,7 @@ fn test_and_shortcircuit_runs_unoptimized() {
|
|
|
35
35
|
|
|
36
36
|
#[test]
|
|
37
37
|
fn test_and_shortcircuit_runs_optimized() {
|
|
38
|
-
let source = "let x = null; let y = x
|
|
38
|
+
let source = "let x = null; let y = x !== null && x.foo;";
|
|
39
39
|
let program = parse(source).expect("parse");
|
|
40
40
|
let program = tishlang_opt::optimize(&program);
|
|
41
41
|
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
@@ -157,13 +157,15 @@ pub enum ExportDeclaration {
|
|
|
157
157
|
/// export default expr
|
|
158
158
|
Default(Expr),
|
|
159
159
|
/// Re-export from another module (#305):
|
|
160
|
-
/// `export { foo, bar as inc } from "./m"` -> specifiers, all=false
|
|
161
|
-
/// `export * from "./m"` -> specifiers=[], all=true
|
|
160
|
+
/// `export { foo, bar as inc } from "./m"` -> specifiers, all=false, from=Some
|
|
161
|
+
/// `export * from "./m"` -> specifiers=[], all=true, from=Some
|
|
162
|
+
/// `export { a, b as c }` -> specifiers, all=false, from=None (#415 —
|
|
163
|
+
/// a LOCAL named export of already-declared bindings)
|
|
162
164
|
/// Specifiers reuse `ImportSpecifier::Named` (`{ a as b }`).
|
|
163
165
|
ReExport {
|
|
164
166
|
specifiers: Vec<ImportSpecifier>,
|
|
165
167
|
all: bool,
|
|
166
|
-
from: Arc<str
|
|
168
|
+
from: Option<Arc<str>>,
|
|
167
169
|
span: Span,
|
|
168
170
|
},
|
|
169
171
|
}
|
|
@@ -173,6 +175,64 @@ pub struct Program {
|
|
|
173
175
|
pub statements: Vec<Statement>,
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
/// #469: adopt a Go/Rust/Swift-style `main` entry point.
|
|
179
|
+
///
|
|
180
|
+
/// tish runs top-level statements (JS module semantics), so `fn main() { … }` on its own defines a
|
|
181
|
+
/// function that is never called — a footgun for devs coming from compiled languages (the native
|
|
182
|
+
/// server case: `fn main() { serve(...) }` silently exits). This appends a synthetic `main()`
|
|
183
|
+
/// invocation to the ENTRY program's top-level statements so a defined `main` actually runs: top-level
|
|
184
|
+
/// statements still execute first (module init), then `main`.
|
|
185
|
+
///
|
|
186
|
+
/// Deliberately conservative to avoid a double-invocation: it fires ONLY when a top-level `main`
|
|
187
|
+
/// function is declared AND the program does not already invoke `main()` itself at top level (the
|
|
188
|
+
/// JS-habit `fn main(){…}; main()` shape). This must be applied to the entry module only (not imported
|
|
189
|
+
/// modules) and identically for every backend — call it from `merge_modules` so interp/vm/native/js
|
|
190
|
+
/// stay consistent by construction.
|
|
191
|
+
///
|
|
192
|
+
/// The synthetic call is a plain `main()` (never `await main()`), even for `async fn main`: a blocking
|
|
193
|
+
/// server main never returns anyway, and the interpreter cannot `await` a user async function's result
|
|
194
|
+
/// (a separate pre-existing gap), so an unwrapped call is the one form that behaves identically on
|
|
195
|
+
/// every backend.
|
|
196
|
+
///
|
|
197
|
+
/// Note: this is a tish-specific divergence from JS/node (node does not auto-call `main`); test it via
|
|
198
|
+
/// interp==vm==native cross-checks, and pair any `.tish` fixture with a `.js` that calls `main()`
|
|
199
|
+
/// explicitly so the node reference matches.
|
|
200
|
+
pub fn append_main_entry(statements: &mut Vec<Statement>) {
|
|
201
|
+
// A top-level `fn main` / `async fn main` declaration.
|
|
202
|
+
let has_main = statements.iter().any(|s| {
|
|
203
|
+
matches!(s, Statement::FunDecl { name, .. } if &**name == "main")
|
|
204
|
+
});
|
|
205
|
+
if !has_main {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Already invoked at top level? Then the user is driving it — don't double-call.
|
|
209
|
+
fn is_main_call(expr: &Expr) -> bool {
|
|
210
|
+
match expr {
|
|
211
|
+
Expr::Call { callee, .. } => {
|
|
212
|
+
matches!(callee.as_ref(), Expr::Ident { name, .. } if &**name == "main")
|
|
213
|
+
}
|
|
214
|
+
Expr::Await { operand, .. } => is_main_call(operand),
|
|
215
|
+
_ => false,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let already_called = statements
|
|
219
|
+
.iter()
|
|
220
|
+
.any(|s| matches!(s, Statement::ExprStmt { expr, .. } if is_main_call(expr)));
|
|
221
|
+
if already_called {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let span = Span::default();
|
|
225
|
+
let call = Expr::Call {
|
|
226
|
+
callee: Box::new(Expr::Ident {
|
|
227
|
+
name: Arc::from("main"),
|
|
228
|
+
span,
|
|
229
|
+
}),
|
|
230
|
+
args: Vec::new(),
|
|
231
|
+
span,
|
|
232
|
+
};
|
|
233
|
+
statements.push(Statement::ExprStmt { expr: call, span });
|
|
234
|
+
}
|
|
235
|
+
|
|
176
236
|
#[derive(Debug, Clone, PartialEq)]
|
|
177
237
|
pub enum Statement {
|
|
178
238
|
Block {
|
|
@@ -231,6 +291,16 @@ pub enum Statement {
|
|
|
231
291
|
body: Box<Statement>,
|
|
232
292
|
span: Span,
|
|
233
293
|
},
|
|
294
|
+
/// `for (let k in object)` — iterates the object's own enumerable keys (JS `for-in` semantics).
|
|
295
|
+
/// Distinct from `ForOf` (which iterates values). Kept as a first-class node so every backend
|
|
296
|
+
/// enumerates keys consistently and `tish fmt` round-trips `for-in`.
|
|
297
|
+
ForIn {
|
|
298
|
+
name: Arc<str>,
|
|
299
|
+
name_span: Span,
|
|
300
|
+
object: Expr,
|
|
301
|
+
body: Box<Statement>,
|
|
302
|
+
span: Span,
|
|
303
|
+
},
|
|
234
304
|
Return {
|
|
235
305
|
value: Option<Expr>,
|
|
236
306
|
span: Span,
|
|
@@ -431,6 +501,9 @@ pub enum Expr {
|
|
|
431
501
|
},
|
|
432
502
|
/// Arrow function: (params) => body
|
|
433
503
|
ArrowFunction {
|
|
504
|
+
/// `true` for `async () => …` (#428) — the body runs in async context (`await` allowed) and
|
|
505
|
+
/// the JS target emits an `async` prefix. Non-async arrows have `async_: false`.
|
|
506
|
+
async_: bool,
|
|
434
507
|
params: Vec<FunParam>,
|
|
435
508
|
body: ArrowBody,
|
|
436
509
|
span: Span,
|
|
@@ -653,6 +726,7 @@ impl Statement {
|
|
|
653
726
|
| Statement::While { span, .. }
|
|
654
727
|
| Statement::For { span, .. }
|
|
655
728
|
| Statement::ForOf { span, .. }
|
|
729
|
+
| Statement::ForIn { span, .. }
|
|
656
730
|
| Statement::Return { span, .. }
|
|
657
731
|
| Statement::Break { span, .. }
|
|
658
732
|
| Statement::Continue { span, .. }
|
|
@@ -149,11 +149,19 @@ pub fn unshift(arr: &Value, args: &[Value]) -> Value {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
pub fn index_of(arr: &Value, search: &Value) -> Value {
|
|
152
|
+
pub fn index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
|
|
153
153
|
let arr = as_boxed_array(arr); let arr = &*arr;
|
|
154
154
|
if let Value::Array(arr) = arr {
|
|
155
155
|
let arr_borrow = arr.borrow();
|
|
156
|
-
|
|
156
|
+
// `fromIndex`: clamp positive to [0, len]; negative counts from the end (`len + n`, floored
|
|
157
|
+
// at 0). Same handling as `includes`. Absent → 0.
|
|
158
|
+
let len = arr_borrow.len() as i64;
|
|
159
|
+
let start = match from {
|
|
160
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
|
|
161
|
+
Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
|
|
162
|
+
_ => 0,
|
|
163
|
+
};
|
|
164
|
+
for (i, v) in arr_borrow.iter().enumerate().skip(start) {
|
|
157
165
|
if v.strict_eq(search) {
|
|
158
166
|
return Value::Number(i as f64);
|
|
159
167
|
}
|
|
@@ -218,6 +226,57 @@ pub fn reverse(arr: &Value) -> Value {
|
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
/// `Array.prototype.toReversed()` — a reversed COPY (ES2023); the receiver is untouched.
|
|
230
|
+
pub fn to_reversed(arr: &Value) -> Value {
|
|
231
|
+
let mut v = snapshot_values(arr);
|
|
232
|
+
v.reverse();
|
|
233
|
+
Value::Array(VmRef::new(v))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// `Array.prototype.toSorted(cmp?)` — a sorted COPY (ES2023); the receiver is untouched. Sorting runs
|
|
237
|
+
/// on the copy via the same comparator/default machinery as `sort`.
|
|
238
|
+
pub fn to_sorted(arr: &Value, comparator: Option<&Value>) -> Value {
|
|
239
|
+
let copy = Value::Array(VmRef::new(snapshot_values(arr)));
|
|
240
|
+
match comparator {
|
|
241
|
+
Some(cmp @ Value::Function(_)) => sort_with_comparator(©, cmp),
|
|
242
|
+
_ => sort_default(©),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// `Array.prototype.with(index, value)` — a COPY with `index` replaced (ES2023). A negative index
|
|
247
|
+
/// counts from the end; an out-of-range index parks a catchable `RangeError` (matches node's message).
|
|
248
|
+
pub fn with(arr: &Value, index: &Value, value: &Value) -> Value {
|
|
249
|
+
let mut v = snapshot_values(arr);
|
|
250
|
+
let len = v.len() as i64;
|
|
251
|
+
let rel = match index {
|
|
252
|
+
Value::Number(n) if n.is_finite() => *n as i64,
|
|
253
|
+
_ => 0,
|
|
254
|
+
};
|
|
255
|
+
let actual = if rel >= 0 { rel } else { len + rel };
|
|
256
|
+
if actual < 0 || actual >= len {
|
|
257
|
+
tishlang_core::set_pending_throw(tishlang_core::range_error(format!(
|
|
258
|
+
"Invalid index : {}",
|
|
259
|
+
rel
|
|
260
|
+
)));
|
|
261
|
+
return Value::Null;
|
|
262
|
+
}
|
|
263
|
+
v[actual as usize] = value.clone();
|
|
264
|
+
Value::Array(VmRef::new(v))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// `Array.prototype.toSpliced(start, deleteCount?, ...items)` — a COPY with the splice applied (ES2023);
|
|
268
|
+
/// unlike `splice` (which returns the removed elements) this returns the resulting array.
|
|
269
|
+
pub fn to_spliced(
|
|
270
|
+
arr: &Value,
|
|
271
|
+
start: &Value,
|
|
272
|
+
delete_count: Option<&Value>,
|
|
273
|
+
items: &[Value],
|
|
274
|
+
) -> Value {
|
|
275
|
+
let copy = Value::Array(VmRef::new(snapshot_values(arr)));
|
|
276
|
+
splice(©, start, delete_count, items);
|
|
277
|
+
copy
|
|
278
|
+
}
|
|
279
|
+
|
|
221
280
|
/// Fisher-Yates shuffle. Returns a new shuffled array (does not mutate).
|
|
222
281
|
pub fn shuffle(arr: &Value) -> Value {
|
|
223
282
|
let arr = as_boxed_array(arr); let arr = &*arr;
|
|
@@ -273,6 +332,55 @@ pub fn fill(arr: &Value, value: &Value, start: &Value, end: &Value) -> Value {
|
|
|
273
332
|
}
|
|
274
333
|
}
|
|
275
334
|
|
|
335
|
+
pub fn last_index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
|
|
336
|
+
let arr = as_boxed_array(arr);
|
|
337
|
+
let arr = &*arr;
|
|
338
|
+
if let Value::Array(arr) = arr {
|
|
339
|
+
let arr_borrow = arr.borrow();
|
|
340
|
+
let len = arr_borrow.len() as i64;
|
|
341
|
+
// Default fromIndex is len-1; positive clamps to len-1; negative counts back from the end.
|
|
342
|
+
let start = match from {
|
|
343
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len - 1),
|
|
344
|
+
Some(Value::Number(n)) => len + *n as i64,
|
|
345
|
+
_ => len - 1,
|
|
346
|
+
};
|
|
347
|
+
let mut i = start;
|
|
348
|
+
while i >= 0 {
|
|
349
|
+
if let Some(v) = arr_borrow.get(i as usize) {
|
|
350
|
+
if v.strict_eq(search) {
|
|
351
|
+
return Value::Number(i as f64);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
i -= 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
Value::Number(-1.0)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// `copyWithin(target, start, end)` — copy the `[start, end)` block to `target`, in place; returns
|
|
361
|
+
/// the array. Indices are clamped, negatives count from the end.
|
|
362
|
+
pub fn copy_within(arr: &Value, target: &Value, start: &Value, end: &Value) -> Value {
|
|
363
|
+
let arr = as_boxed_array(arr);
|
|
364
|
+
let arr = &*arr;
|
|
365
|
+
if let Value::Array(a) = arr {
|
|
366
|
+
let mut v = a.borrow_mut();
|
|
367
|
+
let len = v.len();
|
|
368
|
+
let t = normalize_index(target, len as i64, 0);
|
|
369
|
+
let s = normalize_index(start, len as i64, 0);
|
|
370
|
+
let e = normalize_index(end, len as i64, len);
|
|
371
|
+
if s < e && t < len {
|
|
372
|
+
let count = (e - s).min(len - t);
|
|
373
|
+
let block: Vec<Value> = v[s..s + count].to_vec();
|
|
374
|
+
for (k, val) in block.into_iter().enumerate() {
|
|
375
|
+
v[t + k] = val;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
drop(v);
|
|
379
|
+
return Value::Array(a.clone());
|
|
380
|
+
}
|
|
381
|
+
Value::Null
|
|
382
|
+
}
|
|
383
|
+
|
|
276
384
|
pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
|
|
277
385
|
let arr = as_boxed_array(arr); let arr = &*arr;
|
|
278
386
|
if let Value::Array(arr) = arr {
|
|
@@ -417,16 +525,25 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
|
|
|
417
525
|
}
|
|
418
526
|
}
|
|
419
527
|
|
|
420
|
-
|
|
528
|
+
/// `initial` is `None` when no initial value was passed (`arr.reduce(fn)`) and `Some` for an explicit
|
|
529
|
+
/// one (`arr.reduce(fn, x)`, INCLUDING `arr.reduce(fn, null)`). This distinction matters: reducing an
|
|
530
|
+
/// empty array with NO initial value is a `TypeError` in JS, while an explicit initial (even `null`)
|
|
531
|
+
/// is returned as-is. A `Value::Null` sentinel can't tell the two apart — hence `Option`.
|
|
532
|
+
pub fn reduce(arr: &Value, callback: &Value, initial: Option<&Value>) -> Value {
|
|
421
533
|
// Packed fast path: fold the `Vec<f64>` snapshot directly. Same no-initial rule as the boxed
|
|
422
534
|
// path (absent init → first element as the seed, scan from index 1).
|
|
423
535
|
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
424
536
|
// `reduce` passes `(accumulator, element, index, array)` — the array is the 4th arg.
|
|
425
537
|
let arr_value = arr.clone();
|
|
426
|
-
let (start_idx, mut acc) =
|
|
427
|
-
(
|
|
428
|
-
|
|
429
|
-
|
|
538
|
+
let (start_idx, mut acc) = match initial {
|
|
539
|
+
Some(v) => (0usize, v.clone()),
|
|
540
|
+
None if !data.is_empty() => (1usize, Value::Number(data[0])),
|
|
541
|
+
None => {
|
|
542
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
543
|
+
"Reduce of empty array with no initial value",
|
|
544
|
+
));
|
|
545
|
+
return Value::Null;
|
|
546
|
+
}
|
|
430
547
|
};
|
|
431
548
|
// `skip(start_idx)` preserves the true element index for the callback's 3rd arg.
|
|
432
549
|
for (i, &x) in data.iter().enumerate().skip(start_idx) {
|
|
@@ -440,11 +557,15 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
|
|
|
440
557
|
let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
|
|
441
558
|
let arr_value = Value::Array(arr.clone()); // 4th callback arg (JS `array`)
|
|
442
559
|
let len = arr_borrow.len();
|
|
443
|
-
let (start_idx, mut acc) =
|
|
444
|
-
|
|
445
|
-
(1, arr_borrow[0].clone())
|
|
446
|
-
|
|
447
|
-
|
|
560
|
+
let (start_idx, mut acc) = match initial {
|
|
561
|
+
Some(v) => (0, v.clone()),
|
|
562
|
+
None if !arr_borrow.is_empty() => (1, arr_borrow[0].clone()),
|
|
563
|
+
None => {
|
|
564
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
565
|
+
"Reduce of empty array with no initial value",
|
|
566
|
+
));
|
|
567
|
+
return Value::Null;
|
|
568
|
+
}
|
|
448
569
|
};
|
|
449
570
|
for i in start_idx..len {
|
|
450
571
|
if tishlang_core::has_pending_throw() { return acc; } // #303
|
|
@@ -457,6 +578,64 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
|
|
|
457
578
|
}
|
|
458
579
|
}
|
|
459
580
|
|
|
581
|
+
/// `reduceRight(cb, initial?)` — like [`reduce`] but folds right-to-left. `initial` is `None` when
|
|
582
|
+
/// omitted (empty array + no initial → catchable TypeError).
|
|
583
|
+
pub fn reduce_right(arr: &Value, callback: &Value, initial: Option<&Value>) -> Value {
|
|
584
|
+
let data = snapshot_values(arr);
|
|
585
|
+
if let Value::Function(cb) = callback {
|
|
586
|
+
let arr_value = arr.clone();
|
|
587
|
+
let len = data.len();
|
|
588
|
+
let (start, mut acc) = match initial {
|
|
589
|
+
Some(v) => (len, v.clone()),
|
|
590
|
+
None if len > 0 => (len - 1, data[len - 1].clone()),
|
|
591
|
+
None => {
|
|
592
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
593
|
+
"Reduce of empty array with no initial value",
|
|
594
|
+
));
|
|
595
|
+
return Value::Null;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
let mut i = start;
|
|
599
|
+
while i > 0 {
|
|
600
|
+
i -= 1;
|
|
601
|
+
if tishlang_core::has_pending_throw() {
|
|
602
|
+
return acc;
|
|
603
|
+
}
|
|
604
|
+
acc = cb.call(&[
|
|
605
|
+
acc,
|
|
606
|
+
data[i].clone(),
|
|
607
|
+
Value::Number(i as f64),
|
|
608
|
+
arr_value.clone(),
|
|
609
|
+
]);
|
|
610
|
+
}
|
|
611
|
+
acc
|
|
612
|
+
} else {
|
|
613
|
+
Value::Null
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// `arr.keys()` — an iterator over the indices `0..len`.
|
|
618
|
+
pub fn keys(arr: &Value) -> Value {
|
|
619
|
+
let len = snapshot_values(arr).len();
|
|
620
|
+
crate::iterator::array_iterator((0..len).map(|i| Value::Number(i as f64)).collect())
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// `arr.values()` — an iterator over the elements.
|
|
624
|
+
pub fn values(arr: &Value) -> Value {
|
|
625
|
+
crate::iterator::array_iterator(snapshot_values(arr))
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/// `arr.entries()` — an iterator over `[index, element]` pairs.
|
|
629
|
+
pub fn entries(arr: &Value) -> Value {
|
|
630
|
+
crate::iterator::array_iterator(
|
|
631
|
+
snapshot_values(arr)
|
|
632
|
+
.into_iter()
|
|
633
|
+
.enumerate()
|
|
634
|
+
.map(|(i, v)| Value::Array(VmRef::new(vec![Value::Number(i as f64), v])))
|
|
635
|
+
.collect(),
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
460
639
|
pub fn for_each(arr: &Value, callback: &Value) -> Value {
|
|
461
640
|
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
462
641
|
let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
|
|
@@ -974,14 +1153,14 @@ mod packed_hof_tests {
|
|
|
974
1153
|
let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
|
|
975
1154
|
let add = cb_num(|acc, x| acc + x);
|
|
976
1155
|
// With init.
|
|
977
|
-
assert_eq!(reduce(&n, &add, &Value::Number(0.0)).as_number(), Some(14.0));
|
|
978
|
-
// No init → first element seeds, scan from index 1 (same total here).
|
|
979
|
-
assert_eq!(reduce(&n, &add,
|
|
1156
|
+
assert_eq!(reduce(&n, &add, Some(&Value::Number(0.0))).as_number(), Some(14.0));
|
|
1157
|
+
// No init (None) → first element seeds, scan from index 1 (same total here).
|
|
1158
|
+
assert_eq!(reduce(&n, &add, None).as_number(), Some(14.0));
|
|
980
1159
|
// Index arg: callback (acc, _elem, index) — sum the indices 0..5 = 10.
|
|
981
1160
|
let sum_idx = Value::native(|a: &[Value]| {
|
|
982
1161
|
Value::Number(a[0].as_number().unwrap() + a[2].as_number().unwrap())
|
|
983
1162
|
});
|
|
984
|
-
assert_eq!(reduce(&n, &sum_idx, &Value::Number(0.0)).as_number(), Some(10.0));
|
|
1163
|
+
assert_eq!(reduce(&n, &sum_idx, Some(&Value::Number(0.0))).as_number(), Some(10.0));
|
|
985
1164
|
}
|
|
986
1165
|
|
|
987
1166
|
#[test]
|