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