1use sha2::{Digest, Sha256};
9use std::sync::Arc;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Typed {
14 Uint8,
15 Uint8Clamped,
16 Int16,
17 Int32,
18 Uint32,
19 Float32,
20}
21
22impl Typed {
23 pub fn name(self) -> &'static str {
24 match self {
25 Typed::Uint8 => "Uint8Array",
26 Typed::Uint8Clamped => "Uint8ClampedArray",
27 Typed::Int16 => "Int16Array",
28 Typed::Int32 => "Int32Array",
29 Typed::Uint32 => "Uint32Array",
30 Typed::Float32 => "Float32Array",
31 }
32 }
33}
34
35#[derive(Clone, Debug, PartialEq)]
37pub enum Js {
38 Undefined,
40 Null,
41 Bool(bool),
42 Num(f64),
43 Str(String),
44 Arr(Vec<Js>),
45 Obj(Obj),
46 Typed(Typed, Arc<Vec<u8>>),
48 Blob(String, Arc<Vec<u8>>),
50}
51
52#[derive(Clone, Debug, Default, PartialEq)]
54pub struct Obj(pub Vec<(String, Js)>);
55
56impl Obj {
57 pub fn new() -> Obj {
58 Obj(Vec::new())
59 }
60
61 pub fn set(&mut self, key: &str, value: impl Into<Js>) -> &mut Obj {
63 let value = value.into();
64 match self.0.iter_mut().find(|(k, _)| k == key) {
65 Some((_, v)) => *v = value,
66 None => self.0.push((key.to_string(), value)),
67 }
68 self
69 }
70
71 pub fn with(mut self, key: &str, value: impl Into<Js>) -> Obj {
73 self.set(key, value);
74 self
75 }
76
77 pub fn with_opt<T: Into<Js>>(mut self, key: &str, value: Option<T>) -> Obj {
79 if let Some(v) = value {
80 self.set(key, v);
81 }
82 self
83 }
84
85 pub fn get(&self, key: &str) -> Option<&Js> {
86 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
87 }
88
89 pub fn get_mut(&mut self, key: &str) -> Option<&mut Js> {
90 self.0.iter_mut().find(|(k, _)| k == key).map(|(_, v)| v)
91 }
92
93 pub fn remove(&mut self, key: &str) -> Option<Js> {
94 let at = self.0.iter().position(|(k, _)| k == key)?;
95 Some(self.0.remove(at).1)
96 }
97
98 pub fn num(&self, key: &str) -> f64 {
99 self.get(key).map_or(f64::NAN, Js::num)
100 }
101
102 pub fn str(&self, key: &str) -> Option<&str> {
103 self.get(key).and_then(Js::as_str)
104 }
105
106 pub fn bool(&self, key: &str) -> bool {
107 matches!(self.get(key), Some(Js::Bool(true)))
108 }
109}
110
111impl Js {
112 pub fn num(&self) -> f64 {
113 match self {
114 Js::Num(n) => *n,
115 _ => f64::NAN,
116 }
117 }
118
119 pub fn as_str(&self) -> Option<&str> {
120 match self {
121 Js::Str(s) => Some(s),
122 _ => None,
123 }
124 }
125
126 pub fn as_obj(&self) -> Option<&Obj> {
127 match self {
128 Js::Obj(o) => Some(o),
129 _ => None,
130 }
131 }
132
133 pub fn as_obj_mut(&mut self) -> Option<&mut Obj> {
134 match self {
135 Js::Obj(o) => Some(o),
136 _ => None,
137 }
138 }
139
140 pub fn as_arr(&self) -> Option<&[Js]> {
141 match self {
142 Js::Arr(a) => Some(a),
143 _ => None,
144 }
145 }
146
147 pub fn bytes(&self) -> Option<&Arc<Vec<u8>>> {
149 match self {
150 Js::Typed(_, b) | Js::Blob(_, b) => Some(b),
151 _ => None,
152 }
153 }
154
155 pub fn typed(kind: Typed, bytes: Vec<u8>) -> Js {
156 Js::Typed(kind, Arc::new(bytes))
157 }
158
159 pub fn u8s(bytes: Vec<u8>) -> Js {
160 Js::typed(Typed::Uint8, bytes)
161 }
162
163 pub fn u32s(values: &[u32]) -> Js {
164 Js::typed(
165 Typed::Uint32,
166 values.iter().flat_map(|v| v.to_le_bytes()).collect(),
167 )
168 }
169
170 pub fn f32s(values: &[f32]) -> Js {
171 Js::typed(
172 Typed::Float32,
173 values.iter().flat_map(|v| v.to_le_bytes()).collect(),
174 )
175 }
176
177 pub fn png(bytes: Vec<u8>) -> Js {
178 Js::Blob("image/png".into(), Arc::new(bytes))
179 }
180
181 pub fn from_json(value: &serde_json::Value) -> Js {
183 match value {
184 serde_json::Value::Null => Js::Null,
185 serde_json::Value::Bool(b) => Js::Bool(*b),
186 serde_json::Value::Number(n) => Js::Num(n.as_f64().unwrap_or(f64::NAN)),
187 serde_json::Value::String(s) => Js::Str(s.clone()),
188 serde_json::Value::Array(a) => Js::Arr(a.iter().map(Js::from_json).collect()),
189 serde_json::Value::Object(o) => Js::Obj(Obj(o
190 .iter()
191 .map(|(k, v)| (k.clone(), Js::from_json(v)))
192 .collect())),
193 }
194 }
195}
196
197impl From<f64> for Js {
198 fn from(v: f64) -> Js {
199 Js::Num(v)
200 }
201}
202impl From<u32> for Js {
203 fn from(v: u32) -> Js {
204 Js::Num(v as f64)
205 }
206}
207impl From<i32> for Js {
208 fn from(v: i32) -> Js {
209 Js::Num(v as f64)
210 }
211}
212impl From<u64> for Js {
213 fn from(v: u64) -> Js {
214 Js::Num(v as f64)
215 }
216}
217impl From<usize> for Js {
218 fn from(v: usize) -> Js {
219 Js::Num(v as f64)
220 }
221}
222impl From<bool> for Js {
223 fn from(v: bool) -> Js {
224 Js::Bool(v)
225 }
226}
227impl From<&str> for Js {
228 fn from(v: &str) -> Js {
229 Js::Str(v.to_string())
230 }
231}
232impl From<String> for Js {
233 fn from(v: String) -> Js {
234 Js::Str(v)
235 }
236}
237impl From<Obj> for Js {
238 fn from(v: Obj) -> Js {
239 Js::Obj(v)
240 }
241}
242impl From<Vec<Js>> for Js {
243 fn from(v: Vec<Js>) -> Js {
244 Js::Arr(v)
245 }
246}
247
248pub fn rect(r: crate::kernels::geometry::Rect) -> Js {
250 Js::Obj(
251 Obj::new()
252 .with("x", r.x)
253 .with("y", r.y)
254 .with("width", r.width)
255 .with("height", r.height),
256 )
257}
258
259pub fn read_rect(v: &Js) -> Option<crate::kernels::geometry::Rect> {
261 let o = v.as_obj()?;
262 Some(crate::kernels::geometry::Rect {
263 x: o.num("x"),
264 y: o.num("y"),
265 width: o.num("width"),
266 height: o.num("height"),
267 })
268}
269
270pub fn detail(v: &crate::engine::diagnostic::Value) -> Js {
272 use crate::engine::diagnostic::Value;
273 match v {
274 Value::Number(n) => Js::Num(*n),
275 Value::Bool(b) => Js::Bool(*b),
276 Value::Text(s) => Js::Str(s.clone()),
277 Value::List(l) => Js::Arr(l.iter().map(detail).collect()),
278 Value::Object(fields) => Js::Obj(Obj(fields
279 .iter()
280 .map(|(k, v)| (k.to_string(), detail(v)))
281 .collect())),
282 }
283}
284
285pub fn write_json(v: &Js, out: &mut String) {
289 match v {
290 Js::Undefined | Js::Null => out.push_str("null"),
291 Js::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
292 Js::Num(n) => {
293 if n.is_finite() {
294 out.push_str(&crate::engine::catalog::js_number(*n))
295 } else {
296 out.push_str("null")
297 }
298 }
299 Js::Str(s) => write_string(s, out),
300 Js::Arr(a) => {
301 out.push('[');
302 for (i, x) in a.iter().enumerate() {
303 if i > 0 {
304 out.push(',');
305 }
306 write_json(x, out);
307 }
308 out.push(']');
309 }
310 Js::Obj(o) => {
311 out.push('{');
312 let mut first = true;
313 for (k, x) in ordered(o) {
314 if matches!(x, Js::Undefined) {
315 continue;
316 }
317 if !first {
318 out.push(',');
319 }
320 first = false;
321 write_string(k, out);
322 out.push(':');
323 write_json(x, out);
324 }
325 out.push('}');
326 }
327 Js::Typed(..) | Js::Blob(..) => out.push_str("{}"),
330 }
331}
332
333fn ordered(o: &Obj) -> Vec<(&String, &Js)> {
335 let index = |k: &str| -> Option<u32> {
336 if k == "0"
337 || (!k.starts_with('0') && !k.is_empty() && k.bytes().all(|b| b.is_ascii_digit()))
338 {
339 k.parse::<u32>().ok().filter(|n| *n < u32::MAX)
340 } else {
341 None
342 }
343 };
344 let mut ints: Vec<(u32, &String, &Js)> =
345 o.0.iter()
346 .filter_map(|(k, v)| index(k).map(|n| (n, k, v)))
347 .collect();
348 ints.sort_by_key(|(n, _, _)| *n);
349 let mut out: Vec<(&String, &Js)> = ints.into_iter().map(|(_, k, v)| (k, v)).collect();
350 out.extend(
351 o.0.iter()
352 .filter(|(k, _)| index(k).is_none())
353 .map(|(k, v)| (k, v)),
354 );
355 out
356}
357
358fn write_string(s: &str, out: &mut String) {
359 out.push('"');
360 for c in s.chars() {
361 match c {
362 '"' => out.push_str("\\\""),
363 '\\' => out.push_str("\\\\"),
364 '\n' => out.push_str("\\n"),
365 '\r' => out.push_str("\\r"),
366 '\t' => out.push_str("\\t"),
367 '\u{8}' => out.push_str("\\b"),
368 '\u{c}' => out.push_str("\\f"),
369 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
370 c => out.push(c),
371 }
372 }
373 out.push('"');
374}
375
376pub fn to_json(v: &Js) -> String {
377 let mut s = String::new();
378 write_json(v, &mut s);
379 s
380}
381
382pub fn short_sha(bytes: &[u8]) -> String {
386 let d = Sha256::digest(bytes);
387 d[..12].iter().map(|b| format!("{b:02x}")).collect()
388}
389
390fn volatile(key: &str) -> bool {
391 matches!(key, "created" | "updated" | "compute" | "id")
392 || key.ends_with("MS")
393 || key == "seconds"
394}
395
396pub fn canonical(v: &Js, run: &str) -> Js {
400 match v {
401 Js::Blob(kind, bytes) => Js::Obj(
402 Obj::new()
403 .with("blob", kind.as_str())
404 .with("sha", short_sha(bytes)),
405 ),
406 Js::Typed(kind, bytes) => Js::Obj(Obj::new().with(kind.name(), short_sha(bytes))),
407 Js::Str(s) => Js::Str(if run.is_empty() {
408 s.clone()
409 } else {
410 s.replace(run, "<run>")
411 }),
412 Js::Arr(a) => Js::Arr(a.iter().map(|x| canonical(x, run)).collect()),
413 Js::Obj(o) => {
414 let host_timed = o.str("code") == Some("COMPUTE_BACKEND");
415 Js::Obj(Obj(o
416 .0
417 .iter()
418 .filter(|(k, _)| !volatile(k) && !(host_timed && (k == "message" || k == "detail")))
419 .map(|(k, x)| (k.clone(), canonical(x, run)))
420 .collect()))
421 }
422 other => other.clone(),
423 }
424}
425
426pub fn fingerprint(v: &Js, run: &str) -> String {
428 short_sha(to_json(&canonical(v, run)).as_bytes())
429}
430
431pub fn to_fixed(value: f64, digits: usize) -> String {
436 if !value.is_finite() {
437 return crate::engine::catalog::js_number(value);
438 }
439 if value.abs() >= 1e21 {
440 return crate::engine::catalog::js_number(value);
441 }
442 let negative = value < 0.0;
443 let exact = format!("{:.*}", digits + 40, value.abs());
445 let (int, frac) = exact.split_once('.').unwrap_or((&exact, ""));
446 let mut digits_vec: Vec<u8> = int.bytes().chain(frac.bytes().take(digits)).collect();
447 let rest = &frac.as_bytes()[digits.min(frac.len())..];
448 let round_up = rest.first().is_some_and(|&d| d >= b'5');
449 if round_up {
450 let mut i = digits_vec.len();
451 loop {
452 if i == 0 {
453 digits_vec.insert(0, b'1');
454 break;
455 }
456 i -= 1;
457 if digits_vec[i] == b'9' {
458 digits_vec[i] = b'0';
459 } else {
460 digits_vec[i] += 1;
461 break;
462 }
463 }
464 }
465 let s = String::from_utf8(digits_vec).unwrap();
466 let split = s.len() - digits;
467 let body = if digits == 0 {
468 s
469 } else {
470 format!("{}.{}", &s[..split], &s[split..])
471 };
472 if negative {
474 format!("-{body}")
475 } else {
476 body
477 }
478}
479
480pub fn to_locale(value: f64) -> String {
483 if !value.is_finite() {
484 return crate::engine::catalog::js_number(value);
485 }
486 let fixed = to_fixed(value, 3);
487 let (int, frac) = fixed.split_once('.').unwrap_or((&fixed, ""));
488 let (sign, int) = int.strip_prefix('-').map_or(("", int), |i| ("-", i));
489 let mut grouped = String::new();
490 for (i, c) in int.chars().enumerate() {
491 if i > 0 && (int.len() - i) % 3 == 0 {
492 grouped.push(',');
493 }
494 grouped.push(c);
495 }
496 let frac = frac.trim_end_matches('0');
497 if frac.is_empty() {
498 format!("{sign}{grouped}")
499 } else {
500 format!("{sign}{grouped}.{frac}")
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn to_fixed_rounds_the_exact_value_half_up_like_javascript() {
510 assert_eq!(to_fixed(0.125, 2), "0.13");
511 assert_eq!(to_fixed(1.005, 2), "1.00"); assert_eq!(to_fixed(2.5, 0), "3");
513 assert_eq!(to_fixed(-1.25, 1), "-1.3");
514 assert_eq!(to_fixed(-0.04, 1), "-0.0");
515 assert_eq!(to_fixed(9.996, 2), "10.00");
516 assert_eq!(to_fixed(3.0, 2), "3.00");
517 }
518
519 #[test]
520 fn to_locale_groups_thousands() {
521 assert_eq!(to_locale(1234567.0), "1,234,567");
522 assert_eq!(to_locale(999.0), "999");
523 assert_eq!(to_locale(1000.5), "1,000.5");
524 }
525
526 #[test]
527 fn json_matches_json_stringify() {
528 let v = Js::Obj(
529 Obj::new()
530 .with("b", 1.5)
531 .with("2", true)
532 .with("a", Js::Undefined)
533 .with("s", "x\"\n\u{1}")
534 .with(
535 "1",
536 Js::Arr(vec![Js::Undefined, Js::Num(f64::NAN), Js::Num(-0.0)]),
537 ),
538 );
539 assert_eq!(
540 to_json(&v),
541 r#"{"1":[null,null,0],"2":true,"b":1.5,"s":"x\"\n\u0001"}"#
542 );
543 }
544}