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