1use wasmtime::{ExnRef, Instance, Linker, Module, Rooted, Store, Val};
4
5use crate::runtime::{StoreData, read_submilli_string};
6
7#[derive(Debug, Clone)]
8pub struct RunResult {
9 pub value: Option<String>,
10 pub console: String,
11}
12
13pub async fn instantiate_program_async(
22 linker: &Linker<StoreData>,
23 store: &mut Store<StoreData>,
24 module: &Module,
25) -> wasmtime::Result<Instance> {
26 let outcome = linker.instantiate_async(&mut *store, module).await;
27 outcome.map_err(|err| uncaught_error(store, err))
28}
29
30pub async fn dispatch_main_async(
36 store: &mut Store<StoreData>,
37 instance: &Instance,
38) -> wasmtime::Result<Option<String>> {
39 let main = instance
40 .get_func(&mut *store, "main")
41 .ok_or_else(|| wasmtime::Error::msg("module has no `main` export"))?;
42
43 if let Some(to_output) = instance.get_func(&mut *store, "__main_output") {
46 let mut out = [Val::null_any_ref()];
47 let r = to_output.call_async(&mut *store, &[], &mut out).await;
48 r.map_err(|err| uncaught_error(store, err))?;
49 if matches!(super::host::abi_arg(&out, 0)?, Val::AnyRef(None)) {
50 return Ok(None);
51 }
52 Ok(Some(read_main_string(&mut *store, &out)?))
53 } else {
54 let r = main.call_async(&mut *store, &[], &mut []).await;
55 r.map_err(|err| uncaught_error(store, err))?;
56 Ok(None)
57 }
58}
59
60pub(crate) fn uncaught_error(
66 store: &mut Store<StoreData>,
67 err: wasmtime::Error,
68) -> wasmtime::Error {
69 if err.is::<super::host::FatalHostError>() || super::limits::is_memory_exhausted(&err) {
70 store.take_pending_exception();
73 return super::limits::name_memory_exhaustion(err);
74 }
75 let Some(exn) = store.take_pending_exception() else {
76 return err;
77 };
78 let denial = thrown_denial(store, exn);
79 let text = super::fuel::settle_result(store, |store| Ok(read_thrown_error_text(store, exn)))
82 .ok()
83 .flatten();
84 let Some(text) = text else {
85 return err;
86 };
87 let backtrace = err.downcast_ref::<wasmtime::WasmBacktrace>().cloned();
91 wasmtime::Error::new(crate::backtrace::ThrownError {
92 message: text,
93 backtrace,
94 denial,
95 })
96}
97
98fn thrown_denial(store: &mut Store<StoreData>, exn: Rooted<ExnRef>) -> Option<super::host::Denial> {
100 let Val::AnyRef(Some(thrown)) = exn.field(&mut *store, 0).ok()? else {
101 return None;
102 };
103 super::host::with_thrown_denials(store, |table, store| table.find(store, &thrown))
104}
105
106fn read_thrown_error_text(store: &mut Store<StoreData>, exn: Rooted<ExnRef>) -> Option<String> {
112 let super::host::ThrownErrorParts {
113 error,
114 name,
115 message,
116 } = super::host::thrown_error_parts(&mut *store, exn)?;
117 let header = if name.is_empty() {
118 message
119 } else {
120 format!("{name}: {message}")
121 };
122 match read_error_fields_line(store, error) {
123 Some(fields) => Some(format!("{header}\n{fields}")),
124 None => Some(header),
125 }
126}
127
128const MAX_ERROR_FIELDS: usize = 8;
129const MAX_ERROR_FIELD_CHARS: usize = 120;
130
131fn read_error_fields_line(
140 store: &mut Store<StoreData>,
141 error: Rooted<wasmtime::StructRef>,
142) -> Option<String> {
143 use crate::runtime::host::{
144 host_boxed_boolean_vtable, host_boxed_number_vtable, host_string_vtable,
145 };
146
147 let Val::AnyRef(Some(names_ref)) = error.field(&mut *store, 1).ok()? else {
148 return None;
149 };
150 let names = names_ref.unwrap_array(&mut *store).ok()?;
151 let Val::AnyRef(Some(payload_ref)) = error.field(&mut *store, 2).ok()? else {
152 return None;
153 };
154 let payload = payload_ref.unwrap_array(&mut *store).ok()?;
155
156 let string_vt = rooted_any(host_string_vtable(store).ok()?)?;
157 let number_vt = rooted_any(host_boxed_number_vtable(store).ok()?)?;
158 let boolean_vt = rooted_any(host_boxed_boolean_vtable(store).ok()?)?;
159
160 let len = names
161 .len(&mut *store)
162 .ok()?
163 .min(payload.len(&mut *store).ok()?);
164 let mut parts: Vec<String> = Vec::new();
165 let mut truncated = false;
166 for i in super::prelude::error::OWN_SLOT_BASE..len {
167 if parts.len() == MAX_ERROR_FIELDS {
168 truncated = true;
169 break;
170 }
171 let name_val = names.get(&mut *store, i).ok()?;
172 let Some(field_name) = super::host::read_string_value(&mut *store, name_val) else {
173 continue;
174 };
175 let value = payload.get(&mut *store, i).ok()?;
176 let Some(rendered) =
177 render_primitive_field(store, value, &string_vt, &number_vt, &boolean_vt)
178 else {
179 continue;
180 };
181 parts.push(format!("{field_name} = {rendered}"));
182 }
183 if parts.is_empty() {
184 return None;
185 }
186 let suffix = if truncated { ", …" } else { "" };
187 Some(format!(" fields: {}{suffix}", parts.join(", ")))
188}
189
190fn rooted_any(val: Val) -> Option<Rooted<wasmtime::AnyRef>> {
191 match val {
192 Val::AnyRef(Some(any)) => Some(any),
193 _ => None,
194 }
195}
196
197fn render_primitive_field(
200 store: &mut Store<StoreData>,
201 value: Val,
202 string_vt: &Rooted<wasmtime::AnyRef>,
203 number_vt: &Rooted<wasmtime::AnyRef>,
204 boolean_vt: &Rooted<wasmtime::AnyRef>,
205) -> Option<String> {
206 use crate::runtime::json::{boxed_bool, boxed_number, boxed_string};
207
208 let any = match value {
209 Val::AnyRef(None) => return Some("null".to_string()),
210 Val::AnyRef(Some(any)) => any,
211 _ => return None,
212 };
213 let value_struct = any.unwrap_struct(&mut *store).ok()?;
214 let undefined = crate::runtime::intrinsic_types::intrinsic_types(&mut *store)
215 .ok()?
216 .undefined
217 .clone();
218 if wasmtime::StructType::eq(&value_struct.ty(&*store).ok()?, &undefined) {
219 return Some("undefined".to_string());
220 }
221 let Val::AnyRef(Some(vt)) = value_struct.field(&mut *store, 0).ok()? else {
222 return None;
223 };
224 let boxed = Val::AnyRef(Some(any));
225 if Rooted::ref_eq(&*store, &vt, string_vt).ok()? {
226 let text = boxed_string(store, boxed).ok()?;
227 return Some(quoted_truncated(&text));
228 }
229 if Rooted::ref_eq(&*store, &vt, number_vt).ok()? {
230 let n = boxed_number(store, boxed).ok()?;
231 return Some(crate::runtime::number::format_number_js(n));
232 }
233 if Rooted::ref_eq(&*store, &vt, boolean_vt).ok()? {
234 let b = boxed_bool(store, boxed).ok()?;
235 return Some(b.to_string());
236 }
237 None
238}
239
240fn quoted_truncated(text: &str) -> String {
241 if text.chars().count() <= MAX_ERROR_FIELD_CHARS {
242 return format!("{text:?}");
243 }
244 let cut: String = text.chars().take(MAX_ERROR_FIELD_CHARS).collect();
245 format!("{:?}", format!("{cut}…"))
246}
247
248fn read_main_string(store: &mut Store<StoreData>, out: &[Val]) -> wasmtime::Result<String> {
252 let s_struct = match super::host::abi_arg(out, 0)? {
253 Val::AnyRef(Some(any)) => any.unwrap_struct(&mut *store)?,
254 Val::AnyRef(None) => {
255 return Err(wasmtime::Error::msg(
256 "main returned null where the wasm signature is non-nullable",
257 ));
258 }
259 other => wasmtime::bail!("unexpected ref-slot value: {other:?}"),
260 };
261 let raw_field = s_struct.field(&mut *store, 1)?;
264 let arr = match raw_field {
265 Val::AnyRef(Some(any)) => any.unwrap_array(&mut *store)?,
266 Val::AnyRef(None) => {
267 return Err(wasmtime::Error::msg(
268 "main's $string had null raw-array field",
269 ));
270 }
271 other => wasmtime::bail!("$string field 1 not an arrayref: {other:?}"),
272 };
273 read_submilli_string(&mut *store, arr)
274}
275
276#[cfg(test)]
277mod tests {
278 use std::time::Duration;
279
280 use crate::codegen::tests::compile;
281 use crate::runtime::RuntimeConfig;
282
283 #[tokio::test]
288 async fn large_string_return_fits_default_fuel() {
289 let bytes = compile(
290 r#"function main(): string {
291 let s: string = "x".repeat(250000);
292 return s + "\"";
293 }"#,
294 );
295 let result = RuntimeConfig::default()
296 .run(&bytes)
297 .await
298 .expect("250 KB string returns within the default fuel budget");
299 assert_eq!(result.value.map(|v| v.len()), Some(250_001));
301 }
302
303 #[tokio::test]
304 async fn fixture_returns_string_and_captures_console() {
305 let bytes =
306 compile(r#"function main(): string { console.log("debug"); return "result"; }"#);
307 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
308 assert_eq!(result.value.as_deref(), Some("result"));
309 assert_eq!(result.console, "debug\n");
310 }
311
312 #[tokio::test]
313 async fn void_main_returns_none() {
314 let bytes = compile("function main(): void { }");
315 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
316 assert!(result.value.is_none());
317 assert_eq!(result.console, "");
318 }
319
320 #[tokio::test]
321 async fn undefined_main_results_are_absent_and_null_is_concrete() {
322 for source in [
323 "function main(): undefined { return undefined; }",
324 "function main(): unknown { return undefined; }",
325 "function main(): unknown { return; }",
326 "function main(): string | undefined { return undefined; }",
327 ] {
328 let result = RuntimeConfig::default()
329 .run(&compile(source))
330 .await
331 .expect("runs");
332 assert_eq!(result.value, None, "{source}");
333 }
334 let result = RuntimeConfig::default()
335 .run(&compile("function main(): null { return null; }"))
336 .await
337 .expect("runs");
338 assert_eq!(result.value.as_deref(), Some("null"));
339 }
340
341 #[tokio::test]
342 async fn optional_string_main_preserves_verbatim_output() {
343 let result = RuntimeConfig::default()
344 .run(&compile(
345 "function main(): string | undefined { return 'hello'; }",
346 ))
347 .await
348 .expect("runs");
349 assert_eq!(result.value.as_deref(), Some("hello"));
350 }
351
352 #[tokio::test]
353 async fn number_main_returns_json_number() {
354 let bytes = compile("function main(): number { return 42; }");
355 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
356 assert_eq!(result.value.as_deref(), Some("42"));
357 assert_eq!(result.console, "");
358 }
359
360 #[tokio::test]
361 async fn boolean_main_returns_json_boolean() {
362 let bytes = compile("function main(): boolean { return 1 < 2; }");
363 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
364 assert_eq!(result.value.as_deref(), Some("true"));
365 assert_eq!(result.console, "");
366 }
367
368 #[tokio::test]
369 async fn widened_boolean_literal_main_preserves_scalar_output() {
370 let source = include_str!("../../tests/fixtures/narrowing/live_boolean_literal_result.ts");
371 for source in [source.to_owned(), source.replace("true", "false")] {
372 let bytes = compile(&source);
373 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
374 assert_eq!(result.value.as_deref(), Some("changed"));
375 }
376 }
377
378 #[tokio::test]
379 async fn console_and_return_are_independent_streams() {
380 let bytes = compile(r#"function main(): number { console.log("hi"); return 7; }"#);
381 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
382 assert_eq!(result.value.as_deref(), Some("7"));
383 assert_eq!(result.console, "hi\n");
384 }
385
386 #[tokio::test]
387 async fn string_return_is_verbatim_not_escaped() {
388 let bytes = compile(r#"function main(): string { return "a\"b\\c\n"; }"#);
390 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
391 assert_eq!(result.value.as_deref(), Some("a\"b\\c\n"));
392 }
393
394 #[tokio::test]
395 async fn nan_returns_nan_via_to_string() {
396 let bytes = compile("function main(): number { return 0 / 0; }");
397 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
398 assert_eq!(result.value.as_deref(), Some("NaN"));
399 }
400
401 #[tokio::test]
402 async fn infinity_returns_infinity_via_to_string() {
403 let bytes = compile("function main(): number { return 1 / 0; }");
404 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
405 assert_eq!(result.value.as_deref(), Some("Infinity"));
406 }
407
408 #[tokio::test]
409 async fn object_main_returns_json_object() {
410 let compiled =
411 compile_full("function main(): { a: number; b: number } { return { a: 1, b: 2 }; }");
412 let result = RuntimeConfig::default()
413 .run_compiled(&compiled)
414 .await
415 .expect("runs");
416 assert_eq!(result.value.as_deref(), Some(r#"{"a":1,"b":2}"#));
417 }
418
419 #[tokio::test]
420 async fn object_via_local_returns_json_object() {
421 let compiled = compile_full(
422 "function main(): { a: number } { const o: { a: number } = { a: 1 }; return o; }",
423 );
424 let result = RuntimeConfig::default()
425 .run_compiled(&compiled)
426 .await
427 .expect("runs");
428 assert_eq!(result.value.as_deref(), Some(r#"{"a":1}"#));
429 }
430
431 #[tokio::test]
432 async fn number_array_main_returns_json_array() {
433 let bytes = compile("function main(): number[] { return [1, 2, 3]; }");
434 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
435 assert_eq!(result.value.as_deref(), Some("[1,2,3]"));
436 }
437
438 #[tokio::test]
439 async fn string_field_object_is_json_escaped() {
440 let compiled = compile_full(r#"function main(): { n: string } { return { n: "a\"b" }; }"#);
441 let result = RuntimeConfig::default()
442 .run_compiled(&compiled)
443 .await
444 .expect("runs");
445 assert_eq!(result.value.as_deref(), Some(r#"{"n":"a\"b"}"#));
446 }
447
448 #[tokio::test]
449 async fn nested_object_main_returns_json() {
450 let compiled = compile_full(
451 "function main(): { inner: { v: number } } { return { inner: { v: 1 } }; }",
452 );
453 let result = RuntimeConfig::default()
454 .run_compiled(&compiled)
455 .await
456 .expect("runs");
457 assert_eq!(result.value.as_deref(), Some(r#"{"inner":{"v":1}}"#));
458 }
459
460 #[tokio::test]
461 async fn array_of_objects_main_returns_json() {
462 let compiled =
463 compile_full("function main(): { v: number }[] { return [{ v: 1 }, { v: 2 }]; }");
464 let result = RuntimeConfig::default()
465 .run_compiled(&compiled)
466 .await
467 .expect("runs");
468 assert_eq!(result.value.as_deref(), Some(r#"[{"v":1},{"v":2}]"#));
469 }
470
471 #[tokio::test]
472 async fn interface_instance_main_returns_json() {
473 let compiled = compile_full(
474 "interface Point { x: number; y: number; }\n\
475 function main(): Point { return { x: 1, y: 2 }; }",
476 );
477 let result = RuntimeConfig::default()
478 .run_compiled(&compiled)
479 .await
480 .expect("runs");
481 assert_eq!(result.value.as_deref(), Some(r#"{"x":1,"y":2}"#));
482 }
483
484 fn compile_full(source: &str) -> crate::compile::CompiledScript {
485 crate::compile::compile_script(source, "script.subm", crate::FileId(0), &[], &[])
486 .expect("test-only compile expects no diagnostics")
487 }
488
489 #[tokio::test]
490 async fn string_main_returned_verbatim() {
491 let bytes = compile(r#"function main(): string { return "hi"; }"#);
493 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
494 assert_eq!(result.value.as_deref(), Some("hi"));
495 }
496
497 #[tokio::test]
498 async fn negative_zero_main_normalizes_to_zero() {
499 let bytes = compile("function main(): number { return 0 * -1; }");
501 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
502 assert_eq!(result.value.as_deref(), Some("0"));
503 }
504
505 #[tokio::test]
506 async fn timeout_interrupts_infinite_loop() {
507 let bytes = compile("function main(): void { while (true) { } }");
509 let cfg = RuntimeConfig {
510 fuel: u64::MAX,
511 timeout: Some(Duration::from_millis(50)),
512 ..RuntimeConfig::default()
513 };
514 let err = cfg.run(&bytes).await.expect_err("must trap");
515 let trap = err.downcast_ref::<wasmtime::Trap>();
516 assert_eq!(
517 trap,
518 Some(&wasmtime::Trap::Interrupt),
519 "expected timeout interrupt, got {trap:?} (full error: {err:?})",
520 );
521 }
522
523 #[tokio::test]
524 async fn failed_assert_in_main_propagates_as_error() {
525 let bytes = compile(r#"function main(): void { assert(false, "boom"); }"#);
526 let err = RuntimeConfig::default()
527 .run(&bytes)
528 .await
529 .expect_err("failed assert should bubble");
530 let rendered = format!("{err:?}");
531 assert!(
532 rendered.contains("boom"),
533 "expected failed assert to surface its thrown message, got: {rendered}",
534 );
535 }
536
537 #[tokio::test]
538 async fn uncaught_throw_surfaces_error_message() {
539 let bytes = compile(r#"function main(): void { throw new Error("boom message"); }"#);
540 let err = RuntimeConfig::default()
541 .run(&bytes)
542 .await
543 .expect_err("uncaught throw should surface as an error");
544 let rendered = format!("{err}");
545 assert!(
546 rendered.contains("boom message"),
547 "uncaught exception should surface its message, got: {rendered}",
548 );
549 assert!(
550 !rendered.contains("thrown Wasm exception"),
551 "the opaque placeholder should be replaced, got: {rendered}",
552 );
553 }
554
555 #[tokio::test]
556 async fn uncaught_throw_renders_source_backtrace() {
557 let src = "function deep(): void {\n throw new Error(\"boom\");\n}\nfunction main(): void {\n deep();\n}";
559 let bytes = crate::codegen::tests::compile(src);
560 let err = RuntimeConfig::default()
561 .run(&bytes)
562 .await
563 .expect_err("uncaught throw");
564 let (sources, file) = crate::Sources::single("script.subm", src).unwrap();
565 let rendered =
566 crate::backtrace::render(&err, &sources, file, crate::backtrace::BacktraceMode::Full)
567 .expect("an uncaught throw should render a backtrace");
568 assert!(
569 rendered.contains("error: Error: boom"),
570 "message header missing: {rendered}"
571 );
572 assert!(
573 rendered.contains("at deep ("),
574 "throw-site frame: {rendered}"
575 );
576 assert!(rendered.contains("at main ("), "caller frame: {rendered}");
577 assert!(
578 rendered.contains("thrown here"),
579 "frame-0 label: {rendered}"
580 );
581 assert!(
582 rendered.contains("throw new Error"),
583 "source context: {rendered}"
584 );
585 assert!(
586 !rendered.contains("fields:"),
587 "a plain Error has no extra data fields: {rendered}"
588 );
589 }
590
591 #[tokio::test]
592 async fn uncaught_top_level_throw_renders_its_statement() {
593 let src = "const n: number = 1;\nif (n === 1) { throw new Error(\"boom\"); }\nfunction main(): void {}";
595 let bytes = crate::codegen::tests::compile(src);
596 let err = RuntimeConfig::default()
597 .run(&bytes)
598 .await
599 .expect_err("uncaught top-level throw");
600 let (sources, file) = crate::Sources::single("script.subm", src).unwrap();
601 let rendered =
602 crate::backtrace::render(&err, &sources, file, crate::backtrace::BacktraceMode::Full)
603 .expect("a top-level throw should render a backtrace");
604 assert!(
605 rendered.starts_with(
606 "error: Error: boom\n at <top level> (script.subm:2:22) [thrown here]\n"
607 ),
608 "{rendered}"
609 );
610 assert!(
611 rendered.contains("2 | if (n === 1) { throw new Error(\"boom\"); }"),
612 "source context: {rendered}"
613 );
614 }
615
616 #[tokio::test]
617 async fn a_top_level_statement_is_the_entry_frame_below_its_callee() {
618 let src = "function refuse(): number { throw new Error(\"boom\"); }\nconst n: number = refuse();\nfunction main(): void {}";
619 let bytes = crate::codegen::tests::compile(src);
620 let err = RuntimeConfig::default()
621 .run(&bytes)
622 .await
623 .expect_err("uncaught top-level throw");
624 let (sources, file) = crate::Sources::single("script.subm", src).unwrap();
625 let rendered =
626 crate::backtrace::render(&err, &sources, file, crate::backtrace::BacktraceMode::Full)
627 .expect("a top-level throw should render a backtrace");
628 assert!(
629 rendered.contains(" at refuse (script.subm:1:")
630 && rendered.contains("[thrown here]")
631 && rendered.contains(" at <top level> (script.subm:2:19) [entry]\n"),
632 "{rendered}"
633 );
634 }
635
636 #[tokio::test]
637 async fn uncaught_class_error_renders_data_fields() {
638 let src = r#"class ApiError extends Error {
639 code: string;
640 status: number;
641 retryable: boolean;
642 requestId: string | null;
643 constructor(code: string, message: string, status: number) {
644 super(message);
645 this.name = "ApiError";
646 this.code = code;
647 this.status = status;
648 this.retryable = false;
649 this.requestId = null;
650 }
651 }
652 function main(): void { throw new ApiError("badRequest", "Bad Request", 400); }"#;
653 let bytes = compile(src);
654 let err = RuntimeConfig::default()
655 .run(&bytes)
656 .await
657 .expect_err("uncaught throw");
658 let rendered = format!("{err}");
659 assert!(
660 rendered.contains("ApiError: Bad Request"),
661 "header: {rendered}"
662 );
663 assert!(
664 rendered.contains(
665 " fields: code = \"badRequest\", requestId = null, retryable = false, status = 400"
666 ),
667 "fields line: {rendered}"
668 );
669 }
670
671 #[tokio::test]
672 async fn error_fields_skip_methods_and_objects() {
673 let src = r#"class DetailError extends Error {
674 code: string;
675 detail: number[];
676 constructor() {
677 super("nope");
678 this.name = "DetailError";
679 this.code = "bad";
680 this.detail = [1, 2];
681 }
682 describe(): string { return this.code; }
683 }
684 function main(): void { throw new DetailError(); }"#;
685 let bytes = compile(src);
686 let err = RuntimeConfig::default()
687 .run(&bytes)
688 .await
689 .expect_err("uncaught throw");
690 let rendered = format!("{err}");
691 assert!(
692 rendered.contains("fields: code = \"bad\""),
693 "primitive field renders: {rendered}"
694 );
695 assert!(
696 !rendered.contains("detail") && !rendered.contains("describe"),
697 "object fields and methods stay out of the dump: {rendered}"
698 );
699 }
700
701 #[tokio::test]
702 async fn error_field_strings_truncate() {
703 let src = r#"class BigError extends Error {
704 body: string;
705 constructor() {
706 super("big");
707 this.name = "BigError";
708 this.body = "x".repeat(500);
709 }
710 }
711 function main(): void { throw new BigError(); }"#;
712 let bytes = compile(src);
713 let err = RuntimeConfig::default()
714 .run(&bytes)
715 .await
716 .expect_err("uncaught throw");
717 let rendered = format!("{err}");
718 assert!(
719 rendered.contains("…") && !rendered.contains(&"x".repeat(200)),
720 "long string field truncates: {rendered}"
721 );
722 }
723
724 #[tokio::test]
725 async fn uncaught_host_throw_renders_clean_backtrace() {
726 let src = "function main(): void {\n Temporal.Instant.from(\"nope\");\n}";
729 let bytes = crate::codegen::tests::compile(src);
730 let err = RuntimeConfig::default()
731 .run(&bytes)
732 .await
733 .expect_err("uncaught host throw");
734 let (sources, file) = crate::Sources::single("script.subm", src).unwrap();
735 let rendered =
736 crate::backtrace::render(&err, &sources, file, crate::backtrace::BacktraceMode::Full)
737 .expect("a host throw should render a backtrace");
738 assert!(
739 rendered.contains("not a valid ISO 8601 instant"),
740 "clean message: {rendered}"
741 );
742 assert!(!rendered.contains("jiff"), "must not leak jiff: {rendered}");
743 assert!(rendered.contains("at main ("), "frame: {rendered}");
744 }
745
746 #[tokio::test]
747 async fn uncaught_host_throw_surfaces_message() {
748 let bytes = compile(r#"function main(): void { JSON.parse("not json") as number; }"#);
751 let err = RuntimeConfig::default()
752 .run(&bytes)
753 .await
754 .expect_err("uncaught host throw should surface as an error");
755 let rendered = format!("{err}");
756 assert!(
757 rendered.contains("JSON.parse:"),
758 "uncaught host exception should surface its message, got: {rendered}",
759 );
760 }
761
762 #[tokio::test]
763 async fn uncaught_json_shape_mismatch_surfaces_message() {
764 let bytes = compile(
767 r#"function main(): number { const n = JSON.parse("\"x\"") as number; return n; }"#,
768 );
769 let err = RuntimeConfig::default()
770 .run(&bytes)
771 .await
772 .expect_err("shape mismatch should surface as an error");
773 let rendered = format!("{err}");
774 assert!(
775 rendered.contains("type mismatch: expected number, got string"),
776 "uncaught shape mismatch should surface its message, got: {rendered}",
777 );
778 }
779
780 #[tokio::test]
781 async fn number_to_string_arithmetic() {
782 let bytes = compile("function main(): string { return (1 + 2 * 3).toString(); }");
783 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
784 assert_eq!(result.value.as_deref(), Some("7"));
785 }
786
787 #[tokio::test]
788 async fn number_to_string_double_precision() {
789 let bytes = compile("function main(): string { return (0.1 + 0.2).toString(); }");
792 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
793 assert_eq!(result.value.as_deref(), Some("0.30000000000000004"));
794 }
795
796 #[tokio::test]
797 async fn string_coercion_alias_for_to_string() {
798 let bytes = compile("function main(): string { return String(42); }");
799 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
800 assert_eq!(result.value.as_deref(), Some("42"));
801 }
802
803 #[tokio::test]
804 async fn number_coercion_parses_decimal() {
805 let bytes = compile(r#"function main(): number { return Number("1.3"); }"#);
806 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
807 assert_eq!(result.value.as_deref(), Some("1.3"));
808 }
809
810 #[tokio::test]
811 async fn number_coercion_empty_string_is_zero() {
812 let bytes = compile(r#"function main(): number { return Number(""); }"#);
814 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
815 assert_eq!(result.value.as_deref(), Some("0"));
816 }
817
818 #[tokio::test]
819 async fn number_coercion_invalid_returns_nan() {
820 let bytes = compile(r#"function main(): number { return Number("abc"); }"#);
821 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
822 assert_eq!(result.value.as_deref(), Some("NaN"));
823 }
824
825 #[tokio::test]
826 async fn parse_int_prefix_decimal() {
827 let bytes = compile(r#"function main(): number { return parseInt("42px"); }"#);
828 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
829 assert_eq!(result.value.as_deref(), Some("42"));
830 }
831
832 #[tokio::test]
833 async fn parse_int_hex_with_explicit_radix() {
834 let bytes = compile(r#"function main(): number { return parseInt("0xff", 16); }"#);
835 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
836 assert_eq!(result.value.as_deref(), Some("255"));
837 }
838
839 #[tokio::test]
840 async fn parse_int_no_digits_is_nan() {
841 let bytes = compile(r#"function main(): number { return parseInt("abc"); }"#);
842 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
843 assert_eq!(result.value.as_deref(), Some("NaN"));
844 }
845
846 #[tokio::test]
847 async fn parse_float_prefix() {
848 let bytes = compile(r#"function main(): number { return parseFloat("1.5x"); }"#);
849 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
850 assert_eq!(result.value.as_deref(), Some("1.5"));
851 }
852
853 #[tokio::test]
854 async fn parse_float_leading_decimal() {
855 let bytes = compile(r#"function main(): number { return parseFloat(".5"); }"#);
856 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
857 assert_eq!(result.value.as_deref(), Some("0.5"));
858 }
859
860 #[tokio::test]
861 async fn number_to_string_round_trip() {
862 let bytes = compile("function main(): boolean { return Number((42).toString()) === 42; }");
863 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
864 assert_eq!(result.value.as_deref(), Some("true"));
865 }
866
867 #[tokio::test]
868 async fn object_structural_equality_distinct_allocations() {
869 let bytes = compile(
870 "function main(): boolean { let p = { a: 1 }; let q = { a: 1 }; return p === q; }",
871 );
872 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
873 assert_eq!(result.value.as_deref(), Some("true"));
874 }
875
876 #[tokio::test]
877 async fn object_structural_inequality_different_field_values() {
878 let bytes = compile(
879 "function main(): boolean { let p = { a: 1 }; let q = { a: 2 }; return p === q; }",
880 );
881 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
882 assert_eq!(result.value.as_deref(), Some("false"));
883 }
884
885 #[tokio::test]
886 async fn object_ref_eq_fast_path() {
887 let bytes = compile("function main(): boolean { let p = { a: 1 }; return p === p; }");
888 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
889 assert_eq!(result.value.as_deref(), Some("true"));
890 }
891
892 #[tokio::test]
893 async fn object_with_string_field_structural_equals() {
894 let bytes = compile(
895 r#"function main(): boolean { let p = { n: "x" }; let q = { n: "x" }; return p === q; }"#,
896 );
897 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
898 assert_eq!(result.value.as_deref(), Some("true"));
899 }
900
901 #[tokio::test]
902 async fn nested_object_equal() {
903 let bytes = compile(
904 "function main(): boolean { let p = { inner: { v: 1 } }; let q = { inner: { v: 1 } }; return p === q; }",
905 );
906 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
907 assert_eq!(result.value.as_deref(), Some("true"));
908 }
909
910 #[tokio::test]
911 async fn nested_object_inner_differs() {
912 let bytes = compile(
913 "function main(): boolean { let p = { inner: { v: 1 } }; let q = { inner: { v: 2 } }; return p === q; }",
914 );
915 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
916 assert_eq!(result.value.as_deref(), Some("false"));
917 }
918
919 #[tokio::test]
920 async fn array_structural_equality_distinct_allocations() {
921 let bytes = compile(
922 "function main(): boolean { let a = [1, 2, 3]; let b = [1, 2, 3]; return a === b; }",
923 );
924 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
925 assert_eq!(result.value.as_deref(), Some("true"));
926 }
927
928 #[tokio::test]
929 async fn array_inequality_same_length_different_elements() {
930 let bytes = compile(
931 "function main(): boolean { let a = [1, 2, 3]; let b = [1, 2, 4]; return a === b; }",
932 );
933 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
934 assert_eq!(result.value.as_deref(), Some("false"));
935 }
936
937 #[tokio::test]
938 async fn array_inequality_different_lengths() {
939 let bytes = compile(
940 "function main(): boolean { let a = [1, 2]; let b = [1, 2, 3]; return a === b; }",
941 );
942 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
943 assert_eq!(result.value.as_deref(), Some("false"));
944 }
945
946 #[tokio::test]
947 async fn array_of_strings_structural_equals() {
948 let bytes = compile(
949 r#"function main(): boolean { let a = ["x", "y"]; let b = ["x", "y"]; return a === b; }"#,
950 );
951 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
952 assert_eq!(result.value.as_deref(), Some("true"));
953 }
954
955 #[tokio::test]
956 async fn array_of_objects_structural_equals() {
957 let bytes = compile(
958 "function main(): boolean { let a = [{ v: 1 }, { v: 2 }]; let b = [{ v: 1 }, { v: 2 }]; return a === b; }",
959 );
960 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
961 assert_eq!(result.value.as_deref(), Some("true"));
962 }
963
964 #[tokio::test]
965 async fn double_eq_same_as_strict_eq() {
966 let bytes = compile(
967 "function main(): boolean { const two: number = 2; return (1 === 1) && (1 == 1) && (1 !== two) && (1 != two); }",
968 );
969 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
970 assert_eq!(result.value.as_deref(), Some("true"));
971 }
972
973 #[tokio::test]
974 async fn object_not_eq_round_trip() {
975 let bytes = compile(
976 "function main(): boolean { let p = { a: 1 }; let q = { a: 2 }; return p !== q; }",
977 );
978 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
979 assert_eq!(result.value.as_deref(), Some("true"));
980 }
981
982 #[tokio::test]
983 async fn null_self_equality() {
984 let bytes = compile("function main(): boolean { return null === null; }");
985 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
986 assert_eq!(result.value.as_deref(), Some("true"));
987 }
988
989 #[tokio::test]
990 async fn null_not_equal_to_self_is_false() {
991 let bytes = compile("function main(): boolean { return null !== null; }");
992 let result = RuntimeConfig::default().run(&bytes).await.expect("runs");
993 assert_eq!(result.value.as_deref(), Some("false"));
994 }
995}