Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2283,6 +2283,8 @@ pub static API_MANIFEST: &[ApiEntry] = &[
method("assert", "notDeepStrictEqual", false, None),
method("assert", "match", false, None),
method("assert", "doesNotMatch", false, None),
method("assert", "throws", false, None),
method("assert", "doesNotThrow", false, None),
method("assert", "ifError", false, None),
method("assert", "default", false, None),
method("assert", "strict", false, None),
Expand All @@ -2301,6 +2303,8 @@ pub static API_MANIFEST: &[ApiEntry] = &[
method("assert/strict", "notDeepStrictEqual", false, None),
method("assert/strict", "match", false, None),
method("assert/strict", "doesNotMatch", false, None),
method("assert/strict", "throws", false, None),
method("assert/strict", "doesNotThrow", false, None),
method("assert/strict", "ifError", false, None),
method("assert/strict", "default", false, None),
class("assert/strict", "AssertionError"),
Expand Down
36 changes: 36 additions & 0 deletions crates/perry-codegen/src/lower_call/native_table/node_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,24 @@ pub(super) const NODE_CORE_ROWS: &[NativeModSig] = &[
args: &[NA_F64, NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "assert",
has_receiver: false,
method: "throws",
class_filter: None,
runtime: "js_assert_throws",
args: &[NA_F64, NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "assert",
has_receiver: false,
method: "doesNotThrow",
class_filter: None,
runtime: "js_assert_does_not_throw",
args: &[NA_F64, NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "assert",
has_receiver: false,
Expand Down Expand Up @@ -702,6 +720,24 @@ pub(super) const NODE_CORE_ROWS: &[NativeModSig] = &[
args: &[NA_F64, NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "assert/strict",
has_receiver: false,
method: "throws",
class_filter: None,
runtime: "js_assert_throws",
args: &[NA_F64, NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "assert/strict",
has_receiver: false,
method: "doesNotThrow",
class_filter: None,
runtime: "js_assert_does_not_throw",
args: &[NA_F64, NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "assert/strict",
has_receiver: false,
Expand Down
175 changes: 175 additions & 0 deletions crates/perry-runtime/src/object/assert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! Split out of `object/mod.rs` (issue #1103). Pure relocation — no
//! logic changes.

use std::os::raw::c_int;

use super::*;

fn undefined_f64() -> f64 {
Expand Down Expand Up @@ -61,6 +63,141 @@ fn regex_test_value(pattern: f64, input: f64) -> Option<bool> {
Some(crate::regex::js_regexp_test(ptr as *const crate::regex::RegExpHeader, input_ptr) != 0)
}

fn read_property(value: f64, key: &str) -> f64 {
let jv = crate::value::JSValue::from_bits(value.to_bits());
if !jv.is_pointer() {
return undefined_f64();
}
let ptr = jv.as_pointer::<ObjectHeader>();
if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 {
return undefined_f64();
}
let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32);
crate::object::js_object_get_field_by_name_f64(ptr, key_ptr)
}

fn is_plain_matcher_object(value: f64) -> bool {
let jv = crate::value::JSValue::from_bits(value.to_bits());
if !jv.is_pointer() {
return false;
}
let ptr = jv.as_pointer::<u8>();
if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 {
return false;
}
unsafe {
let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
(*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT
}
}

fn object_matcher_matches(actual: f64, expected: f64) -> bool {
if !is_plain_matcher_object(expected) {
return false;
}

let mut saw_expected_key = false;
for key in ["name", "message", "code", "errno"] {
let expected_prop = read_property(expected, key);
if is_null_or_undefined(expected_prop) {
continue;
}
saw_expected_key = true;
let actual_prop = read_property(actual, key);
if is_null_or_undefined(actual_prop) && matches!(key, "code" | "errno") {
continue;
}
if !assert_same_value(actual_prop, expected_prop)
&& crate::value::js_jsvalue_loose_equals(actual_prop, expected_prop) == 0
{
return false;
}
}

saw_expected_key
}

fn constructor_name_matches_builtin_error(thrown: f64, expected: f64) -> bool {
fn global_builtin(name: &'static [u8]) -> f64 {
crate::object::js_get_global_this_builtin_value(name.as_ptr(), name.len())
}

let expected_name = if expected.to_bits() == global_builtin(b"Error").to_bits() {
"Error"
} else if expected.to_bits() == global_builtin(b"TypeError").to_bits() {
"TypeError"
} else if expected.to_bits() == global_builtin(b"RangeError").to_bits() {
"RangeError"
} else if expected.to_bits() == global_builtin(b"ReferenceError").to_bits() {
"ReferenceError"
} else if expected.to_bits() == global_builtin(b"SyntaxError").to_bits() {
"SyntaxError"
} else if expected.to_bits() == global_builtin(b"AggregateError").to_bits() {
"AggregateError"
} else {
let expected_name = read_property(expected, "name");
if is_null_or_undefined(expected_name) {
return false;
}
let expected_name = value_to_string(expected_name);
if !matches!(
expected_name.as_str(),
"Error"
| "TypeError"
| "RangeError"
| "ReferenceError"
| "SyntaxError"
| "AggregateError"
) {
return false;
}
let thrown_name = read_property(thrown, "name");
return !is_null_or_undefined(thrown_name) && value_to_string(thrown_name) == expected_name;
};
if expected_name == "Error" && is_error_value(thrown) {
return true;
}
let thrown_name = read_property(thrown, "name");
!is_null_or_undefined(thrown_name) && value_to_string(thrown_name) == expected_name
}

fn expected_error_matches(thrown: f64, expected: f64) -> bool {
if is_null_or_undefined(expected) {
return true;
}
if let Some(matches_thrown) = regex_test_value(expected, thrown) {
if matches_thrown {
return true;
}
let message = read_property(thrown, "message");
if !is_null_or_undefined(message) && regex_test_value(expected, message).unwrap_or(false) {
return true;
}
}
if crate::value::js_is_truthy(crate::object::js_instanceof_dynamic(thrown, expected)) != 0 {
return true;
}
if constructor_name_matches_builtin_error(thrown, expected) {
return true;
}
object_matcher_matches(thrown, expected)
}

fn call_block_capturing_throw(block: f64) -> Result<f64, f64> {
let trap_buf = crate::exception::js_try_push();
let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut c_int) };
let result = if jumped == 0 {
let value = unsafe { crate::closure::js_native_call_value(block, std::ptr::null(), 0) };
Ok(value)
} else {
let exc = crate::exception::js_get_exception();
crate::exception::js_clear_exception();
Err(exc)
};
crate::exception::js_try_end();
result
}

fn assertion_message(custom_message: f64, fallback: &str) -> String {
if is_null_or_undefined(custom_message) {
fallback.to_string()
Expand Down Expand Up @@ -340,6 +477,44 @@ pub extern "C" fn js_assert_does_not_match(actual: f64, expected: f64, message:
)
}

#[no_mangle]
pub extern "C" fn js_assert_throws(block: f64, expected: f64, message: f64) -> f64 {
match call_block_capturing_throw(block) {
Err(thrown) if expected_error_matches(thrown, expected) => undefined_f64(),
Err(thrown) => throw_assertion(
assertion_message(
message,
"The thrown error did not match the expected matcher",
),
thrown,
expected,
"throws",
false,
),
Ok(_) => throw_assertion(
assertion_message(message, "Missing expected exception"),
undefined_f64(),
expected,
"throws",
false,
),
}
}

#[no_mangle]
pub extern "C" fn js_assert_does_not_throw(block: f64, _expected: f64, message: f64) -> f64 {
match call_block_capturing_throw(block) {
Ok(_) => undefined_f64(),
Err(thrown) => throw_assertion(
assertion_message(message, "Got unwanted exception"),
thrown,
undefined_f64(),
"doesNotThrow",
false,
),
}
}

/// `new assert.AssertionError({actual, expected, operator, message, ...})`
/// constructor. Reuses `make_assertion_error` so the resulting object
/// carries the `CLASS_ID_ASSERTION_ERROR` class id, satisfies
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/object/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,8 @@ pub(crate) fn is_native_module_callable_export(module: &str, prop: &str) -> bool
| ("assert", "notDeepStrictEqual")
| ("assert", "match")
| ("assert", "doesNotMatch")
| ("assert", "throws")
| ("assert", "doesNotThrow")
| ("assert", "ifError")
| ("assert", "CallTracker")
| ("assert/strict", "ok")
Expand All @@ -389,6 +391,8 @@ pub(crate) fn is_native_module_callable_export(module: &str, prop: &str) -> bool
| ("assert/strict", "notDeepStrictEqual")
| ("assert/strict", "match")
| ("assert/strict", "doesNotMatch")
| ("assert/strict", "throws")
| ("assert/strict", "doesNotThrow")
| ("assert/strict", "ifError")
| ("os", "platform")
| ("os", "arch")
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-runtime/src/object/native_module_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,12 @@ pub(crate) unsafe fn dispatch_native_module_method(
("assert", "doesNotMatch") | ("assert/strict", "doesNotMatch") => {
js_assert_does_not_match(arg(0), arg(1), arg(2))
}
("assert", "throws") | ("assert/strict", "throws") => {
js_assert_throws(arg(0), arg(1), arg(2))
}
("assert", "doesNotThrow") | ("assert/strict", "doesNotThrow") => {
js_assert_does_not_throw(arg(0), arg(1), arg(2))
}
("assert", "ifError") | ("assert/strict", "ifError") => js_assert_if_error(arg(0)),

// ── fs module (args are NaN-boxed f64, booleans return as i32→f64) ──
Expand Down
10 changes: 9 additions & 1 deletion docs/api/perry.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Auto-generated from Perry's API manifest (#465). Do not edit by hand.
// Source: perry-api-manifest::API_MANIFEST
// Coverage: 1282 entries across 81 modules
// Coverage: 1286 entries across 81 modules

declare module "@perryts/pdf" {
/** stdlib */
Expand Down Expand Up @@ -38,6 +38,8 @@ declare module "assert" {
/** stdlib */
export function doesNotMatch(...args: any[]): any;
/** stdlib */
export function doesNotThrow(...args: any[]): any;
/** stdlib */
export function equal(...args: any[]): any;
/** stdlib */
export function fail(...args: any[]): any;
Expand All @@ -59,6 +61,8 @@ declare module "assert" {
export function strict(...args: any[]): any;
/** stdlib */
export function strictEqual(...args: any[]): any;
/** stdlib */
export function throws(...args: any[]): any;
}

declare module "assert/strict" {
Expand All @@ -73,6 +77,8 @@ declare module "assert/strict" {
/** stdlib */
export function doesNotMatch(...args: any[]): any;
/** stdlib */
export function doesNotThrow(...args: any[]): any;
/** stdlib */
export function equal(...args: any[]): any;
/** stdlib */
export function fail(...args: any[]): any;
Expand All @@ -92,6 +98,8 @@ declare module "assert/strict" {
export function ok(...args: any[]): any;
/** stdlib */
export function strictEqual(...args: any[]): any;
/** stdlib */
export function throws(...args: any[]): any;
}

declare module "async_hooks" {
Expand Down
6 changes: 5 additions & 1 deletion docs/src/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target.

Total: 1282 entries across 81 modules.
Total: 1286 entries across 81 modules.

## Modules

Expand Down Expand Up @@ -120,6 +120,7 @@ Total: 1282 entries across 81 modules.
- `deepStrictEqual` — module
- `default` — module
- `doesNotMatch` — module
- `doesNotThrow` — module
- `equal` — module
- `fail` — module
- `ifError` — module
Expand All @@ -131,6 +132,7 @@ Total: 1282 entries across 81 modules.
- `ok` — module
- `strict` — module
- `strictEqual` — module
- `throws` — module

### Properties

Expand All @@ -148,6 +150,7 @@ Total: 1282 entries across 81 modules.
- `deepStrictEqual` — module
- `default` — module
- `doesNotMatch` — module
- `doesNotThrow` — module
- `equal` — module
- `fail` — module
- `ifError` — module
Expand All @@ -158,6 +161,7 @@ Total: 1282 entries across 81 modules.
- `notStrictEqual` — module
- `ok` — module
- `strictEqual` — module
- `throws` — module

## `async_hooks`

Expand Down