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
3 changes: 3 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
# this is a visibility job, not a gate.
run: |
cargo llvm-cov --workspace --no-fail-fast \
--exclude perry-doc-fixture-my-bindings \
--exclude perry-ui-macos \
--exclude perry-ui-ios \
--exclude perry-ui-visionos \
Expand All @@ -64,6 +65,7 @@ jobs:
--exclude perry-ui-windows \
--html --output-dir target/llvm-cov-html
cargo llvm-cov report --no-fail-fast \
--exclude perry-doc-fixture-my-bindings \
--exclude perry-ui-macos \
--exclude perry-ui-ios \
--exclude perry-ui-visionos \
Expand All @@ -74,6 +76,7 @@ jobs:
--exclude perry-ui-windows \
--lcov --output-path target/lcov.info
cargo llvm-cov report --no-fail-fast \
--exclude perry-doc-fixture-my-bindings \
--exclude perry-ui-macos \
--exclude perry-ui-ios \
--exclude perry-ui-visionos \
Expand Down
45 changes: 34 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,18 +189,41 @@ jobs:
RUST_TEST_THREADS=1 cargo test -p perry-runtime
# The remaining workspace includes large `perry` / `perry-stdlib`
# test binaries. Keep Cargo build jobs serialized so the runner
# does not link several of those large test binaries at once.
# does not link several of those large test binaries at once, then
# run packages one at a time and prune linked test executables so
# target/debug/deps does not exhaust the runner disk mid-job.
export CARGO_BUILD_JOBS=1
cargo test --workspace \
--exclude perry-runtime \
--exclude perry-ui-macos \
--exclude perry-ui-ios \
--exclude perry-ui-visionos \
--exclude perry-ui-tvos \
--exclude perry-ui-watchos \
--exclude perry-ui-gtk4 \
--exclude perry-ui-android \
--exclude perry-ui-windows
workspace_packages="$(
cargo metadata --no-deps --format-version 1 | python3 -c '
import json
import sys

excluded = {
"perry-runtime",
"perry-ui-macos",
"perry-ui-ios",
"perry-ui-visionos",
"perry-ui-tvos",
"perry-ui-watchos",
"perry-ui-gtk4",
"perry-ui-android",
"perry-ui-windows",
"perry-doc-fixture-my-bindings",
}
metadata = json.load(sys.stdin)
workspace_members = set(metadata["workspace_members"])
for package in metadata["packages"]:
if package["id"] in workspace_members and package["name"] not in excluded:
print(package["name"])
'
)"

for package in $workspace_packages; do
echo "::group::cargo test -p $package"
cargo test -p "$package"
echo "::endgroup::"
find target/debug/deps -maxdepth 1 -type f -perm -111 ! -name '*.so' -delete
done

# ---------------------------------------------------------------------------
# Compiler-output regression gate
Expand Down
15 changes: 11 additions & 4 deletions crates/perry-codegen/src/ext_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,17 @@ pub fn take_used_providers() -> HashSet<OwnerKind> {
mod tests {
use super::*;

static PROVIDER_TEST_LOCK: Mutex<()> = Mutex::new(());

// `USED_PROVIDERS` is a process-wide static; other tests in the same
// process may concurrently insert into it via `LlBlock::call`. We
// therefore check membership rather than exact set equality. The
// non-registered-FFI check uses a deliberately unique symbol name
// that no other test will ever insert.
// process may concurrently insert into it via `LlBlock::call`, and these
// module tests drain it. Serialize the explicit drain/record assertions so
// one test cannot steal another test's providers.
#[test]
fn registry_dispatch_routes_to_correct_owner() {
let _guard = PROVIDER_TEST_LOCK
.lock()
.expect("provider test lock poisoned");
// Drain anything left over from prior tests.
let _ = take_used_providers();

Expand Down Expand Up @@ -380,6 +384,9 @@ mod tests {
/// `Undefined symbols: _js_node_http_create_server_with_options`.
#[test]
fn emitted_create_server_symbol_routes_to_http() {
let _guard = PROVIDER_TEST_LOCK
.lock()
.expect("provider test lock poisoned");
let _ = take_used_providers();
record_ffi_call("js_node_http_create_server_with_options");
let got = take_used_providers();
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,8 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_rangeerror_new", I64, &[I64]);
module.declare_function("js_syntaxerror_new", I64, &[I64]);
module.declare_function("js_referenceerror_new", I64, &[I64]);
module.declare_function("js_throw_symbol_constructor_type_error", DOUBLE, &[]);
module.declare_function("js_throw_bigint_constructor_type_error", DOUBLE, &[]);
module.declare_function("js_evalerror_new", I64, &[I64]);
module.declare_function("js_urierror_new", I64, &[I64]);
// WeakMap / WeakSet / WeakRef / FinalizationRegistry — called
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-ext-fastify/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ lazy_static = "1.5"

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
perry-runtime.workspace = true
perry-runtime = { workspace = true, features = ["external-ws-symbols"] }
2 changes: 1 addition & 1 deletion crates/perry-ext-http-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ tokio-tungstenite = { workspace = true }

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
perry-runtime.workspace = true
perry-runtime = { workspace = true, features = ["external-ws-symbols"] }
2 changes: 1 addition & 1 deletion crates/perry-ext-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ perry-ext-http-server = { path = "../perry-ext-http-server" }
# directly. Cargo feature unification keeps the stdlib feature on when
# both stdlib and this crate link the same perry-runtime — no duplicate
# symbols, no behaviour change for the default `full` build.
perry-runtime.workspace = true
perry-runtime = { workspace = true, features = ["external-ws-symbols"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "http2"], default-features = false }
tokio = { workspace = true }
serde_json = "1"
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-ext-ws/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ lazy_static = "1.5"

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
perry-runtime.workspace = true
perry-runtime = { workspace = true, features = ["external-ws-symbols"] }
44 changes: 43 additions & 1 deletion crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! `new (someFn)(args)` form via `Expr::NewDynamic`.

use anyhow::{anyhow, Result};
use perry_types::LocalId;
use perry_types::{LocalId, Type};
use swc_ecma_ast as ast;

use crate::ir::Expr;
Expand All @@ -32,6 +32,30 @@ fn peel_new_callee(mut expr: &ast::Expr) -> &ast::Expr {
}
}

fn nonconstructable_builtin_throw_expr(name: &str, mut args: Vec<Expr>) -> Expr {
let helper = match name {
"Symbol" => "js_throw_symbol_constructor_type_error",
"BigInt" => "js_throw_bigint_constructor_type_error",
_ => unreachable!(),
};
let throw_expr = Expr::Call {
callee: Box::new(Expr::ExternFuncRef {
name: helper.to_string(),
param_types: Vec::new(),
return_type: Type::Any,
}),
args: Vec::new(),
type_args: Vec::new(),
};

if args.is_empty() {
throw_expr
} else {
args.push(throw_expr);
Expr::Sequence(args)
}
}

pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> Result<Expr> {
let callee_expr = peel_new_callee(new_expr.callee.as_ref());

Expand Down Expand Up @@ -413,6 +437,19 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
}
}
}
if matches!(class_name.as_str(), "Symbol" | "BigInt") {
let args = new_expr
.args
.as_ref()
.map(|args| {
args.iter()
.map(|a| lower_expr(ctx, &a.expr))
.collect::<Result<Vec<_>>>()
})
.transpose()?
.unwrap_or_default();
return Ok(nonconstructable_builtin_throw_expr(&class_name, args));
}
if class_name == "Proxy" {
let args = new_expr
.args
Expand Down Expand Up @@ -934,6 +971,11 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
.transpose()?
.unwrap_or_default();
if let Expr::PropertyGet { object, property } = callee.as_ref() {
if matches!(object.as_ref(), Expr::GlobalGet(_))
&& matches!(property.as_str(), "Symbol" | "BigInt")
{
return Ok(nonconstructable_builtin_throw_expr(property, args));
}
if matches!(object.as_ref(), Expr::GlobalGet(_)) && property == "File" {
ctx.uses_fetch = true;
return Ok(Expr::New {
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ default = ["full"]
full = ["dep:hostname", "dep:dirs"]
# When perry-stdlib is the consumer, exclude no-op stubs to avoid duplicate symbols
stdlib = []
# Extension-crate test binaries that link the real perry-ext-ws symbols still
# need runtime-link for perry-ffi helpers, but must not also export runtime-only
# js_ws_* stubs.
external-ws-symbols = []
# Enable geisterhand in-process fuzzer (callback registry + dispatch queue)
geisterhand = []
# iOS/tvOS game loop: provides main() that spawns user code on game thread + UIApplicationMain
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,23 @@ pub extern "C" fn js_error_get_stack(error: *mut ErrorHeader) -> *mut StringHead
}
}

fn throw_builtin_not_constructor(name: &'static str) -> ! {
let message = format!("{name} is not a constructor");
let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32);
let err = js_typeerror_new(msg);
crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64))
}

#[no_mangle]
pub extern "C" fn js_throw_symbol_constructor_type_error() -> f64 {
throw_builtin_not_constructor("Symbol")
}

#[no_mangle]
pub extern "C" fn js_throw_bigint_constructor_type_error() -> f64 {
throw_builtin_not_constructor("BigInt")
}

fn throw_capture_stack_trace_target_type_error() -> ! {
let message = b"The \"targetObject\" argument must be an object";
let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32);
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,12 @@ pub unsafe extern "C" fn js_new_function_construct(
std::slice::from_raw_parts(args_ptr, args_len)
};
match name {
"Symbol" => {
return crate::error::js_throw_symbol_constructor_type_error();
}
"BigInt" => {
return crate::error::js_throw_bigint_constructor_type_error();
}
"Date" => {
if args.is_empty() {
return crate::date::js_date_new();
Expand Down
14 changes: 10 additions & 4 deletions crates/perry-runtime/src/stdlib_stubs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
//! call in runtime-only mode prints `[perry] warning: ...` once per
//! symbol per process — see issue #464 and `src/stub_diag.rs`.

use crate::promise::Promise;
use crate::string::StringHeader;
use crate::stub_diag::perry_stub_warn;
use std::ptr;

#[cfg(not(any(
target_os = "ios",
target_os = "android",
feature = "external-ws-symbols"
)))]
const WS_REASON: &str =
"WebSocket symbol from perry-stdlib not linked into this binary (runtime-only build)";
const READLINE_REASON: &str =
Expand All @@ -31,7 +33,11 @@ const STDLIB_DISPATCH_REASON: &str =
// WebSocket implementation using tungstenite+rustls. These stubs must NOT
// be compiled for either platform, otherwise the real implementations will
// be shadowed by the no-op stubs.
#[cfg(not(any(target_os = "ios", target_os = "android")))]
#[cfg(not(any(
target_os = "ios",
target_os = "android",
feature = "external-ws-symbols"
)))]
mod ws_stubs {
use super::{perry_stub_warn, WS_REASON};
use crate::promise::Promise;
Expand Down
31 changes: 31 additions & 0 deletions test-parity/node-suite/globals/symbol-bigint-new-throws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function describe(value: unknown): string {
if (typeof value === "symbol") {
return `symbol:${String(value)}`;
}
if (typeof value === "bigint") {
return `bigint:${value.toString()}`;
}
const ctor = (value as any)?.constructor?.name ?? "no-ctor";
return `${typeof value}:${ctor}`;
}

function show(label: string, fn: () => unknown) {
try {
console.log(label, "ok", describe(fn()));
} catch (err: any) {
console.log(label, err?.name, err?.message);
}
}

const SymbolAlias: any = Symbol;
const BigIntAlias: any = BigInt;

show("symbol call", () => Symbol("x"));
show("bigint call", () => BigInt("42"));

show("symbol direct", () => new (Symbol as any)("x"));
show("bigint direct", () => new (BigInt as any)("1"));
show("symbol global", () => new (globalThis.Symbol as any)("x"));
show("bigint global", () => new (globalThis.BigInt as any)("1"));
show("symbol alias", () => new SymbolAlias("x"));
show("bigint alias", () => new BigIntAlias("1"));