@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.
Files changed (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. 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.to_display_string().into())
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(Arc::clone(k)))
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(Arc::clone(k)), v.clone()])))
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.trim().parse().unwrap_or(f64::NAN))
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::clone(s),
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());