@tishlang/tish-format 1.0.13 → 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 +2 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +10 -2
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -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 +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- 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 +1 -1
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -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
- package/README.md +0 -138
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
//! `Date` — real constructor + instance methods for the non-JS targets (interpreter, VM, native).
|
|
2
|
+
//!
|
|
3
|
+
//! Representation is runtime-agnostic: a `Date` instance is a plain `Value::Object` whose methods
|
|
4
|
+
//! are per-instance `Value::native` closures that all capture the SAME `VmRef<f64>` epoch-millis
|
|
5
|
+
//! cell. Mutators (`setTime`) write the cell; getters recompute calendar fields from it. No new
|
|
6
|
+
//! `Value` variant is needed, so the interpreter, bytecode VM and native-compiled code all share
|
|
7
|
+
//! this one implementation.
|
|
8
|
+
//!
|
|
9
|
+
//! **Timezone:** Tish's `Date` runs in **UTC** — `getTimezoneOffset()` is `0` and the local-time
|
|
10
|
+
//! getters (`getFullYear`, `getHours`, …) are exact aliases of their `getUTC*` counterparts. There
|
|
11
|
+
//! is no timezone database in the core builtins (keeps them lean + wasm-friendly). `getTime`,
|
|
12
|
+
//! `valueOf`, the `getUTC*` family and `toISOString` are therefore fully deterministic everywhere.
|
|
13
|
+
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
16
|
+
|
|
17
|
+
const CONSTRUCT: &str = "__construct";
|
|
18
|
+
const MS_PER_DAY: i64 = 86_400_000;
|
|
19
|
+
|
|
20
|
+
const WEEKDAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
21
|
+
const MONTHS: [&str; 12] = [
|
|
22
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/// Current wall-clock time as epoch milliseconds.
|
|
26
|
+
fn now_ms() -> f64 {
|
|
27
|
+
std::time::SystemTime::now()
|
|
28
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
29
|
+
.map(|d| d.as_millis() as f64)
|
|
30
|
+
.unwrap_or(0.0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A `(method name, field extractor)` pair for the numeric `Date` getters. The explicit
|
|
34
|
+
/// `fn(&Civil) -> i64` coerces the (non-capturing) extractor closures to a single fn-pointer type so
|
|
35
|
+
/// they can live in one array.
|
|
36
|
+
type DateGetter = (&'static str, fn(&Civil) -> i64);
|
|
37
|
+
|
|
38
|
+
/// Broken-down UTC calendar fields for an epoch-millis instant.
|
|
39
|
+
struct Civil {
|
|
40
|
+
year: i64,
|
|
41
|
+
/// 1..=12 (callers convert to JS's 0-based month where needed).
|
|
42
|
+
month: i64,
|
|
43
|
+
day: i64,
|
|
44
|
+
hours: i64,
|
|
45
|
+
minutes: i64,
|
|
46
|
+
seconds: i64,
|
|
47
|
+
millis: i64,
|
|
48
|
+
/// 0 = Sunday … 6 = Saturday.
|
|
49
|
+
weekday: i64,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Days since 1970-01-01 for a proleptic-Gregorian (y, m∈1..=12, d) — Howard Hinnant's algorithm.
|
|
53
|
+
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
|
|
54
|
+
let y = if m <= 2 { y - 1 } else { y };
|
|
55
|
+
let era = if y >= 0 { y } else { y - 399 } / 400;
|
|
56
|
+
let yoe = y - era * 400; // [0, 399]
|
|
57
|
+
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; // [0, 365]
|
|
58
|
+
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
|
|
59
|
+
era * 146097 + doe - 719468
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Inverse of [`days_from_civil`]: days-since-epoch → (year, month∈1..=12, day) — Hinnant.
|
|
63
|
+
fn civil_from_days(z: i64) -> (i64, i64, i64) {
|
|
64
|
+
let z = z + 719468;
|
|
65
|
+
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
|
66
|
+
let doe = z - era * 146097; // [0, 146096]
|
|
67
|
+
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
|
|
68
|
+
let y = yoe + era * 400;
|
|
69
|
+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
|
|
70
|
+
let mp = (5 * doy + 2) / 153; // [0, 11]
|
|
71
|
+
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
|
72
|
+
let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
|
|
73
|
+
(if m <= 2 { y + 1 } else { y }, m, d)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Decompose epoch milliseconds into UTC calendar fields. Uses Euclidean div/rem so pre-1970
|
|
77
|
+
/// (negative) instants decompose correctly.
|
|
78
|
+
fn civil_from_ms(ms: f64) -> Civil {
|
|
79
|
+
let ms_i = ms.floor() as i64;
|
|
80
|
+
let days = ms_i.div_euclid(MS_PER_DAY);
|
|
81
|
+
let tod = ms_i.rem_euclid(MS_PER_DAY); // [0, 86_399_999]
|
|
82
|
+
let (year, month, day) = civil_from_days(days);
|
|
83
|
+
// 1970-01-01 was a Thursday (weekday index 4).
|
|
84
|
+
let weekday = (days.rem_euclid(7) + 4).rem_euclid(7);
|
|
85
|
+
Civil {
|
|
86
|
+
year,
|
|
87
|
+
month,
|
|
88
|
+
day,
|
|
89
|
+
hours: tod / 3_600_000,
|
|
90
|
+
minutes: (tod / 60_000) % 60,
|
|
91
|
+
seconds: (tod / 1000) % 60,
|
|
92
|
+
millis: tod % 1000,
|
|
93
|
+
weekday,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Build epoch millis from UTC components (month is 0-based, JS-style).
|
|
98
|
+
#[allow(clippy::too_many_arguments)]
|
|
99
|
+
fn ms_from_utc(
|
|
100
|
+
year: i64,
|
|
101
|
+
month0: i64,
|
|
102
|
+
day: i64,
|
|
103
|
+
hours: i64,
|
|
104
|
+
minutes: i64,
|
|
105
|
+
seconds: i64,
|
|
106
|
+
millis: i64,
|
|
107
|
+
) -> f64 {
|
|
108
|
+
// Normalize the 0-based month into a year/month carry so `Date.UTC(2020, 13, 1)` works.
|
|
109
|
+
let y = year + month0.div_euclid(12);
|
|
110
|
+
let m = month0.rem_euclid(12) + 1; // 1..=12
|
|
111
|
+
let days = days_from_civil(y, m, day);
|
|
112
|
+
(days * MS_PER_DAY
|
|
113
|
+
+ hours * 3_600_000
|
|
114
|
+
+ minutes * 60_000
|
|
115
|
+
+ seconds * 1000
|
|
116
|
+
+ millis) as f64
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// ISO-8601 string for an epoch-millis instant (always UTC, `…Z`). `None` when the instant is NaN
|
|
120
|
+
/// (JS would throw `RangeError` from `toISOString` on an invalid date).
|
|
121
|
+
fn to_iso(ms: f64) -> Option<String> {
|
|
122
|
+
if !ms.is_finite() {
|
|
123
|
+
return None;
|
|
124
|
+
}
|
|
125
|
+
let c = civil_from_ms(ms);
|
|
126
|
+
Some(if (0..=9999).contains(&c.year) {
|
|
127
|
+
format!(
|
|
128
|
+
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
|
|
129
|
+
c.year, c.month, c.day, c.hours, c.minutes, c.seconds, c.millis
|
|
130
|
+
)
|
|
131
|
+
} else {
|
|
132
|
+
// Expanded-year form (JS uses ±YYYYYY for years outside 0..=9999).
|
|
133
|
+
format!(
|
|
134
|
+
"{}{:06}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
|
|
135
|
+
if c.year < 0 { '-' } else { '+' },
|
|
136
|
+
c.year.abs(),
|
|
137
|
+
c.month,
|
|
138
|
+
c.day,
|
|
139
|
+
c.hours,
|
|
140
|
+
c.minutes,
|
|
141
|
+
c.seconds,
|
|
142
|
+
c.millis
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Human form, e.g. `Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time)`.
|
|
148
|
+
fn to_string_full(ms: f64) -> String {
|
|
149
|
+
if !ms.is_finite() {
|
|
150
|
+
return "Invalid Date".to_string();
|
|
151
|
+
}
|
|
152
|
+
let c = civil_from_ms(ms);
|
|
153
|
+
format!(
|
|
154
|
+
"{} {} {:02} {:04} {:02}:{:02}:{:02} GMT+0000 (Coordinated Universal Time)",
|
|
155
|
+
WEEKDAYS[c.weekday as usize],
|
|
156
|
+
MONTHS[(c.month - 1) as usize],
|
|
157
|
+
c.day,
|
|
158
|
+
c.year,
|
|
159
|
+
c.hours,
|
|
160
|
+
c.minutes,
|
|
161
|
+
c.seconds
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn to_date_string(ms: f64) -> String {
|
|
166
|
+
if !ms.is_finite() {
|
|
167
|
+
return "Invalid Date".to_string();
|
|
168
|
+
}
|
|
169
|
+
let c = civil_from_ms(ms);
|
|
170
|
+
format!(
|
|
171
|
+
"{} {} {:02} {:04}",
|
|
172
|
+
WEEKDAYS[c.weekday as usize],
|
|
173
|
+
MONTHS[(c.month - 1) as usize],
|
|
174
|
+
c.day,
|
|
175
|
+
c.year
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Parse a subset of the formats `Date.parse` accepts: ISO-8601 date (`YYYY-MM-DD`) and date-time
|
|
180
|
+
/// (`YYYY-MM-DDTHH:MM[:SS[.sss]]`) with an optional `Z` or `±HH:MM` offset. No offset ⇒ UTC
|
|
181
|
+
/// (Tish runs Dates in UTC). Returns NaN on anything it cannot parse.
|
|
182
|
+
fn parse_date(s: &str) -> f64 {
|
|
183
|
+
let s = s.trim();
|
|
184
|
+
// Split date and (optional) time on 'T' or a space.
|
|
185
|
+
let (date_part, time_part) = match s.find(['T', ' ']) {
|
|
186
|
+
Some(i) => (&s[..i], Some(&s[i + 1..])),
|
|
187
|
+
None => (s, None),
|
|
188
|
+
};
|
|
189
|
+
let mut dit = date_part.split('-');
|
|
190
|
+
// Leading '-' (negative year) is not supported here; the common cases are positive years.
|
|
191
|
+
let year: i64 = match dit.next().and_then(|x| x.parse().ok()) {
|
|
192
|
+
Some(y) => y,
|
|
193
|
+
None => return f64::NAN,
|
|
194
|
+
};
|
|
195
|
+
let month: i64 = dit.next().and_then(|x| x.parse().ok()).unwrap_or(1);
|
|
196
|
+
let day: i64 = dit.next().and_then(|x| x.parse().ok()).unwrap_or(1);
|
|
197
|
+
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
|
|
198
|
+
return f64::NAN;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let (mut hh, mut mm, mut ss, mut ms, mut offset_min) = (0i64, 0i64, 0i64, 0i64, 0i64);
|
|
202
|
+
if let Some(tp) = time_part {
|
|
203
|
+
let tp = tp.trim();
|
|
204
|
+
// Strip a trailing timezone designator.
|
|
205
|
+
let (clock, tz): (&str, Option<&str>) = if let Some(stripped) = tp.strip_suffix('Z') {
|
|
206
|
+
(stripped, Some("Z"))
|
|
207
|
+
} else if let Some(pos) = tp.rfind(['+', '-']) {
|
|
208
|
+
(&tp[..pos], Some(&tp[pos..]))
|
|
209
|
+
} else {
|
|
210
|
+
(tp, None)
|
|
211
|
+
};
|
|
212
|
+
let mut cit = clock.split(':');
|
|
213
|
+
hh = cit.next().and_then(|x| x.parse().ok()).unwrap_or(0);
|
|
214
|
+
mm = cit.next().and_then(|x| x.parse().ok()).unwrap_or(0);
|
|
215
|
+
if let Some(sec) = cit.next() {
|
|
216
|
+
let mut sp = sec.split('.');
|
|
217
|
+
ss = sp.next().and_then(|x| x.parse().ok()).unwrap_or(0);
|
|
218
|
+
if let Some(frac) = sp.next() {
|
|
219
|
+
// milliseconds = first 3 fractional digits, right-padded.
|
|
220
|
+
let mut f = frac.to_string();
|
|
221
|
+
f.truncate(3);
|
|
222
|
+
while f.len() < 3 {
|
|
223
|
+
f.push('0');
|
|
224
|
+
}
|
|
225
|
+
ms = f.parse().unwrap_or(0);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if let Some(tz) = tz {
|
|
229
|
+
if tz != "Z" && tz.len() >= 3 {
|
|
230
|
+
let sign = if tz.starts_with('-') { -1 } else { 1 };
|
|
231
|
+
let body = &tz[1..];
|
|
232
|
+
let mut oit = body.split(':');
|
|
233
|
+
let oh: i64 = oit.next().and_then(|x| x.parse().ok()).unwrap_or(0);
|
|
234
|
+
let om: i64 = oit.next().and_then(|x| x.parse().ok()).unwrap_or(0);
|
|
235
|
+
offset_min = sign * (oh * 60 + om);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if !(0..=23).contains(&hh) || !(0..=59).contains(&mm) || !(0..=60).contains(&ss) {
|
|
240
|
+
return f64::NAN;
|
|
241
|
+
}
|
|
242
|
+
ms_from_utc(year, month - 1, day, hh, mm, ss, ms) - (offset_min * 60_000) as f64
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// `args[i]` as f64, defaulting to `dflt` when absent.
|
|
246
|
+
fn arg_num(args: &[Value], i: usize, dflt: f64) -> f64 {
|
|
247
|
+
args.get(i).and_then(Value::as_number).unwrap_or(dflt)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// A numeric getter over the instance's current epoch-millis (NaN-safe).
|
|
251
|
+
fn num_getter(store: &VmRef<f64>, f: fn(&Civil) -> i64) -> Value {
|
|
252
|
+
let s = store.clone();
|
|
253
|
+
Value::native(move |_args: &[Value]| {
|
|
254
|
+
let ms = *s.borrow();
|
|
255
|
+
if ms.is_nan() {
|
|
256
|
+
Value::Number(f64::NAN)
|
|
257
|
+
} else {
|
|
258
|
+
Value::Number(f(&civil_from_ms(ms)) as f64)
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// A string getter over the instance's current epoch-millis.
|
|
264
|
+
fn str_getter(store: &VmRef<f64>, f: fn(f64) -> String) -> Value {
|
|
265
|
+
let s = store.clone();
|
|
266
|
+
Value::native(move |_args: &[Value]| Value::String(f(*s.borrow()).into()))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Construct a `Date` instance object backing onto a shared `VmRef<f64>` epoch-millis cell.
|
|
270
|
+
pub fn date_instance(ms: f64) -> Value {
|
|
271
|
+
let store: VmRef<f64> = VmRef::new(ms);
|
|
272
|
+
let mut m = ObjectMap::default();
|
|
273
|
+
|
|
274
|
+
// Identity / numeric value.
|
|
275
|
+
{
|
|
276
|
+
let s = store.clone();
|
|
277
|
+
m.insert(
|
|
278
|
+
Arc::from("getTime"),
|
|
279
|
+
Value::native(move |_| Value::Number(*s.borrow())),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
{
|
|
283
|
+
let s = store.clone();
|
|
284
|
+
m.insert(
|
|
285
|
+
Arc::from("valueOf"),
|
|
286
|
+
Value::native(move |_| Value::Number(*s.borrow())),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
{
|
|
290
|
+
let s = store.clone();
|
|
291
|
+
m.insert(
|
|
292
|
+
Arc::from("setTime"),
|
|
293
|
+
Value::native(move |args: &[Value]| {
|
|
294
|
+
let ms = arg_num(args, 0, f64::NAN);
|
|
295
|
+
*s.borrow_mut() = ms;
|
|
296
|
+
Value::Number(ms)
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// UTC field getters (the canonical, deterministic family).
|
|
302
|
+
let utc: [DateGetter; 8] = [
|
|
303
|
+
("getUTCFullYear", |c| c.year),
|
|
304
|
+
("getUTCMonth", |c| c.month - 1), // JS months are 0-based
|
|
305
|
+
("getUTCDate", |c| c.day),
|
|
306
|
+
("getUTCDay", |c| c.weekday),
|
|
307
|
+
("getUTCHours", |c| c.hours),
|
|
308
|
+
("getUTCMinutes", |c| c.minutes),
|
|
309
|
+
("getUTCSeconds", |c| c.seconds),
|
|
310
|
+
("getUTCMilliseconds", |c| c.millis),
|
|
311
|
+
];
|
|
312
|
+
for (name, f) in utc {
|
|
313
|
+
m.insert(Arc::from(name), num_getter(&store, f));
|
|
314
|
+
}
|
|
315
|
+
// Local-time getters: Tish runs Dates in UTC, so these alias the UTC family.
|
|
316
|
+
let local: [DateGetter; 8] = [
|
|
317
|
+
("getFullYear", |c| c.year),
|
|
318
|
+
("getMonth", |c| c.month - 1),
|
|
319
|
+
("getDate", |c| c.day),
|
|
320
|
+
("getDay", |c| c.weekday),
|
|
321
|
+
("getHours", |c| c.hours),
|
|
322
|
+
("getMinutes", |c| c.minutes),
|
|
323
|
+
("getSeconds", |c| c.seconds),
|
|
324
|
+
("getMilliseconds", |c| c.millis),
|
|
325
|
+
];
|
|
326
|
+
for (name, f) in local {
|
|
327
|
+
m.insert(Arc::from(name), num_getter(&store, f));
|
|
328
|
+
}
|
|
329
|
+
m.insert(
|
|
330
|
+
Arc::from("getTimezoneOffset"),
|
|
331
|
+
Value::native(|_| Value::Number(0.0)),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// String renderings.
|
|
335
|
+
{
|
|
336
|
+
let s = store.clone();
|
|
337
|
+
m.insert(
|
|
338
|
+
Arc::from("toISOString"),
|
|
339
|
+
Value::native(move |_| match to_iso(*s.borrow()) {
|
|
340
|
+
Some(iso) => Value::String(iso.into()),
|
|
341
|
+
None => Value::Null,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
{
|
|
346
|
+
let s = store.clone();
|
|
347
|
+
m.insert(
|
|
348
|
+
Arc::from("toJSON"),
|
|
349
|
+
Value::native(move |_| match to_iso(*s.borrow()) {
|
|
350
|
+
Some(iso) => Value::String(iso.into()),
|
|
351
|
+
None => Value::Null,
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
m.insert(Arc::from("toString"), str_getter(&store, to_string_full));
|
|
356
|
+
m.insert(
|
|
357
|
+
Arc::from("toUTCString"),
|
|
358
|
+
str_getter(&store, |ms| {
|
|
359
|
+
if !ms.is_finite() {
|
|
360
|
+
return "Invalid Date".to_string();
|
|
361
|
+
}
|
|
362
|
+
let c = civil_from_ms(ms);
|
|
363
|
+
format!(
|
|
364
|
+
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
|
|
365
|
+
WEEKDAYS[c.weekday as usize],
|
|
366
|
+
c.day,
|
|
367
|
+
MONTHS[(c.month - 1) as usize],
|
|
368
|
+
c.year,
|
|
369
|
+
c.hours,
|
|
370
|
+
c.minutes,
|
|
371
|
+
c.seconds
|
|
372
|
+
)
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
m.insert(Arc::from("toDateString"), str_getter(&store, to_date_string));
|
|
376
|
+
m.insert(
|
|
377
|
+
Arc::from("toTimeString"),
|
|
378
|
+
str_getter(&store, |ms| {
|
|
379
|
+
if !ms.is_finite() {
|
|
380
|
+
return "Invalid Date".to_string();
|
|
381
|
+
}
|
|
382
|
+
let c = civil_from_ms(ms);
|
|
383
|
+
format!(
|
|
384
|
+
"{:02}:{:02}:{:02} GMT+0000 (Coordinated Universal Time)",
|
|
385
|
+
c.hours, c.minutes, c.seconds
|
|
386
|
+
)
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
// Locale variants map to the UTC renderings (no locale/ICU data in core builtins).
|
|
390
|
+
m.insert(
|
|
391
|
+
Arc::from("toLocaleDateString"),
|
|
392
|
+
str_getter(&store, to_date_string),
|
|
393
|
+
);
|
|
394
|
+
m.insert(
|
|
395
|
+
Arc::from("toLocaleTimeString"),
|
|
396
|
+
str_getter(&store, |ms| {
|
|
397
|
+
if !ms.is_finite() {
|
|
398
|
+
return "Invalid Date".to_string();
|
|
399
|
+
}
|
|
400
|
+
let c = civil_from_ms(ms);
|
|
401
|
+
format!("{:02}:{:02}:{:02}", c.hours, c.minutes, c.seconds)
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
m.insert(Arc::from("toLocaleString"), str_getter(&store, to_string_full));
|
|
405
|
+
|
|
406
|
+
Value::object(m)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// Interpret constructor arguments (`new Date(...)`) into an epoch-millis instant.
|
|
410
|
+
fn ms_from_args(args: &[Value]) -> f64 {
|
|
411
|
+
match args.len() {
|
|
412
|
+
0 => now_ms(),
|
|
413
|
+
1 => match &args[0] {
|
|
414
|
+
Value::Number(n) => *n,
|
|
415
|
+
Value::String(s) => parse_date(s),
|
|
416
|
+
other => other.as_number().unwrap_or(f64::NAN),
|
|
417
|
+
},
|
|
418
|
+
_ => {
|
|
419
|
+
// (year, month0, day=1, hours=0, minutes=0, seconds=0, ms=0) — UTC semantics.
|
|
420
|
+
let year = arg_num(args, 0, f64::NAN);
|
|
421
|
+
if !year.is_finite() {
|
|
422
|
+
return f64::NAN;
|
|
423
|
+
}
|
|
424
|
+
ms_from_utc(
|
|
425
|
+
year as i64,
|
|
426
|
+
arg_num(args, 1, 0.0) as i64,
|
|
427
|
+
arg_num(args, 2, 1.0) as i64,
|
|
428
|
+
arg_num(args, 3, 0.0) as i64,
|
|
429
|
+
arg_num(args, 4, 0.0) as i64,
|
|
430
|
+
arg_num(args, 5, 0.0) as i64,
|
|
431
|
+
arg_num(args, 6, 0.0) as i64,
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// The global `Date`: callable as a constructor (`new Date(...)`) and carrying the statics
|
|
438
|
+
/// `Date.now()`, `Date.parse(str)` and `Date.UTC(...)`. Backwards-compatible with the previous
|
|
439
|
+
/// `Date.now()`-only object.
|
|
440
|
+
pub fn date_constructor_value() -> Value {
|
|
441
|
+
let mut m = ObjectMap::default();
|
|
442
|
+
m.insert(
|
|
443
|
+
Arc::from(CONSTRUCT),
|
|
444
|
+
Value::native(|args: &[Value]| date_instance(ms_from_args(args))),
|
|
445
|
+
);
|
|
446
|
+
m.insert(
|
|
447
|
+
Arc::from("now"),
|
|
448
|
+
Value::native(|_| Value::Number(now_ms())),
|
|
449
|
+
);
|
|
450
|
+
m.insert(
|
|
451
|
+
Arc::from("parse"),
|
|
452
|
+
Value::native(|args: &[Value]| {
|
|
453
|
+
let ms = match args.first() {
|
|
454
|
+
Some(Value::String(s)) => parse_date(s),
|
|
455
|
+
Some(v) => v.as_number().unwrap_or(f64::NAN),
|
|
456
|
+
None => f64::NAN,
|
|
457
|
+
};
|
|
458
|
+
Value::Number(ms)
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
m.insert(
|
|
462
|
+
Arc::from("UTC"),
|
|
463
|
+
Value::native(|args: &[Value]| {
|
|
464
|
+
if args.is_empty() {
|
|
465
|
+
return Value::Number(f64::NAN);
|
|
466
|
+
}
|
|
467
|
+
let year = arg_num(args, 0, f64::NAN);
|
|
468
|
+
if !year.is_finite() {
|
|
469
|
+
return Value::Number(f64::NAN);
|
|
470
|
+
}
|
|
471
|
+
Value::Number(ms_from_utc(
|
|
472
|
+
year as i64,
|
|
473
|
+
arg_num(args, 1, 0.0) as i64,
|
|
474
|
+
arg_num(args, 2, 1.0) as i64,
|
|
475
|
+
arg_num(args, 3, 0.0) as i64,
|
|
476
|
+
arg_num(args, 4, 0.0) as i64,
|
|
477
|
+
arg_num(args, 5, 0.0) as i64,
|
|
478
|
+
arg_num(args, 6, 0.0) as i64,
|
|
479
|
+
))
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
Value::object(m)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#[cfg(test)]
|
|
486
|
+
mod tests {
|
|
487
|
+
use super::*;
|
|
488
|
+
|
|
489
|
+
fn iso(ms: f64) -> String {
|
|
490
|
+
to_iso(ms).unwrap()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
#[test]
|
|
494
|
+
fn epoch_is_unix_zero() {
|
|
495
|
+
assert_eq!(iso(0.0), "1970-01-01T00:00:00.000Z");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[test]
|
|
499
|
+
fn known_instant_roundtrips() {
|
|
500
|
+
// 2021-06-09T12:34:56.789Z
|
|
501
|
+
let ms = ms_from_utc(2021, 5, 9, 12, 34, 56, 789);
|
|
502
|
+
assert_eq!(iso(ms), "2021-06-09T12:34:56.789Z");
|
|
503
|
+
let c = civil_from_ms(ms);
|
|
504
|
+
assert_eq!((c.year, c.month, c.day), (2021, 6, 9));
|
|
505
|
+
assert_eq!((c.hours, c.minutes, c.seconds, c.millis), (12, 34, 56, 789));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
#[test]
|
|
509
|
+
fn weekday_anchor() {
|
|
510
|
+
// 1970-01-01 = Thursday(4); 2021-06-09 = Wednesday(3).
|
|
511
|
+
assert_eq!(civil_from_ms(0.0).weekday, 4);
|
|
512
|
+
assert_eq!(civil_from_ms(ms_from_utc(2021, 5, 9, 0, 0, 0, 0)).weekday, 3);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[test]
|
|
516
|
+
fn pre_epoch_negative() {
|
|
517
|
+
// 1969-12-31T00:00:00Z = -86_400_000 ms, a Wednesday(3).
|
|
518
|
+
let ms = ms_from_utc(1969, 11, 31, 0, 0, 0, 0);
|
|
519
|
+
assert_eq!(ms, -86_400_000.0);
|
|
520
|
+
assert_eq!(iso(ms), "1969-12-31T00:00:00.000Z");
|
|
521
|
+
assert_eq!(civil_from_ms(ms).weekday, 3);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn parse_iso_forms() {
|
|
526
|
+
assert_eq!(parse_date("1970-01-01"), 0.0);
|
|
527
|
+
assert_eq!(parse_date("1970-01-01T00:00:00.000Z"), 0.0);
|
|
528
|
+
assert_eq!(parse_date("2021-06-09T12:34:56.789Z"), ms_from_utc(2021, 5, 9, 12, 34, 56, 789));
|
|
529
|
+
// +01:00 offset pulls the UTC instant back one hour.
|
|
530
|
+
assert_eq!(parse_date("1970-01-01T01:00:00+01:00"), 0.0);
|
|
531
|
+
assert!(parse_date("not a date").is_nan());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#[test]
|
|
535
|
+
fn leap_day() {
|
|
536
|
+
assert_eq!(iso(ms_from_utc(2020, 1, 29, 0, 0, 0, 0)), "2020-02-29T00:00:00.000Z");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
@@ -48,13 +48,62 @@ pub fn is_nan(args: &[Value]) -> Value {
|
|
|
48
48
|
|
|
49
49
|
/// Array.isArray(value)
|
|
50
50
|
pub fn array_is_array(args: &[Value]) -> Value {
|
|
51
|
-
Value::Bool(matches!(args.first(), Some(Value::Array(_))))
|
|
51
|
+
Value::Bool(matches!(args.first(), Some(Value::Array(_)) | Some(Value::NumberArray(_))))
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/// String(value) — convert value to string (JS String constructor as function).
|
|
55
|
+
/// Uses JS `ToString` (arrays comma-join recursively, objects → "[object Object]"),
|
|
56
|
+
/// not the inspect/display form.
|
|
55
57
|
pub fn string_convert(args: &[Value]) -> Value {
|
|
56
58
|
let v = args.first().unwrap_or(&Value::Null);
|
|
57
|
-
Value::String(v.
|
|
59
|
+
Value::String(v.to_js_string().into())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// JS `Number(value)` coercion (ToNumber), issue #36. Numbers pass through; booleans →
|
|
63
|
+
/// 1/0; null → 0; strings parse (trimmed, with `0x`/`0b`/`0o` and `Infinity`, `""` → 0,
|
|
64
|
+
/// otherwise NaN); arrays/objects go via their string form (so `Number([5])` → 5,
|
|
65
|
+
/// `Number([])` → 0, objects → NaN).
|
|
66
|
+
pub fn number_convert(args: &[Value]) -> Value {
|
|
67
|
+
let v = args.first().unwrap_or(&Value::Null);
|
|
68
|
+
let n = match v {
|
|
69
|
+
Value::Number(n) => *n,
|
|
70
|
+
Value::Bool(b) => {
|
|
71
|
+
if *b {
|
|
72
|
+
1.0
|
|
73
|
+
} else {
|
|
74
|
+
0.0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
Value::Null => 0.0,
|
|
78
|
+
Value::String(s) => parse_numeric_string(s),
|
|
79
|
+
other => parse_numeric_string(&other.to_js_string()),
|
|
80
|
+
};
|
|
81
|
+
Value::Number(n)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Parse a string as JS `Number` does: trimmed; `""` → 0; `0x`/`0o`/`0b` radix prefixes;
|
|
85
|
+
/// `Infinity`/`-Infinity`; plain decimal/float; anything else → NaN. Public so the
|
|
86
|
+
/// tree-walk interpreter (distinct `Value` type) shares the exact coercion.
|
|
87
|
+
pub fn parse_numeric_string(s: &str) -> f64 {
|
|
88
|
+
let t = s.trim();
|
|
89
|
+
if t.is_empty() {
|
|
90
|
+
return 0.0;
|
|
91
|
+
}
|
|
92
|
+
let radix = |rest: &str, r: u32| i64::from_str_radix(rest, r).map(|x| x as f64).unwrap_or(f64::NAN);
|
|
93
|
+
if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
|
|
94
|
+
return radix(rest, 16);
|
|
95
|
+
}
|
|
96
|
+
if let Some(rest) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
|
|
97
|
+
return radix(rest, 8);
|
|
98
|
+
}
|
|
99
|
+
if let Some(rest) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
|
|
100
|
+
return radix(rest, 2);
|
|
101
|
+
}
|
|
102
|
+
match t {
|
|
103
|
+
"Infinity" | "+Infinity" => f64::INFINITY,
|
|
104
|
+
"-Infinity" => f64::NEG_INFINITY,
|
|
105
|
+
_ => t.parse::<f64>().unwrap_or(f64::NAN),
|
|
106
|
+
}
|
|
58
107
|
}
|
|
59
108
|
|
|
60
109
|
/// String.fromCharCode(...codes)
|
|
@@ -76,7 +125,7 @@ pub fn object_keys(args: &[Value]) -> Value {
|
|
|
76
125
|
let keys: Vec<Value> = obj_borrow
|
|
77
126
|
.strings
|
|
78
127
|
.keys()
|
|
79
|
-
.map(|k| Value::String(
|
|
128
|
+
.map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
|
|
80
129
|
.collect();
|
|
81
130
|
Value::Array(VmRef::new(keys))
|
|
82
131
|
} else {
|
|
@@ -102,7 +151,7 @@ pub fn object_entries(args: &[Value]) -> Value {
|
|
|
102
151
|
let entries: Vec<Value> = obj_borrow
|
|
103
152
|
.strings
|
|
104
153
|
.iter()
|
|
105
|
-
.map(|(k, v)| Value::Array(VmRef::new(vec![Value::String(
|
|
154
|
+
.map(|(k, v)| Value::Array(VmRef::new(vec![Value::String(tishlang_core::ArcStr::from(k.as_ref())), v.clone()])))
|
|
106
155
|
.collect();
|
|
107
156
|
Value::Array(VmRef::new(entries))
|
|
108
157
|
} else {
|
|
@@ -184,7 +233,38 @@ pub fn parse_float(args: &[Value]) -> Value {
|
|
|
184
233
|
.first()
|
|
185
234
|
.map(Value::to_display_string)
|
|
186
235
|
.unwrap_or_default();
|
|
187
|
-
Value::Number(s
|
|
236
|
+
Value::Number(js_parse_float(&s))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// JS `parseFloat`: skips leading whitespace, then parses the **longest leading prefix**
|
|
240
|
+
/// that's a valid float (so `parseFloat("3.14abc")` → 3.14, `parseFloat("12.3.4")` → 12.3).
|
|
241
|
+
/// Handles `Infinity`/`-Infinity`; returns NaN when no numeric prefix is present. Issue #36.
|
|
242
|
+
/// Public so the tree-walk interpreter (distinct `Value`) shares the exact behavior.
|
|
243
|
+
pub fn js_parse_float(s: &str) -> f64 {
|
|
244
|
+
let t = s.trim_start();
|
|
245
|
+
if t.starts_with("Infinity") || t.starts_with("+Infinity") {
|
|
246
|
+
return f64::INFINITY;
|
|
247
|
+
}
|
|
248
|
+
if t.starts_with("-Infinity") {
|
|
249
|
+
return f64::NEG_INFINITY;
|
|
250
|
+
}
|
|
251
|
+
// Take a generous run of float-shaped chars, then shrink from the right until it parses.
|
|
252
|
+
let mut end = 0;
|
|
253
|
+
for (i, c) in t.char_indices() {
|
|
254
|
+
if c.is_ascii_digit() || matches!(c, '.' | '+' | '-' | 'e' | 'E') {
|
|
255
|
+
end = i + c.len_utf8();
|
|
256
|
+
} else {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
let mut slice = &t[..end];
|
|
261
|
+
while !slice.is_empty() {
|
|
262
|
+
if let Ok(n) = slice.parse::<f64>() {
|
|
263
|
+
return n;
|
|
264
|
+
}
|
|
265
|
+
slice = &slice[..slice.len() - 1];
|
|
266
|
+
}
|
|
267
|
+
f64::NAN
|
|
188
268
|
}
|
|
189
269
|
|
|
190
270
|
/// Object.fromEntries(entries)
|
|
@@ -198,7 +278,7 @@ pub fn object_from_entries(args: &[Value]) -> Value {
|
|
|
198
278
|
let pair_borrow = pair.borrow();
|
|
199
279
|
if pair_borrow.len() >= 2 {
|
|
200
280
|
let key: Arc<str> = match &pair_borrow[0] {
|
|
201
|
-
Value::String(s) => Arc::
|
|
281
|
+
Value::String(s) => Arc::from(s.as_str()),
|
|
202
282
|
v => v.to_display_string().into(),
|
|
203
283
|
};
|
|
204
284
|
obj.insert(key, pair_borrow[1].clone());
|