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