@tishlang/tish-format 1.0.12 → 2.0.1
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/Cargo.toml +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -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/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- 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
|
@@ -0,0 +1,1447 @@
|
|
|
1
|
+
//! Minimal runtime for Tish compiled output.
|
|
2
|
+
//!
|
|
3
|
+
//! Re-exports core types from tishlang_core and provides console, Math,
|
|
4
|
+
//! and other builtin functions for compiled Tish programs.
|
|
5
|
+
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::sync::OnceLock;
|
|
8
|
+
use tishlang_builtins::helpers::extract_num;
|
|
9
|
+
#[cfg(feature = "fs")]
|
|
10
|
+
use tishlang_builtins::helpers::make_error_value;
|
|
11
|
+
|
|
12
|
+
pub use tishlang_builtins::symbol::symbol_object;
|
|
13
|
+
pub use tishlang_core::ObjectMap;
|
|
14
|
+
pub use tishlang_core::Value;
|
|
15
|
+
pub use tishlang_core::ArcStr;
|
|
16
|
+
/// Used by native codegen for `f()` / `obj()` dispatch (`Value::Function` or `__call` on objects).
|
|
17
|
+
pub use tishlang_core::value_call;
|
|
18
|
+
/// JS ToInt32/ToUint32 for the emitted bitwise/shift code (modulo 2³², NaN/±Infinity → 0).
|
|
19
|
+
pub use tishlang_core::{to_int32, to_uint32};
|
|
20
|
+
// Re-export the shared-mutable wrapper so the Rust code emitted by
|
|
21
|
+
// `tishlang_compile::codegen` can write `VmRef::new(...)` without needing
|
|
22
|
+
// a direct dependency on `tishlang_core` from the generated crate.
|
|
23
|
+
pub use tishlang_core::{VmReadGuard, VmRef, VmWriteGuard};
|
|
24
|
+
|
|
25
|
+
/// `for…of` iterable normalization for the native backend: a JS iterator object (one with a
|
|
26
|
+
/// callable `next()` returning `{ value, done }`, e.g. a `Map`/`Set` `.values()` result) is
|
|
27
|
+
/// drained into a `Value::Array`; arrays, strings, and everything else pass through unchanged.
|
|
28
|
+
pub fn normalize_for_of(v: Value) -> Value {
|
|
29
|
+
match tishlang_core::drain_iterator(&v) {
|
|
30
|
+
Some(items) => Value::Array(VmRef::new(items)),
|
|
31
|
+
None => v,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub use tishlang_builtins::construct::array_construct;
|
|
36
|
+
pub use tishlang_builtins::construct::error_constructor_value as tish_error_constructor;
|
|
37
|
+
pub use tishlang_builtins::construct::{
|
|
38
|
+
audio_context_constructor_value as tish_audio_context_constructor, construct as tish_construct,
|
|
39
|
+
};
|
|
40
|
+
pub use tishlang_builtins::typedarrays::{
|
|
41
|
+
float32_array_constructor_value as tish_float32_array_constructor,
|
|
42
|
+
float64_array_constructor_value as tish_float64_array_constructor,
|
|
43
|
+
float64_array_packed,
|
|
44
|
+
int16_array_constructor_value as tish_int16_array_constructor,
|
|
45
|
+
int32_array_constructor_value as tish_int32_array_constructor,
|
|
46
|
+
int8_array_constructor_value as tish_int8_array_constructor,
|
|
47
|
+
uint16_array_constructor_value as tish_uint16_array_constructor,
|
|
48
|
+
uint32_array_constructor_value as tish_uint32_array_constructor,
|
|
49
|
+
uint8_array_constructor_value as tish_uint8_array_constructor,
|
|
50
|
+
uint8_clamped_array_constructor_value as tish_uint8_clamped_array_constructor,
|
|
51
|
+
};
|
|
52
|
+
pub use tishlang_builtins::date::date_constructor_value as tish_date_constructor;
|
|
53
|
+
pub use tishlang_builtins::collections::{
|
|
54
|
+
collection_size, map_constructor_value as tish_map_constructor,
|
|
55
|
+
set_constructor_value as tish_set_constructor,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Re-export array methods from tishlang_builtins
|
|
59
|
+
pub use tishlang_builtins::array::{
|
|
60
|
+
concat as array_concat_impl, every as array_every, filter as array_filter, find as array_find,
|
|
61
|
+
find_index as array_find_index, flat as array_flat_impl, flat_map as array_flat_map,
|
|
62
|
+
for_each as array_for_each, includes as array_includes_impl, index_of as array_index_of_impl,
|
|
63
|
+
join as array_join_impl, map as array_map, pop as array_pop, push as array_push_impl,
|
|
64
|
+
fill as array_fill, reduce as array_reduce, reverse as array_reverse, shift as array_shift,
|
|
65
|
+
shuffle as array_shuffle, slice as array_slice_impl, some as array_some,
|
|
66
|
+
sort_default as array_sort_default, sort_numeric_asc as array_sort_numeric_asc,
|
|
67
|
+
sort_numeric_desc as array_sort_numeric_desc,
|
|
68
|
+
sort_with_comparator as array_sort_with_comparator, splice as array_splice_impl,
|
|
69
|
+
unshift as array_unshift_impl,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Re-export string methods from tishlang_builtins
|
|
73
|
+
pub use tishlang_builtins::string::{
|
|
74
|
+
char_at as string_char_at_impl, char_code_at as string_char_code_at_impl,
|
|
75
|
+
ends_with as string_ends_with_impl, escape_html as string_escape_html_impl,
|
|
76
|
+
includes as string_includes_impl, index_of as string_index_of_impl,
|
|
77
|
+
last_index_of as string_last_index_of_impl, pad_end as string_pad_end_impl,
|
|
78
|
+
pad_start as string_pad_start_impl, repeat as string_repeat_impl,
|
|
79
|
+
replace as string_replace_impl, replace_all as string_replace_all_impl,
|
|
80
|
+
slice as string_slice_impl, split as string_split_impl,
|
|
81
|
+
starts_with as string_starts_with_impl, substr as string_substr_impl,
|
|
82
|
+
substring as string_substring_impl,
|
|
83
|
+
to_lower_case as string_to_lower_case, to_upper_case as string_to_upper_case,
|
|
84
|
+
trim as string_trim,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Wrapper functions to maintain API compatibility
|
|
88
|
+
pub fn array_push(arr: &Value, args: &[Value]) -> Value {
|
|
89
|
+
array_push_impl(arr, args)
|
|
90
|
+
}
|
|
91
|
+
pub fn array_unshift(arr: &Value, args: &[Value]) -> Value {
|
|
92
|
+
array_unshift_impl(arr, args)
|
|
93
|
+
}
|
|
94
|
+
pub fn array_index_of(arr: &Value, search: &Value) -> Value {
|
|
95
|
+
array_index_of_impl(arr, search)
|
|
96
|
+
}
|
|
97
|
+
pub fn array_includes(arr: &Value, search: &Value, from: &Value) -> Value {
|
|
98
|
+
array_includes_impl(arr, search, Some(from))
|
|
99
|
+
}
|
|
100
|
+
pub fn array_join(arr: &Value, sep: &Value) -> Value {
|
|
101
|
+
array_join_impl(arr, sep)
|
|
102
|
+
}
|
|
103
|
+
pub fn array_splice(
|
|
104
|
+
arr: &Value,
|
|
105
|
+
start: &Value,
|
|
106
|
+
delete_count: Option<&Value>,
|
|
107
|
+
items: &[Value],
|
|
108
|
+
) -> Value {
|
|
109
|
+
array_splice_impl(arr, start, delete_count, items)
|
|
110
|
+
}
|
|
111
|
+
pub fn array_slice(arr: &Value, start: &Value, end: &Value) -> Value {
|
|
112
|
+
array_slice_impl(arr, start, end)
|
|
113
|
+
}
|
|
114
|
+
pub fn array_concat(arr: &Value, args: &[Value]) -> Value {
|
|
115
|
+
array_concat_impl(arr, args)
|
|
116
|
+
}
|
|
117
|
+
pub fn array_flat(arr: &Value, depth: &Value) -> Value {
|
|
118
|
+
array_flat_impl(arr, depth)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn array_sort(arr: &Value, comparator: Option<&Value>) -> Value {
|
|
122
|
+
match comparator {
|
|
123
|
+
Some(cmp) => array_sort_with_comparator(arr, cmp),
|
|
124
|
+
None => array_sort_default(arr),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pub fn string_index_of(s: &Value, search: &Value, from: &Value) -> Value {
|
|
129
|
+
string_index_of_impl(s, search, Some(from))
|
|
130
|
+
}
|
|
131
|
+
pub fn string_includes(s: &Value, search: &Value, from: &Value) -> Value {
|
|
132
|
+
string_includes_impl(s, search, Some(from))
|
|
133
|
+
}
|
|
134
|
+
pub fn string_slice(s: &Value, start: &Value, end: &Value) -> Value {
|
|
135
|
+
string_slice_impl(s, start, end)
|
|
136
|
+
}
|
|
137
|
+
pub fn string_substring(s: &Value, start: &Value, end: &Value) -> Value {
|
|
138
|
+
string_substring_impl(s, start, end)
|
|
139
|
+
}
|
|
140
|
+
pub fn string_substr(s: &Value, start: &Value, length: &Value) -> Value {
|
|
141
|
+
string_substr_impl(s, start, length)
|
|
142
|
+
}
|
|
143
|
+
pub fn string_split(s: &Value, sep: &Value) -> Value {
|
|
144
|
+
// A RegExp separator routes to the regex splitter (matches string_replace's regex handling
|
|
145
|
+
// and the interpreter/VM), so `"a1b2c".split(RegExp("\\d",""))` works on the rust backend.
|
|
146
|
+
#[cfg(feature = "regex")]
|
|
147
|
+
if matches!(sep, Value::RegExp(_)) {
|
|
148
|
+
return string_split_regex(s, sep, None);
|
|
149
|
+
}
|
|
150
|
+
string_split_impl(s, sep)
|
|
151
|
+
}
|
|
152
|
+
pub fn string_starts_with(s: &Value, search: &Value) -> Value {
|
|
153
|
+
string_starts_with_impl(s, search)
|
|
154
|
+
}
|
|
155
|
+
pub fn string_ends_with(s: &Value, search: &Value) -> Value {
|
|
156
|
+
string_ends_with_impl(s, search)
|
|
157
|
+
}
|
|
158
|
+
pub fn string_replace(s: &Value, search: &Value, replacement: &Value) -> Value {
|
|
159
|
+
#[cfg(feature = "regex")]
|
|
160
|
+
if matches!(search, Value::RegExp(_)) {
|
|
161
|
+
return string_replace_regex_or_callback(s, search, replacement);
|
|
162
|
+
}
|
|
163
|
+
string_replace_impl(s, search, replacement)
|
|
164
|
+
}
|
|
165
|
+
pub fn string_replace_all(s: &Value, search: &Value, replacement: &Value) -> Value {
|
|
166
|
+
string_replace_all_impl(s, search, replacement)
|
|
167
|
+
}
|
|
168
|
+
pub fn string_char_at(s: &Value, idx: &Value) -> Value {
|
|
169
|
+
string_char_at_impl(s, idx)
|
|
170
|
+
}
|
|
171
|
+
pub fn string_char_code_at(s: &Value, idx: &Value) -> Value {
|
|
172
|
+
string_char_code_at_impl(s, idx)
|
|
173
|
+
}
|
|
174
|
+
pub fn string_repeat(s: &Value, count: &Value) -> Value {
|
|
175
|
+
string_repeat_impl(s, count)
|
|
176
|
+
}
|
|
177
|
+
pub fn string_pad_start(s: &Value, target_len: &Value, pad: &Value) -> Value {
|
|
178
|
+
string_pad_start_impl(s, target_len, pad)
|
|
179
|
+
}
|
|
180
|
+
pub fn string_pad_end(s: &Value, target_len: &Value, pad: &Value) -> Value {
|
|
181
|
+
string_pad_end_impl(s, target_len, pad)
|
|
182
|
+
}
|
|
183
|
+
pub fn string_last_index_of(s: &Value, search: &Value, position: &Value) -> Value {
|
|
184
|
+
string_last_index_of_impl(s, search, position)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Number.prototype.toFixed(digits) - format number with fixed decimal places (0-20)
|
|
188
|
+
///
|
|
189
|
+
/// Delegates to the single source of truth in `tishlang_builtins::number` so the rust
|
|
190
|
+
/// backend, the bytecode VM, and the interpreter stay byte-identical. See
|
|
191
|
+
/// `tish/docs/full-backend-parity-plan.md` (Workstream A).
|
|
192
|
+
pub fn number_to_fixed(n: &Value, digits: &Value) -> Value {
|
|
193
|
+
tishlang_builtins::number::to_fixed(n, digits)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// `.toString([radix])` for the compiled backend (issue #59). A number receiver uses the
|
|
197
|
+
/// shared radix formatter so it stays byte-identical with the VM / interpreter; any other
|
|
198
|
+
/// receiver falls back to its normal JS string, so `[1,2].toString()` / `obj.toString()`
|
|
199
|
+
/// keep working.
|
|
200
|
+
pub fn number_to_string(n: &Value, radix: &Value) -> Value {
|
|
201
|
+
match n {
|
|
202
|
+
Value::Number(_) => tishlang_builtins::number::to_string(n, radix),
|
|
203
|
+
other => Value::String(other.to_js_string().into()),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Operators module for compound assignment operations
|
|
208
|
+
pub mod ops {
|
|
209
|
+
use tishlang_core::Value;
|
|
210
|
+
|
|
211
|
+
#[inline]
|
|
212
|
+
pub fn add(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
|
|
213
|
+
match (left, right) {
|
|
214
|
+
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
|
|
215
|
+
(Value::String(a), Value::String(b)) => {
|
|
216
|
+
let mut s = String::with_capacity(a.len() + b.len());
|
|
217
|
+
s.push_str(a);
|
|
218
|
+
s.push_str(b);
|
|
219
|
+
Ok(Value::String(s.into()))
|
|
220
|
+
}
|
|
221
|
+
(Value::String(a), b) => {
|
|
222
|
+
let b_str = b.to_js_string();
|
|
223
|
+
let mut s = String::with_capacity(a.len() + b_str.len());
|
|
224
|
+
s.push_str(a);
|
|
225
|
+
s.push_str(&b_str);
|
|
226
|
+
Ok(Value::String(s.into()))
|
|
227
|
+
}
|
|
228
|
+
(a, Value::String(b)) => {
|
|
229
|
+
let a_str = a.to_js_string();
|
|
230
|
+
let mut s = String::with_capacity(a_str.len() + b.len());
|
|
231
|
+
s.push_str(&a_str);
|
|
232
|
+
s.push_str(b);
|
|
233
|
+
Ok(Value::String(s.into()))
|
|
234
|
+
}
|
|
235
|
+
// Neither operand is a string here ⇒ numeric coercion, matching the VM's `eval_binop`
|
|
236
|
+
// (`as_number().unwrap_or(NaN)`): a null/bool/object operand (e.g. an out-of-bounds array
|
|
237
|
+
// read) coerces to NaN, so `number + null` is NaN — NOT an error that the codegen's
|
|
238
|
+
// `.unwrap_or(Value::Null)` would silently turn into `null` (the old rust-AOT divergence).
|
|
239
|
+
(a, b) => Ok(Value::Number(
|
|
240
|
+
a.as_number().unwrap_or(f64::NAN) + b.as_number().unwrap_or(f64::NAN),
|
|
241
|
+
)),
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#[inline]
|
|
246
|
+
pub fn sub(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
|
|
247
|
+
match (left, right) {
|
|
248
|
+
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)),
|
|
249
|
+
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
250
|
+
(a, b) => Ok(Value::Number(
|
|
251
|
+
a.as_number().unwrap_or(f64::NAN) - b.as_number().unwrap_or(f64::NAN),
|
|
252
|
+
)),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[inline]
|
|
257
|
+
pub fn mul(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
|
|
258
|
+
match (left, right) {
|
|
259
|
+
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)),
|
|
260
|
+
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
261
|
+
(a, b) => Ok(Value::Number(
|
|
262
|
+
a.as_number().unwrap_or(f64::NAN) * b.as_number().unwrap_or(f64::NAN),
|
|
263
|
+
)),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[inline]
|
|
268
|
+
pub fn div(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
|
|
269
|
+
match (left, right) {
|
|
270
|
+
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a / b)),
|
|
271
|
+
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
272
|
+
(a, b) => Ok(Value::Number(
|
|
273
|
+
a.as_number().unwrap_or(f64::NAN) / b.as_number().unwrap_or(f64::NAN),
|
|
274
|
+
)),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Compare two values for <. Supports number vs number and string vs string.
|
|
279
|
+
#[inline]
|
|
280
|
+
pub fn lt(left: &Value, right: &Value) -> Value {
|
|
281
|
+
let b = match (left, right) {
|
|
282
|
+
(Value::Number(a), Value::Number(b)) => a < b,
|
|
283
|
+
(Value::String(a), Value::String(b)) => a.as_str() < b.as_str(),
|
|
284
|
+
_ => false,
|
|
285
|
+
};
|
|
286
|
+
Value::Bool(b)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[inline]
|
|
290
|
+
pub fn le(left: &Value, right: &Value) -> Value {
|
|
291
|
+
let b = match (left, right) {
|
|
292
|
+
(Value::Number(a), Value::Number(b)) => a <= b,
|
|
293
|
+
(Value::String(a), Value::String(b)) => a.as_str() <= b.as_str(),
|
|
294
|
+
_ => false,
|
|
295
|
+
};
|
|
296
|
+
Value::Bool(b)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[inline]
|
|
300
|
+
pub fn gt(left: &Value, right: &Value) -> Value {
|
|
301
|
+
let b = match (left, right) {
|
|
302
|
+
(Value::Number(a), Value::Number(b)) => a > b,
|
|
303
|
+
(Value::String(a), Value::String(b)) => a.as_str() > b.as_str(),
|
|
304
|
+
_ => false,
|
|
305
|
+
};
|
|
306
|
+
Value::Bool(b)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[inline]
|
|
310
|
+
pub fn ge(left: &Value, right: &Value) -> Value {
|
|
311
|
+
let b = match (left, right) {
|
|
312
|
+
(Value::Number(a), Value::Number(b)) => a >= b,
|
|
313
|
+
(Value::String(a), Value::String(b)) => a.as_str() >= b.as_str(),
|
|
314
|
+
_ => false,
|
|
315
|
+
};
|
|
316
|
+
Value::Bool(b)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[inline]
|
|
320
|
+
pub fn modulo(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
|
|
321
|
+
match (left, right) {
|
|
322
|
+
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)),
|
|
323
|
+
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
324
|
+
(a, b) => Ok(Value::Number(
|
|
325
|
+
a.as_number().unwrap_or(f64::NAN) % b.as_number().unwrap_or(f64::NAN),
|
|
326
|
+
)),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
use tishlang_builtins::globals::{
|
|
332
|
+
array_is_array as builtins_array_is_array, boolean as builtins_boolean,
|
|
333
|
+
decode_uri as builtins_decode_uri, encode_uri as builtins_encode_uri,
|
|
334
|
+
is_finite as builtins_is_finite, is_nan as builtins_is_nan,
|
|
335
|
+
object_assign as builtins_object_assign, object_entries as builtins_object_entries,
|
|
336
|
+
object_from_entries as builtins_object_from_entries, object_keys as builtins_object_keys,
|
|
337
|
+
object_values as builtins_object_values,
|
|
338
|
+
number_convert as builtins_number_convert,
|
|
339
|
+
string_convert as builtins_string_convert,
|
|
340
|
+
string_from_char_code as builtins_string_from_char_code,
|
|
341
|
+
};
|
|
342
|
+
use tishlang_core::{json_parse as core_json_parse, json_stringify as core_json_stringify};
|
|
343
|
+
|
|
344
|
+
/// Public JSON helpers used by codegen-emitted code (specifically the
|
|
345
|
+
/// `_tish_write_json` impls on user-declared `type` aliases). Re-exporting
|
|
346
|
+
/// from the runtime keeps the generated source decoupled from
|
|
347
|
+
/// `tishlang_core` — generated code only ever names `tishlang_runtime`.
|
|
348
|
+
pub mod json {
|
|
349
|
+
pub use tishlang_core::json_stringify_into as stringify_into;
|
|
350
|
+
/// Append the JSON-escaped contents of `s` (without surrounding
|
|
351
|
+
/// quotes) to `buf`. Used by typed-struct serialisers for `String`
|
|
352
|
+
/// fields. Falls through to `tishlang_core::json_stringify_into`'s
|
|
353
|
+
/// internal helper via a `Value::String` round-trip when the inner
|
|
354
|
+
/// helper isn't directly exposed.
|
|
355
|
+
pub fn escape_into(buf: &mut String, s: &str) {
|
|
356
|
+
// Inline the same escape rules as tishlang_core::json::
|
|
357
|
+
// `escape_json_string_into`. Kept locally so we don't widen
|
|
358
|
+
// tishlang_core's public surface unnecessarily.
|
|
359
|
+
let bytes = s.as_bytes();
|
|
360
|
+
let mut start = 0usize;
|
|
361
|
+
for (i, &b) in bytes.iter().enumerate() {
|
|
362
|
+
if b < 0x20 || b == b'"' || b == b'\\' {
|
|
363
|
+
if start < i {
|
|
364
|
+
buf.push_str(&s[start..i]);
|
|
365
|
+
}
|
|
366
|
+
match b {
|
|
367
|
+
b'"' => buf.push_str("\\\""),
|
|
368
|
+
b'\\' => buf.push_str("\\\\"),
|
|
369
|
+
b'\n' => buf.push_str("\\n"),
|
|
370
|
+
b'\r' => buf.push_str("\\r"),
|
|
371
|
+
b'\t' => buf.push_str("\\t"),
|
|
372
|
+
b'\x08' => buf.push_str("\\b"),
|
|
373
|
+
b'\x0c' => buf.push_str("\\f"),
|
|
374
|
+
_ => {
|
|
375
|
+
use std::fmt::Write;
|
|
376
|
+
let _ = write!(buf, "\\u{:04x}", b as u32);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
start = i + 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if start < bytes.len() {
|
|
383
|
+
buf.push_str(&s[start..]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Error type for Tish throw/catch + non-local control flow (used to model `return`/`throw`
|
|
389
|
+
/// escaping `try`/`finally` in the Rust backend, which has no native exceptions).
|
|
390
|
+
#[derive(Debug, Clone)]
|
|
391
|
+
pub enum TishError {
|
|
392
|
+
/// A JS `throw` — catchable by `catch`.
|
|
393
|
+
Throw(Value),
|
|
394
|
+
/// A JS `return value` that must escape an enclosing `try`/`finally` and unwind to the
|
|
395
|
+
/// function boundary (running each `finally` on the way out). Never caught by `catch`.
|
|
396
|
+
Return(Value),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
impl fmt::Display for TishError {
|
|
400
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
401
|
+
match self {
|
|
402
|
+
TishError::Throw(v) => write!(f, "{}", v.to_display_string()),
|
|
403
|
+
TishError::Return(v) => write!(f, "return {}", v.to_display_string()),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
impl std::error::Error for TishError {}
|
|
409
|
+
|
|
410
|
+
/// Function-boundary unwind: convert a completion that escaped a function body's `Result`-closure
|
|
411
|
+
/// back into the function's `Value`. A `return v` yields `v`; an uncaught `throw` panics (matching
|
|
412
|
+
/// the behavior of a throw with no enclosing `try`); any other error panics.
|
|
413
|
+
pub fn fn_unwind(e: Box<dyn std::error::Error>) -> Value {
|
|
414
|
+
match e.downcast::<TishError>() {
|
|
415
|
+
Ok(te) => match *te {
|
|
416
|
+
TishError::Return(v) => v,
|
|
417
|
+
TishError::Throw(v) => panic!("uncaught throw: {}", v.to_display_string()),
|
|
418
|
+
},
|
|
419
|
+
Err(orig) => panic!("error in native Tish: {:?}", orig),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
424
|
+
enum LogLevel {
|
|
425
|
+
Debug = 0,
|
|
426
|
+
Info = 1,
|
|
427
|
+
Log = 2,
|
|
428
|
+
Warn = 3,
|
|
429
|
+
Error = 4,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new();
|
|
433
|
+
|
|
434
|
+
fn get_log_level() -> LogLevel {
|
|
435
|
+
*LOG_LEVEL.get_or_init(|| match std::env::var("TISH_LOG_LEVEL").as_deref() {
|
|
436
|
+
Ok("debug") => LogLevel::Debug,
|
|
437
|
+
Ok("info") => LogLevel::Info,
|
|
438
|
+
Ok("warn") => LogLevel::Warn,
|
|
439
|
+
Ok("error") => LogLevel::Error,
|
|
440
|
+
_ => LogLevel::Log,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fn format_args(args: &[Value]) -> String {
|
|
445
|
+
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors())
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
pub fn console_debug(args: &[Value]) {
|
|
449
|
+
if get_log_level() <= LogLevel::Debug {
|
|
450
|
+
println!("{}", format_args(args));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
pub fn console_info(args: &[Value]) {
|
|
455
|
+
if get_log_level() <= LogLevel::Info {
|
|
456
|
+
println!("{}", format_args(args));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
pub fn console_log(args: &[Value]) {
|
|
461
|
+
if get_log_level() <= LogLevel::Log {
|
|
462
|
+
println!("{}", format_args(args));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
pub fn console_warn(args: &[Value]) {
|
|
467
|
+
if get_log_level() <= LogLevel::Warn {
|
|
468
|
+
eprintln!("{}", format_args(args));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
pub fn console_error(args: &[Value]) {
|
|
473
|
+
eprintln!("{}", format_args(args));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
pub fn parse_int(args: &[Value]) -> Value {
|
|
477
|
+
tishlang_builtins::globals::parse_int(args)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
pub fn parse_float(args: &[Value]) -> Value {
|
|
481
|
+
tishlang_builtins::globals::parse_float(args)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
pub fn is_finite(args: &[Value]) -> Value {
|
|
485
|
+
builtins_is_finite(args)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
pub fn is_nan(args: &[Value]) -> Value {
|
|
489
|
+
builtins_is_nan(args)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
pub fn boolean(args: &[Value]) -> Value {
|
|
493
|
+
builtins_boolean(args)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
pub fn decode_uri(args: &[Value]) -> Value {
|
|
497
|
+
builtins_decode_uri(args)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
pub fn encode_uri(args: &[Value]) -> Value {
|
|
501
|
+
builtins_encode_uri(args)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Math functions - use tishlang_builtins::math
|
|
505
|
+
pub use tishlang_builtins::math::{
|
|
506
|
+
abs as tish_math_abs_impl, ceil as tish_math_ceil_impl, cos as tish_math_cos_impl,
|
|
507
|
+
exp as tish_math_exp_impl, floor as tish_math_floor_impl, max as tish_math_max_impl,
|
|
508
|
+
min as tish_math_min_impl, pow as tish_math_pow_impl, random as tish_math_random_impl,
|
|
509
|
+
round as tish_math_round_impl, sign as tish_math_sign_impl, sin as tish_math_sin_impl,
|
|
510
|
+
imul as tish_math_imul_impl,
|
|
511
|
+
sqrt as tish_math_sqrt_impl, tan as tish_math_tan_impl, trunc as tish_math_trunc_impl,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// Wrapper functions to maintain API (existing callers use math_* naming)
|
|
515
|
+
pub fn math_abs(args: &[Value]) -> Value {
|
|
516
|
+
tish_math_abs_impl(args)
|
|
517
|
+
}
|
|
518
|
+
pub fn math_sqrt(args: &[Value]) -> Value {
|
|
519
|
+
tish_math_sqrt_impl(args)
|
|
520
|
+
}
|
|
521
|
+
pub fn math_floor(args: &[Value]) -> Value {
|
|
522
|
+
tish_math_floor_impl(args)
|
|
523
|
+
}
|
|
524
|
+
pub fn math_ceil(args: &[Value]) -> Value {
|
|
525
|
+
tish_math_ceil_impl(args)
|
|
526
|
+
}
|
|
527
|
+
pub fn math_round(args: &[Value]) -> Value {
|
|
528
|
+
tish_math_round_impl(args)
|
|
529
|
+
}
|
|
530
|
+
pub fn math_min(args: &[Value]) -> Value {
|
|
531
|
+
tish_math_min_impl(args)
|
|
532
|
+
}
|
|
533
|
+
pub fn math_max(args: &[Value]) -> Value {
|
|
534
|
+
tish_math_max_impl(args)
|
|
535
|
+
}
|
|
536
|
+
pub fn math_sin(args: &[Value]) -> Value {
|
|
537
|
+
tish_math_sin_impl(args)
|
|
538
|
+
}
|
|
539
|
+
pub fn math_cos(args: &[Value]) -> Value {
|
|
540
|
+
tish_math_cos_impl(args)
|
|
541
|
+
}
|
|
542
|
+
pub fn math_tan(args: &[Value]) -> Value {
|
|
543
|
+
tish_math_tan_impl(args)
|
|
544
|
+
}
|
|
545
|
+
pub fn math_exp(args: &[Value]) -> Value {
|
|
546
|
+
tish_math_exp_impl(args)
|
|
547
|
+
}
|
|
548
|
+
pub fn math_trunc(args: &[Value]) -> Value {
|
|
549
|
+
tish_math_trunc_impl(args)
|
|
550
|
+
}
|
|
551
|
+
pub fn math_imul(args: &[Value]) -> Value {
|
|
552
|
+
tish_math_imul_impl(args)
|
|
553
|
+
}
|
|
554
|
+
pub fn math_pow(args: &[Value]) -> Value {
|
|
555
|
+
tish_math_pow_impl(args)
|
|
556
|
+
}
|
|
557
|
+
pub fn math_sign(args: &[Value]) -> Value {
|
|
558
|
+
tish_math_sign_impl(args)
|
|
559
|
+
}
|
|
560
|
+
pub fn math_random(args: &[Value]) -> Value {
|
|
561
|
+
tish_math_random_impl(args)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
pub fn math_log(args: &[Value]) -> Value {
|
|
565
|
+
let n = extract_num(args.first()).unwrap_or(f64::NAN);
|
|
566
|
+
Value::Number(n.ln())
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61). Compiled backends
|
|
570
|
+
// (native/cranelift/wasi) share this runtime, so wiring them here resolves all of them.
|
|
571
|
+
macro_rules! runtime_math_unary {
|
|
572
|
+
($name:ident, $method:ident) => {
|
|
573
|
+
pub fn $name(args: &[Value]) -> Value {
|
|
574
|
+
let n = extract_num(args.first()).unwrap_or(f64::NAN);
|
|
575
|
+
Value::Number(n.$method())
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
runtime_math_unary!(math_sinh, sinh);
|
|
580
|
+
runtime_math_unary!(math_cosh, cosh);
|
|
581
|
+
runtime_math_unary!(math_tanh, tanh);
|
|
582
|
+
runtime_math_unary!(math_asinh, asinh);
|
|
583
|
+
runtime_math_unary!(math_acosh, acosh);
|
|
584
|
+
runtime_math_unary!(math_atanh, atanh);
|
|
585
|
+
runtime_math_unary!(math_cbrt, cbrt);
|
|
586
|
+
runtime_math_unary!(math_log2, log2);
|
|
587
|
+
runtime_math_unary!(math_log10, log10);
|
|
588
|
+
|
|
589
|
+
pub fn json_stringify(args: &[Value]) -> Value {
|
|
590
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
591
|
+
Value::String(core_json_stringify(&v).into())
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
pub fn json_parse(args: &[Value]) -> Value {
|
|
595
|
+
let s = args
|
|
596
|
+
.first()
|
|
597
|
+
.map(|v| v.to_display_string())
|
|
598
|
+
.unwrap_or_default();
|
|
599
|
+
core_json_parse(&s).unwrap_or(Value::Null)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
pub fn array_is_array(args: &[Value]) -> Value {
|
|
604
|
+
builtins_array_is_array(args)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
pub fn string_from_char_code(args: &[Value]) -> Value {
|
|
608
|
+
builtins_string_from_char_code(args)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/// `String(value)` as a function (JS `ToString`). Wired into the codegen `String`
|
|
612
|
+
/// global as `__call` so compiled `String(x)` matches the VM/interp.
|
|
613
|
+
pub fn string_convert(args: &[Value]) -> Value {
|
|
614
|
+
builtins_string_convert(args)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
pub fn number_convert(args: &[Value]) -> Value {
|
|
618
|
+
builtins_number_convert(args)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#[cfg(feature = "process")]
|
|
622
|
+
pub fn process_exit(args: &[Value]) -> Value {
|
|
623
|
+
let code = args
|
|
624
|
+
.first()
|
|
625
|
+
.and_then(|v| match v {
|
|
626
|
+
Value::Number(n) => Some(*n as i32),
|
|
627
|
+
_ => None,
|
|
628
|
+
})
|
|
629
|
+
.unwrap_or(0);
|
|
630
|
+
std::process::exit(code);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
#[cfg(feature = "process")]
|
|
634
|
+
pub fn process_cwd(_args: &[Value]) -> Value {
|
|
635
|
+
let cwd = std::env::current_dir()
|
|
636
|
+
.map(|p| p.display().to_string())
|
|
637
|
+
.unwrap_or_default();
|
|
638
|
+
Value::String(cwd.into())
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#[cfg(feature = "process")]
|
|
642
|
+
pub fn process_exec(args: &[Value]) -> Value {
|
|
643
|
+
use std::process::Command;
|
|
644
|
+
let cmd = args
|
|
645
|
+
.first()
|
|
646
|
+
.map(|v| v.to_display_string())
|
|
647
|
+
.unwrap_or_default();
|
|
648
|
+
if cmd.is_empty() {
|
|
649
|
+
return Value::Number(0.0);
|
|
650
|
+
}
|
|
651
|
+
match Command::new("sh").arg("-c").arg(&cmd).status() {
|
|
652
|
+
Ok(status) => Value::Number(status.code().unwrap_or(1) as f64),
|
|
653
|
+
Err(_) => Value::Number(1.0),
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[cfg(feature = "fs")]
|
|
658
|
+
pub fn read_file(args: &[Value]) -> Value {
|
|
659
|
+
let path = args
|
|
660
|
+
.first()
|
|
661
|
+
.map(|v| v.to_display_string())
|
|
662
|
+
.unwrap_or_default();
|
|
663
|
+
match std::fs::read_to_string(&path) {
|
|
664
|
+
Ok(content) => Value::String(content.into()),
|
|
665
|
+
Err(e) => make_error_value(e),
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#[cfg(feature = "fs")]
|
|
670
|
+
pub fn write_file(args: &[Value]) -> Value {
|
|
671
|
+
let path = args
|
|
672
|
+
.first()
|
|
673
|
+
.map(|v| v.to_display_string())
|
|
674
|
+
.unwrap_or_default();
|
|
675
|
+
let content = args
|
|
676
|
+
.get(1)
|
|
677
|
+
.map(|v| v.to_display_string())
|
|
678
|
+
.unwrap_or_default();
|
|
679
|
+
match std::fs::write(&path, &content) {
|
|
680
|
+
Ok(()) => Value::Bool(true),
|
|
681
|
+
Err(e) => make_error_value(e),
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#[cfg(feature = "fs")]
|
|
686
|
+
pub fn file_exists(args: &[Value]) -> Value {
|
|
687
|
+
let path = args
|
|
688
|
+
.first()
|
|
689
|
+
.map(|v| v.to_display_string())
|
|
690
|
+
.unwrap_or_default();
|
|
691
|
+
Value::Bool(std::path::Path::new(&path).exists())
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
#[cfg(feature = "fs")]
|
|
695
|
+
pub fn is_dir(args: &[Value]) -> Value {
|
|
696
|
+
let path = args
|
|
697
|
+
.first()
|
|
698
|
+
.map(|v| v.to_display_string())
|
|
699
|
+
.unwrap_or_default();
|
|
700
|
+
Value::Bool(std::path::Path::new(&path).is_dir())
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#[cfg(feature = "fs")]
|
|
704
|
+
pub fn read_dir(args: &[Value]) -> Value {
|
|
705
|
+
let path = args
|
|
706
|
+
.first()
|
|
707
|
+
.map(|v| v.to_display_string())
|
|
708
|
+
.unwrap_or_else(|| ".".to_string());
|
|
709
|
+
match std::fs::read_dir(&path) {
|
|
710
|
+
Ok(entries) => {
|
|
711
|
+
let files: Vec<Value> = entries
|
|
712
|
+
.filter_map(|e| e.ok())
|
|
713
|
+
.map(|e| Value::String(e.file_name().to_string_lossy().into()))
|
|
714
|
+
.collect();
|
|
715
|
+
Value::Array(VmRef::new(files))
|
|
716
|
+
}
|
|
717
|
+
Err(e) => make_error_value(e),
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#[cfg(feature = "fs")]
|
|
722
|
+
pub fn mkdir(args: &[Value]) -> Value {
|
|
723
|
+
let path = args
|
|
724
|
+
.first()
|
|
725
|
+
.map(|v| v.to_display_string())
|
|
726
|
+
.unwrap_or_default();
|
|
727
|
+
match std::fs::create_dir_all(&path) {
|
|
728
|
+
Ok(()) => Value::Bool(true),
|
|
729
|
+
Err(e) => make_error_value(e),
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
use std::sync::Arc;
|
|
734
|
+
|
|
735
|
+
#[inline]
|
|
736
|
+
pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
737
|
+
let key = key.as_ref();
|
|
738
|
+
match obj {
|
|
739
|
+
Value::Object(map) => {
|
|
740
|
+
// `Set`/`Map` instances expose a computed `.size` (the backing store has no real
|
|
741
|
+
// `size` key); `collection_size` returns `None` for any other object.
|
|
742
|
+
if key == "size" {
|
|
743
|
+
if let Some(n) = collection_size(obj) {
|
|
744
|
+
return Value::Number(n);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// The map's key type is `Arc<str>`, which implements
|
|
748
|
+
// `Borrow<str>` — so we can look up with a borrowed `&str`
|
|
749
|
+
// directly. Previously we allocated a fresh `Arc<str>` on
|
|
750
|
+
// every call (one heap alloc per `r.field` read in tight
|
|
751
|
+
// handler loops); this version is alloc-free on the hit path.
|
|
752
|
+
map.borrow().strings.get(key).cloned().unwrap_or(Value::Null)
|
|
753
|
+
}
|
|
754
|
+
Value::Array(arr) => {
|
|
755
|
+
if key == "length" {
|
|
756
|
+
Value::Number(arr.borrow().len() as f64)
|
|
757
|
+
} else if let Ok(idx) = key.parse::<usize>() {
|
|
758
|
+
arr.borrow().get(idx).cloned().unwrap_or(Value::Null)
|
|
759
|
+
} else {
|
|
760
|
+
Value::Null
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Packed `Float64Array` (`TISH_PACKED_ARRAYS`): `.length` and numeric-key reads, mirroring
|
|
764
|
+
// the boxed `Array` arm. Methods (`reduce`/`map`/…) materialise via `as_boxed_array`.
|
|
765
|
+
Value::NumberArray(arr) => {
|
|
766
|
+
if key == "length" {
|
|
767
|
+
Value::Number(arr.borrow().len() as f64)
|
|
768
|
+
} else if let Ok(idx) = key.parse::<usize>() {
|
|
769
|
+
arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
|
|
770
|
+
} else {
|
|
771
|
+
Value::Null
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
Value::String(s) => {
|
|
775
|
+
if key == "length" {
|
|
776
|
+
Value::Number(s.chars().count() as f64)
|
|
777
|
+
} else {
|
|
778
|
+
Value::Null
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
#[cfg(feature = "regex")]
|
|
782
|
+
Value::RegExp(re) => match key {
|
|
783
|
+
"exec" => {
|
|
784
|
+
let rc = re.clone();
|
|
785
|
+
Value::native(move |args: &[Value]| {
|
|
786
|
+
let input = args.first().unwrap_or(&Value::Null);
|
|
787
|
+
regexp_exec(&Value::RegExp(rc.clone()), input)
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
"test" => {
|
|
791
|
+
let rc = re.clone();
|
|
792
|
+
Value::native(move |args: &[Value]| {
|
|
793
|
+
let input = args.first().unwrap_or(&Value::Null);
|
|
794
|
+
regexp_test(&Value::RegExp(rc.clone()), input)
|
|
795
|
+
})
|
|
796
|
+
}
|
|
797
|
+
// Properties — mirror the interpreter + bytecode VM so all backends agree.
|
|
798
|
+
"source" => Value::String(re.borrow().source.clone().into()),
|
|
799
|
+
"flags" => Value::String(re.borrow().flags_string().into()),
|
|
800
|
+
"lastIndex" => Value::Number(re.borrow().last_index as f64),
|
|
801
|
+
"global" => Value::Bool(re.borrow().flags.global),
|
|
802
|
+
"ignoreCase" => Value::Bool(re.borrow().flags.ignore_case),
|
|
803
|
+
"multiline" => Value::Bool(re.borrow().flags.multiline),
|
|
804
|
+
"dotAll" => Value::Bool(re.borrow().flags.dot_all),
|
|
805
|
+
"unicode" => Value::Bool(re.borrow().flags.unicode),
|
|
806
|
+
"sticky" => Value::Bool(re.borrow().flags.sticky),
|
|
807
|
+
_ => Value::Null,
|
|
808
|
+
},
|
|
809
|
+
Value::Opaque(o) => o
|
|
810
|
+
.get_method(key)
|
|
811
|
+
.map(Value::Function)
|
|
812
|
+
.unwrap_or(Value::Null),
|
|
813
|
+
// Promise instance methods (`.then`/`.catch`), bound to this promise. Returning a
|
|
814
|
+
// callable here makes the rust backend match the VM family (interp/vm/cranelift/wasi),
|
|
815
|
+
// which expose these via `GetMember`. Both `p.then(cb)` (member) and `p["catch"](cb)`
|
|
816
|
+
// (index, used because `catch` is reserved) route through here / `get_index`.
|
|
817
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
818
|
+
Value::Promise(p) => match key {
|
|
819
|
+
"then" => {
|
|
820
|
+
let pc = p.clone();
|
|
821
|
+
Value::native(move |args: &[Value]| promise_instance_then(&pc, args))
|
|
822
|
+
}
|
|
823
|
+
"catch" => {
|
|
824
|
+
let pc = p.clone();
|
|
825
|
+
Value::native(move |args: &[Value]| promise_instance_catch(&pc, args))
|
|
826
|
+
}
|
|
827
|
+
_ => Value::Null,
|
|
828
|
+
},
|
|
829
|
+
_ => Value::Null,
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
#[inline]
|
|
834
|
+
pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
835
|
+
match obj {
|
|
836
|
+
Value::Array(arr) => {
|
|
837
|
+
let idx = match index {
|
|
838
|
+
Value::Number(n) => *n as usize,
|
|
839
|
+
_ => return Value::Null,
|
|
840
|
+
};
|
|
841
|
+
arr.borrow().get(idx).cloned().unwrap_or(Value::Null)
|
|
842
|
+
}
|
|
843
|
+
// Packed `Float64Array` indexing (`TISH_PACKED_ARRAYS`); mirrors the boxed `Array` arm.
|
|
844
|
+
Value::NumberArray(arr) => {
|
|
845
|
+
let idx = match index {
|
|
846
|
+
Value::Number(n) => *n as usize,
|
|
847
|
+
_ => return Value::Null,
|
|
848
|
+
};
|
|
849
|
+
arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
|
|
850
|
+
}
|
|
851
|
+
// `str[i]` returns the character at index `i` (issue #17) — matches the VM /
|
|
852
|
+
// interpreter; out-of-bounds / negative / non-integer indices yield null.
|
|
853
|
+
Value::String(s) => match index {
|
|
854
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => s
|
|
855
|
+
.chars()
|
|
856
|
+
.nth(*n as usize)
|
|
857
|
+
.map(|c| Value::String(c.to_string().into()))
|
|
858
|
+
.unwrap_or(Value::Null),
|
|
859
|
+
_ => Value::Null,
|
|
860
|
+
},
|
|
861
|
+
Value::Object(_) => tishlang_core::object_get(obj, index).unwrap_or(Value::Null),
|
|
862
|
+
// `promise["then"|"catch"]` — string-keyed access mirrors `get_prop` (bracket form
|
|
863
|
+
// is required for `catch`, a reserved word). Keeps the rust backend on par with the VM.
|
|
864
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
865
|
+
Value::Promise(_) => match index {
|
|
866
|
+
Value::String(k) => get_prop(obj, k.as_str()),
|
|
867
|
+
_ => Value::Null,
|
|
868
|
+
},
|
|
869
|
+
_ => Value::Null,
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/// `delete obj[key]` / `delete obj.prop` (issue #40). Objects drop the string key; arrays
|
|
874
|
+
/// clear a numeric index to a `null` hole (length preserved). Always evaluates to `true`.
|
|
875
|
+
#[inline]
|
|
876
|
+
pub fn delete_property(obj: &Value, key: &Value) -> Value {
|
|
877
|
+
match obj {
|
|
878
|
+
Value::Object(m) => {
|
|
879
|
+
let key_s = match key {
|
|
880
|
+
Value::String(s) => s.to_string(),
|
|
881
|
+
other => other.to_js_string(),
|
|
882
|
+
};
|
|
883
|
+
m.borrow_mut().strings.remove(key_s.as_str());
|
|
884
|
+
}
|
|
885
|
+
Value::Array(arr) => {
|
|
886
|
+
if let Value::Number(n) = key {
|
|
887
|
+
let n = *n;
|
|
888
|
+
if n >= 0.0 && n.fract() == 0.0 {
|
|
889
|
+
let i = n as usize;
|
|
890
|
+
let mut a = arr.borrow_mut();
|
|
891
|
+
if i < a.len() {
|
|
892
|
+
a[i] = Value::Null;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
_ => {}
|
|
898
|
+
}
|
|
899
|
+
Value::Bool(true)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
#[inline]
|
|
903
|
+
pub fn set_prop(obj: &Value, key: &str, val: Value) -> Value {
|
|
904
|
+
match obj {
|
|
905
|
+
Value::Object(map) => {
|
|
906
|
+
// Try the in-place update path first: if the key already
|
|
907
|
+
// exists we re-use the existing `Arc<str>` and skip the
|
|
908
|
+
// alloc. Only newly-inserted keys pay for `Arc::from(key)`.
|
|
909
|
+
let mut m = map.borrow_mut();
|
|
910
|
+
if let Some(slot) = m.strings.get_mut(key) {
|
|
911
|
+
*slot = val.clone();
|
|
912
|
+
} else {
|
|
913
|
+
m.strings.insert(Arc::from(key), val.clone());
|
|
914
|
+
}
|
|
915
|
+
val
|
|
916
|
+
}
|
|
917
|
+
// `arr.length = k` truncates / grows the array (holes read back as Null), matching
|
|
918
|
+
// JS and the VM/interpreter (issue #62).
|
|
919
|
+
Value::Array(arr) if key == "length" => {
|
|
920
|
+
let n = extract_num(Some(&val)).unwrap_or(f64::NAN);
|
|
921
|
+
if n.is_nan() || n < 0.0 || n.fract() != 0.0 || n > 4_294_967_295.0 {
|
|
922
|
+
panic!("Invalid array length");
|
|
923
|
+
}
|
|
924
|
+
arr.borrow_mut().resize(n as usize, Value::Null);
|
|
925
|
+
val
|
|
926
|
+
}
|
|
927
|
+
_ => panic!("Cannot assign property on non-object"),
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#[inline]
|
|
932
|
+
pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
|
|
933
|
+
match obj {
|
|
934
|
+
Value::Array(arr) => {
|
|
935
|
+
let index = match idx {
|
|
936
|
+
Value::Number(n) => *n as usize,
|
|
937
|
+
_ => panic!("Array index must be number"),
|
|
938
|
+
};
|
|
939
|
+
let mut arr_mut = arr.borrow_mut();
|
|
940
|
+
while arr_mut.len() <= index {
|
|
941
|
+
arr_mut.push(Value::Null);
|
|
942
|
+
}
|
|
943
|
+
arr_mut[index] = val.clone();
|
|
944
|
+
val
|
|
945
|
+
}
|
|
946
|
+
// Packed `Float64Array` write (`TISH_PACKED_ARRAYS`). On the native path a `NumberArray` is
|
|
947
|
+
// always a `Float64Array`, so storing the f64 (non-numeric → `NaN`) is the correct view
|
|
948
|
+
// semantics — and unlike the boxed v1 backing, it actually coerces the write to the element
|
|
949
|
+
// type. Out-of-range index zero-fills, matching the boxed grow-with-Null behaviour.
|
|
950
|
+
Value::NumberArray(arr) => {
|
|
951
|
+
let index = match idx {
|
|
952
|
+
Value::Number(n) => *n as usize,
|
|
953
|
+
_ => panic!("Array index must be number"),
|
|
954
|
+
};
|
|
955
|
+
let mut arr_mut = arr.borrow_mut();
|
|
956
|
+
while arr_mut.len() <= index {
|
|
957
|
+
arr_mut.push(0.0);
|
|
958
|
+
}
|
|
959
|
+
arr_mut[index] = val.as_number().unwrap_or(f64::NAN);
|
|
960
|
+
val
|
|
961
|
+
}
|
|
962
|
+
Value::Object(_) => {
|
|
963
|
+
tishlang_core::object_set(obj, idx, val.clone()).expect("object set");
|
|
964
|
+
val
|
|
965
|
+
}
|
|
966
|
+
_ => panic!("Cannot index assign on non-array/object"),
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
pub fn in_operator(key: &Value, obj: &Value) -> Value {
|
|
971
|
+
match obj {
|
|
972
|
+
Value::Object(_) => Value::Bool(tishlang_core::object_has(obj, key)),
|
|
973
|
+
Value::Array(arr) => {
|
|
974
|
+
let key_str: Arc<str> = match key {
|
|
975
|
+
Value::String(s) => Arc::from(s.as_str()),
|
|
976
|
+
Value::Number(n) => n.to_string().into(),
|
|
977
|
+
_ => return Value::Bool(false),
|
|
978
|
+
};
|
|
979
|
+
let result = key_str.as_ref() == "length"
|
|
980
|
+
|| key_str
|
|
981
|
+
.parse::<usize>()
|
|
982
|
+
.ok()
|
|
983
|
+
.map(|i| i < arr.borrow().len())
|
|
984
|
+
.unwrap_or(false);
|
|
985
|
+
Value::Bool(result)
|
|
986
|
+
}
|
|
987
|
+
// Packed `Float64Array` (`TISH_PACKED_ARRAYS`); same key set as the boxed `Array` arm.
|
|
988
|
+
Value::NumberArray(arr) => {
|
|
989
|
+
let key_str: Arc<str> = match key {
|
|
990
|
+
Value::String(s) => Arc::from(s.as_str()),
|
|
991
|
+
Value::Number(n) => n.to_string().into(),
|
|
992
|
+
_ => return Value::Bool(false),
|
|
993
|
+
};
|
|
994
|
+
let result = key_str.as_ref() == "length"
|
|
995
|
+
|| key_str
|
|
996
|
+
.parse::<usize>()
|
|
997
|
+
.ok()
|
|
998
|
+
.map(|i| i < arr.borrow().len())
|
|
999
|
+
.unwrap_or(false);
|
|
1000
|
+
Value::Bool(result)
|
|
1001
|
+
}
|
|
1002
|
+
_ => Value::Bool(false),
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Object functions - delegate to tishlang_builtins::globals
|
|
1007
|
+
pub fn object_assign(args: &[Value]) -> Value {
|
|
1008
|
+
builtins_object_assign(args)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
pub fn object_keys(args: &[Value]) -> Value {
|
|
1012
|
+
builtins_object_keys(args)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
pub fn object_values(args: &[Value]) -> Value {
|
|
1016
|
+
builtins_object_values(args)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
pub fn object_entries(args: &[Value]) -> Value {
|
|
1020
|
+
builtins_object_entries(args)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
pub fn object_from_entries(args: &[Value]) -> Value {
|
|
1024
|
+
builtins_object_from_entries(args)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// HTTP Support
|
|
1028
|
+
#[cfg(feature = "http")]
|
|
1029
|
+
mod promise_io;
|
|
1030
|
+
|
|
1031
|
+
#[cfg(feature = "http")]
|
|
1032
|
+
mod http;
|
|
1033
|
+
|
|
1034
|
+
#[cfg(feature = "http")]
|
|
1035
|
+
mod http_prefork;
|
|
1036
|
+
|
|
1037
|
+
#[cfg(feature = "http-io-uring")]
|
|
1038
|
+
mod http_io_uring;
|
|
1039
|
+
|
|
1040
|
+
#[cfg(feature = "http-hyper")]
|
|
1041
|
+
mod http_hyper;
|
|
1042
|
+
|
|
1043
|
+
#[cfg(feature = "http")]
|
|
1044
|
+
mod http_fetch;
|
|
1045
|
+
|
|
1046
|
+
mod timers;
|
|
1047
|
+
|
|
1048
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
1049
|
+
mod promise;
|
|
1050
|
+
|
|
1051
|
+
#[cfg(feature = "http")]
|
|
1052
|
+
mod native_promise;
|
|
1053
|
+
|
|
1054
|
+
#[cfg(feature = "ws")]
|
|
1055
|
+
mod ws;
|
|
1056
|
+
|
|
1057
|
+
#[cfg(feature = "tty")]
|
|
1058
|
+
pub mod tty;
|
|
1059
|
+
#[cfg(feature = "tty")]
|
|
1060
|
+
pub use tty::{
|
|
1061
|
+
tty_enter_alt_screen, tty_is_tty, tty_leave_alt_screen, tty_read, tty_read_line, tty_set_raw_mode, tty_size,
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
#[cfg(feature = "ws")]
|
|
1065
|
+
pub use ws::{
|
|
1066
|
+
web_socket_client, web_socket_server_accept, web_socket_server_construct,
|
|
1067
|
+
web_socket_server_listen, ws_broadcast_native, ws_send_native,
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
#[cfg(feature = "http")]
|
|
1071
|
+
pub use http::{
|
|
1072
|
+
await_fetch as http_await_fetch, await_fetch_all as http_await_fetch_all,
|
|
1073
|
+
register_static_route,
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// `serve` is the user-facing entry point for Tish's HTTP server. By default
|
|
1077
|
+
// it uses the tiny_http + SO_REUSEPORT path in `http.rs`. When compiled with
|
|
1078
|
+
// `--features http-hyper` and the `TISH_HTTP_BACKEND=hyper` env var is set
|
|
1079
|
+
// at runtime, it dispatches to the hyper backend in `http_hyper.rs`.
|
|
1080
|
+
//
|
|
1081
|
+
// The env-var switch (rather than a cargo feature switch) means one built
|
|
1082
|
+
// binary can toggle backends for A/B benchmarking and production rollout
|
|
1083
|
+
// without rebuilding. When `http-hyper` is not compiled in, the switch is a
|
|
1084
|
+
// no-op and the tiny_http path is used unconditionally.
|
|
1085
|
+
#[cfg(feature = "http")]
|
|
1086
|
+
pub fn http_serve<F>(args: &[tishlang_core::Value], handler: F) -> tishlang_core::Value
|
|
1087
|
+
where
|
|
1088
|
+
F: Fn(&[tishlang_core::Value]) -> tishlang_core::Value + Send + Sync + 'static,
|
|
1089
|
+
{
|
|
1090
|
+
#[cfg(feature = "http-hyper")]
|
|
1091
|
+
{
|
|
1092
|
+
if http_hyper::is_enabled_via_env() {
|
|
1093
|
+
return http_hyper::serve(args, handler);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
http::serve(args, handler)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/// `serve(port, { onWorker: (workerId) => handler })` — the object form of
|
|
1100
|
+
/// `serve`. Picks up `onWorker`, invokes it once per HTTP accept thread to
|
|
1101
|
+
/// build that thread's handler, then enters the normal parallel accept
|
|
1102
|
+
/// loop. See [`http::serve_per_worker`] for the full doc.
|
|
1103
|
+
///
|
|
1104
|
+
/// This is broadly useful for any Tish app that wants per-worker state —
|
|
1105
|
+
/// DB connection pools, in-process caches, counters, etc. — without a
|
|
1106
|
+
/// global `RwLock` or forcing everything through the single-thread
|
|
1107
|
+
/// dispatcher. It also plays nicely with prefork: `onWorker` sees global
|
|
1108
|
+
/// worker ids across processes so logs and sharded state are easy to key.
|
|
1109
|
+
#[cfg(feature = "http")]
|
|
1110
|
+
pub fn http_serve_per_worker(
|
|
1111
|
+
args: &[tishlang_core::Value],
|
|
1112
|
+
factory_value: tishlang_core::Value,
|
|
1113
|
+
) -> tishlang_core::Value {
|
|
1114
|
+
use tishlang_core::Value;
|
|
1115
|
+
// factory_value should be Value::Function (passed down by codegen after
|
|
1116
|
+
// extracting `onWorker` from the options object).
|
|
1117
|
+
let Value::Function(factory) = factory_value else {
|
|
1118
|
+
eprintln!("[tish http] serve: onWorker must be a function (id) => handler");
|
|
1119
|
+
return Value::Null;
|
|
1120
|
+
};
|
|
1121
|
+
let factory: tishlang_core::NativeFn = factory;
|
|
1122
|
+
http::serve_per_worker(args, move |worker_id| {
|
|
1123
|
+
let handler_val = factory.call(&[Value::Number(worker_id as f64)]);
|
|
1124
|
+
match handler_val {
|
|
1125
|
+
Value::Function(f) => f,
|
|
1126
|
+
_ => panic!(
|
|
1127
|
+
"onWorker returned {:?} for worker {}; must return a function",
|
|
1128
|
+
handler_val, worker_id
|
|
1129
|
+
),
|
|
1130
|
+
}
|
|
1131
|
+
})
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
pub use timers::{
|
|
1135
|
+
clear_interval as timer_clear_interval, clear_timeout as timer_clear_timeout, drain_timers,
|
|
1136
|
+
set_interval as timer_set_interval, set_timeout as timer_set_timeout,
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
1140
|
+
pub use promise::{
|
|
1141
|
+
await_promise, await_promise_throw, promise_instance_catch, promise_instance_then,
|
|
1142
|
+
promise_object, promise_spawn as promise_spawn_value,
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
#[cfg(feature = "http")]
|
|
1146
|
+
pub use native_promise::{fetch_all_promise, fetch_async_promise, fetch_promise};
|
|
1147
|
+
|
|
1148
|
+
// RegExp Support
|
|
1149
|
+
#[cfg(feature = "regex")]
|
|
1150
|
+
pub use tishlang_core::{RegExpFlags, TishRegExp};
|
|
1151
|
+
|
|
1152
|
+
#[cfg(feature = "regex")]
|
|
1153
|
+
pub fn regexp_new(args: &[Value]) -> Value {
|
|
1154
|
+
let pattern = match args.first() {
|
|
1155
|
+
Some(Value::String(s)) => s.to_string(),
|
|
1156
|
+
Some(v) => v.to_display_string(),
|
|
1157
|
+
None => String::new(),
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
let flags = match args.get(1) {
|
|
1161
|
+
Some(Value::String(s)) => s.to_string(),
|
|
1162
|
+
Some(Value::Null) | None => String::new(),
|
|
1163
|
+
Some(v) => v.to_display_string(),
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
match TishRegExp::new(&pattern, &flags) {
|
|
1167
|
+
Ok(re) => Value::RegExp(VmRef::new(re)),
|
|
1168
|
+
Err(e) => {
|
|
1169
|
+
eprintln!("RegExp error: {}", e);
|
|
1170
|
+
Value::Null
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
#[cfg(feature = "regex")]
|
|
1176
|
+
pub fn regexp_test(re: &Value, input: &Value) -> Value {
|
|
1177
|
+
if let Value::RegExp(re) = re {
|
|
1178
|
+
let input_str = input.to_display_string();
|
|
1179
|
+
Value::Bool(re.borrow_mut().test(&input_str))
|
|
1180
|
+
} else {
|
|
1181
|
+
Value::Bool(false)
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
#[cfg(feature = "regex")]
|
|
1186
|
+
pub fn regexp_exec(re: &Value, input: &Value) -> Value {
|
|
1187
|
+
if let Value::RegExp(re) = re {
|
|
1188
|
+
let input_str = input.to_display_string();
|
|
1189
|
+
regexp_exec_impl(&mut re.borrow_mut(), &input_str)
|
|
1190
|
+
} else {
|
|
1191
|
+
Value::Null
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
#[cfg(feature = "regex")]
|
|
1196
|
+
fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
|
|
1197
|
+
use tishlang_core::ObjectMap;
|
|
1198
|
+
|
|
1199
|
+
let start = if re.flags.global || re.flags.sticky {
|
|
1200
|
+
re.last_index
|
|
1201
|
+
} else {
|
|
1202
|
+
0
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
let char_count = input.chars().count();
|
|
1206
|
+
if start > char_count {
|
|
1207
|
+
if re.flags.global || re.flags.sticky {
|
|
1208
|
+
re.last_index = 0;
|
|
1209
|
+
}
|
|
1210
|
+
return Value::Null;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
let byte_start: usize = input.chars().take(start).map(|c| c.len_utf8()).sum();
|
|
1214
|
+
let search_str = &input[byte_start..];
|
|
1215
|
+
|
|
1216
|
+
match re.regex.captures(search_str) {
|
|
1217
|
+
Ok(Some(caps)) => {
|
|
1218
|
+
let full_match = caps.get(0).unwrap();
|
|
1219
|
+
|
|
1220
|
+
if re.flags.sticky && full_match.start() != 0 {
|
|
1221
|
+
re.last_index = 0;
|
|
1222
|
+
return Value::Null;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let match_byte_start = byte_start + full_match.start();
|
|
1226
|
+
let match_char_index = input[..match_byte_start].chars().count();
|
|
1227
|
+
|
|
1228
|
+
let mut obj: ObjectMap = ObjectMap::default();
|
|
1229
|
+
obj.insert(Arc::from("0"), Value::String(full_match.as_str().into()));
|
|
1230
|
+
for i in 1..caps.len() {
|
|
1231
|
+
let val = match caps.get(i) {
|
|
1232
|
+
Some(m) => Value::String(m.as_str().into()),
|
|
1233
|
+
None => Value::Null,
|
|
1234
|
+
};
|
|
1235
|
+
obj.insert(Arc::from(i.to_string().as_str()), val);
|
|
1236
|
+
}
|
|
1237
|
+
obj.insert(Arc::from("index"), Value::Number(match_char_index as f64));
|
|
1238
|
+
|
|
1239
|
+
if re.flags.global || re.flags.sticky {
|
|
1240
|
+
let match_end_chars = input[..byte_start + full_match.end()].chars().count();
|
|
1241
|
+
re.last_index = if full_match.start() == full_match.end() {
|
|
1242
|
+
match_end_chars + 1
|
|
1243
|
+
} else {
|
|
1244
|
+
match_end_chars
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
Value::object(obj)
|
|
1249
|
+
}
|
|
1250
|
+
Ok(None) | Err(_) => {
|
|
1251
|
+
if re.flags.global || re.flags.sticky {
|
|
1252
|
+
re.last_index = 0;
|
|
1253
|
+
}
|
|
1254
|
+
Value::Null
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
#[cfg(feature = "regex")]
|
|
1260
|
+
pub fn string_split_regex(s: &Value, separator: &Value, limit: Option<usize>) -> Value {
|
|
1261
|
+
let input = match s {
|
|
1262
|
+
Value::String(s) => s.as_ref(),
|
|
1263
|
+
_ => return Value::Array(VmRef::new(vec![s.clone()])),
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
let max = limit.unwrap_or(usize::MAX);
|
|
1267
|
+
if max == 0 {
|
|
1268
|
+
return Value::Array(VmRef::new(Vec::new()));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
match separator {
|
|
1272
|
+
Value::RegExp(re) => {
|
|
1273
|
+
let re = re.borrow();
|
|
1274
|
+
let mut result = Vec::new();
|
|
1275
|
+
let mut last_end = 0;
|
|
1276
|
+
|
|
1277
|
+
for mat in re.regex.find_iter(input) {
|
|
1278
|
+
match mat {
|
|
1279
|
+
Ok(m) => {
|
|
1280
|
+
if result.len() >= max - 1 {
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
result.push(Value::String(input[last_end..m.start()].into()));
|
|
1284
|
+
last_end = m.end();
|
|
1285
|
+
}
|
|
1286
|
+
Err(_) => break,
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if result.len() < max {
|
|
1291
|
+
result.push(Value::String(input[last_end..].into()));
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
Value::Array(VmRef::new(result))
|
|
1295
|
+
}
|
|
1296
|
+
Value::String(sep) => {
|
|
1297
|
+
let parts: Vec<Value> = input
|
|
1298
|
+
.splitn(max, sep.as_str())
|
|
1299
|
+
.map(|s| Value::String(s.into()))
|
|
1300
|
+
.collect();
|
|
1301
|
+
Value::Array(VmRef::new(parts))
|
|
1302
|
+
}
|
|
1303
|
+
_ => Value::Array(VmRef::new(vec![Value::String(input.into())])),
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
#[cfg(feature = "regex")]
|
|
1308
|
+
pub fn string_match_regex(s: &Value, regexp: &Value) -> Value {
|
|
1309
|
+
let input = match s {
|
|
1310
|
+
Value::String(s) => s.as_ref(),
|
|
1311
|
+
_ => return Value::Null,
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
match regexp {
|
|
1315
|
+
Value::RegExp(re) => {
|
|
1316
|
+
let mut re = re.borrow_mut();
|
|
1317
|
+
|
|
1318
|
+
if re.flags.global {
|
|
1319
|
+
let mut matches = Vec::new();
|
|
1320
|
+
re.last_index = 0;
|
|
1321
|
+
|
|
1322
|
+
while let Ok(Some(m)) = re.regex.find_from_pos(input, re.last_index) {
|
|
1323
|
+
matches.push(Value::String(m.as_str().into()));
|
|
1324
|
+
if m.start() == m.end() {
|
|
1325
|
+
re.last_index = m.end() + 1;
|
|
1326
|
+
} else {
|
|
1327
|
+
re.last_index = m.end();
|
|
1328
|
+
}
|
|
1329
|
+
if re.last_index > input.len() {
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
re.last_index = 0;
|
|
1335
|
+
|
|
1336
|
+
if matches.is_empty() {
|
|
1337
|
+
Value::Null
|
|
1338
|
+
} else {
|
|
1339
|
+
Value::Array(VmRef::new(matches))
|
|
1340
|
+
}
|
|
1341
|
+
} else {
|
|
1342
|
+
regexp_exec_impl(&mut re, input)
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
Value::String(pattern) => match tishlang_core::TishRegExp::new(pattern, "") {
|
|
1346
|
+
Ok(mut re) => regexp_exec_impl(&mut re, input),
|
|
1347
|
+
Err(_) => Value::Null,
|
|
1348
|
+
},
|
|
1349
|
+
_ => Value::Null,
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
#[cfg(feature = "regex")]
|
|
1354
|
+
fn string_replace_regex_or_callback(s: &Value, search: &Value, replacement: &Value) -> Value {
|
|
1355
|
+
let input = match s {
|
|
1356
|
+
Value::String(s) => s.as_ref(),
|
|
1357
|
+
_ => return s.clone(),
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
let Value::RegExp(re) = search else {
|
|
1361
|
+
return s.clone();
|
|
1362
|
+
};
|
|
1363
|
+
let re_guard = re.borrow();
|
|
1364
|
+
|
|
1365
|
+
if let Value::Function(cb) = replacement {
|
|
1366
|
+
let limit = if re_guard.flags.global { usize::MAX } else { 1 };
|
|
1367
|
+
let mut result = String::new();
|
|
1368
|
+
let mut last_end: usize = 0;
|
|
1369
|
+
for (count, cap_result) in re_guard.regex.captures_iter(input).enumerate() {
|
|
1370
|
+
if count >= limit {
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
let Ok(caps) = cap_result else {
|
|
1374
|
+
break;
|
|
1375
|
+
};
|
|
1376
|
+
let full = caps.get(0).unwrap();
|
|
1377
|
+
let match_str = full.as_str();
|
|
1378
|
+
let byte_start = full.start();
|
|
1379
|
+
let char_index = input[..byte_start].chars().count();
|
|
1380
|
+
|
|
1381
|
+
let mut args = vec![Value::String(match_str.into())];
|
|
1382
|
+
for i in 1..caps.len() {
|
|
1383
|
+
let val = match caps.get(i) {
|
|
1384
|
+
Some(m) => Value::String(m.as_str().into()),
|
|
1385
|
+
None => Value::Null,
|
|
1386
|
+
};
|
|
1387
|
+
args.push(val);
|
|
1388
|
+
}
|
|
1389
|
+
args.push(Value::Number(char_index as f64));
|
|
1390
|
+
args.push(Value::String(input.into()));
|
|
1391
|
+
|
|
1392
|
+
let repl_val = cb.call(&args);
|
|
1393
|
+
let repl_str = repl_val.to_display_string();
|
|
1394
|
+
result.push_str(&input[last_end..byte_start]);
|
|
1395
|
+
result.push_str(&repl_str);
|
|
1396
|
+
last_end = full.end();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
result.push_str(&input[last_end..]);
|
|
1400
|
+
Value::String(result.into())
|
|
1401
|
+
} else {
|
|
1402
|
+
let replacement_str = replacement.to_display_string();
|
|
1403
|
+
if re_guard.flags.global {
|
|
1404
|
+
match re_guard.regex.replace_all(input, replacement_str.as_str()) {
|
|
1405
|
+
std::borrow::Cow::Borrowed(x) => Value::String(x.into()),
|
|
1406
|
+
std::borrow::Cow::Owned(x) => Value::String(x.into()),
|
|
1407
|
+
}
|
|
1408
|
+
} else {
|
|
1409
|
+
match re_guard.regex.replace(input, replacement_str.as_str()) {
|
|
1410
|
+
std::borrow::Cow::Borrowed(x) => Value::String(x.into()),
|
|
1411
|
+
std::borrow::Cow::Owned(x) => Value::String(x.into()),
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
#[cfg(feature = "regex")]
|
|
1418
|
+
pub fn string_search_regex(s: &Value, regexp: &Value) -> Value {
|
|
1419
|
+
let input = match s {
|
|
1420
|
+
Value::String(s) => s.as_ref(),
|
|
1421
|
+
_ => return Value::Number(-1.0),
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
match regexp {
|
|
1425
|
+
Value::RegExp(re) => {
|
|
1426
|
+
let re = re.borrow();
|
|
1427
|
+
match re.regex.find(input) {
|
|
1428
|
+
Ok(Some(m)) => {
|
|
1429
|
+
let char_index = input[..m.start()].chars().count();
|
|
1430
|
+
Value::Number(char_index as f64)
|
|
1431
|
+
}
|
|
1432
|
+
_ => Value::Number(-1.0),
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
Value::String(pattern) => match tishlang_core::TishRegExp::new(pattern, "") {
|
|
1436
|
+
Ok(re) => match re.regex.find(input) {
|
|
1437
|
+
Ok(Some(m)) => {
|
|
1438
|
+
let char_index = input[..m.start()].chars().count();
|
|
1439
|
+
Value::Number(char_index as f64)
|
|
1440
|
+
}
|
|
1441
|
+
_ => Value::Number(-1.0),
|
|
1442
|
+
},
|
|
1443
|
+
Err(_) => Value::Number(-1.0),
|
|
1444
|
+
},
|
|
1445
|
+
_ => Value::Number(-1.0),
|
|
1446
|
+
}
|
|
1447
|
+
}
|