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
14 changes: 7 additions & 7 deletions crates/perry-codegen/src/lower_array_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ pub(crate) fn lower_array_method(

match property {
"pop" => {
if !args.is_empty() {
bail!("perry-codegen: Array.pop takes no args, got {}", args.len());
// No-arg method; JS ignores extras but evaluates them for side
// effects before the call. Evaluate then discard.
for extra in args.iter() {
let _ = lower_expr(ctx, extra)?;
}
let blk = ctx.block();
let recv_handle = unbox_to_i64(blk, &recv_box);
Expand Down Expand Up @@ -723,11 +725,9 @@ pub(crate) fn lower_array_method(
Ok(nanbox_pointer_inline(blk, &result))
}
"shift" => {
if !args.is_empty() {
bail!(
"perry-codegen: Array.shift takes no args, got {}",
args.len()
);
// No-arg method; extras are evaluated for side effects then ignored.
for extra in args.iter() {
let _ = lower_expr(ctx, extra)?;
}
let blk = ctx.block();
let recv_handle = unbox_to_i64(blk, &recv_box);
Expand Down
27 changes: 11 additions & 16 deletions crates/perry-codegen/src/lower_string_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,11 @@ pub(crate) fn lower_string_method(
}
// Unary string-returning methods (no args).
"toLowerCase" | "toUpperCase" | "trim" | "trimStart" | "trimEnd" => {
if !args.is_empty() {
bail!(
"perry-codegen: String.{} takes no args, got {}",
property,
args.len()
);
// These methods take no args; JS ignores any extras but still
// evaluates them for side effects (ECMA-262 argument evaluation
// precedes the call). Evaluate then discard.
for extra in args.iter() {
let _ = lower_expr(ctx, extra)?;
}
let blk = ctx.block();
let recv_handle = unbox_str_handle(blk, &recv_box);
Expand Down Expand Up @@ -900,23 +899,19 @@ pub(crate) fn lower_string_method(
Ok(nanbox_pointer_inline(blk, &result))
}
"isWellFormed" => {
if !args.is_empty() {
bail!(
"perry-codegen: String.isWellFormed takes no args, got {}",
args.len()
);
// No-arg method; extras are evaluated for side effects then ignored.
for extra in args.iter() {
let _ = lower_expr(ctx, extra)?;
}
let blk = ctx.block();
let recv_handle = unbox_str_handle(blk, &recv_box);
// Returns a NaN-tagged boolean directly.
Ok(blk.call(DOUBLE, "js_string_is_well_formed", &[(I64, &recv_handle)]))
}
"toWellFormed" => {
if !args.is_empty() {
bail!(
"perry-codegen: String.toWellFormed takes no args, got {}",
args.len()
);
// No-arg method; extras are evaluated for side effects then ignored.
for extra in args.iter() {
let _ = lower_expr(ctx, extra)?;
}
let blk = ctx.block();
let recv_handle = unbox_str_handle(blk, &recv_box);
Expand Down
129 changes: 129 additions & 0 deletions crates/perry-codegen/tests/argless_builtin_extra_args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Regression: argless builtin methods (`String.trim`, `String.toLowerCase`,
//! `Array.pop`, …) must accept and ignore extra arguments rather than bailing.
//!
//! JS ignores surplus args to argless methods (`" x ".trim(1)` is legal and
//! returns the trimmed string), so codegen must lower these without erroring.

use perry_codegen::{compile_module, AppMetadata, CompileOptions};
use perry_hir::{Expr, Module, ModuleInitKind, Stmt};

fn empty_opts() -> CompileOptions {
CompileOptions {
target: None,
is_entry_module: false,
non_entry_module_prefixes: Vec::new(),
import_function_prefixes: std::collections::HashMap::new(),
import_function_origin_names: std::collections::HashMap::new(),
import_function_v8_specifiers: std::collections::HashMap::new(),
import_function_node_submodule: std::collections::HashMap::new(),
namespace_node_submodules: std::collections::HashMap::new(),
namespace_v8_specifiers: std::collections::HashMap::new(),
namespace_member_prefixes: std::collections::HashMap::new(),
emit_ir_only: true,
verify_native_regions: false,
disable_buffer_fast_path: false,
namespace_imports: Vec::new(),
namespace_reexport_named_imports: std::collections::HashSet::new(),
imported_classes: Vec::new(),
imported_enums: Vec::new(),
imported_async_funcs: std::collections::HashSet::new(),
type_aliases: std::collections::HashMap::new(),
imported_func_param_counts: std::collections::HashMap::new(),
imported_func_has_rest: std::collections::HashSet::new(),
imported_func_synthetic_arguments: std::collections::HashSet::new(),
imported_func_return_types: std::collections::HashMap::new(),
imported_vars: std::collections::HashSet::new(),
output_type: "executable".to_string(),
needs_stdlib: false,
needs_ui: false,
needs_geisterhand: false,
geisterhand_port: 7676,
enabled_features: Vec::new(),
native_module_init_names: Vec::new(),
js_module_specifiers: Vec::new(),
bundled_extensions: Vec::new(),
native_library_functions: Vec::new(),
i18n_table: None,
fast_math: false,
fp_contract_mode: perry_codegen::FpContractMode::Off,
app_metadata: AppMetadata::default(),
namespace_entries: Vec::new(),
dynamic_import_path_to_prefix: std::collections::HashMap::new(),
deferred_module_prefixes: std::collections::HashSet::new(),
module_init_deps: Vec::new(),
is_dynamic_import_target: false,
debug_locations: false,
module_source: None,
}
}

fn module_with_init(init: Vec<Stmt>) -> Module {
Module {
name: "argless_extra_args.ts".to_string(),
imports: Vec::new(),
exports: Vec::new(),
classes: Vec::new(),
interfaces: Vec::new(),
type_aliases: Vec::new(),
enums: Vec::new(),
globals: Vec::new(),
functions: Vec::new(),
init,
exported_native_instances: Vec::new(),
exported_func_return_native_instances: Vec::new(),
exported_objects: Vec::new(),
exported_functions: Vec::new(),
widgets: Vec::new(),
uses_fetch: false,
uses_webassembly: false,
extern_funcs: Vec::new(),
init_was_unrolled: false,
has_top_level_await: false,
init_kind: ModuleInitKind::Eager,
async_step_closures: std::collections::HashSet::new(),
closure_display_names: std::collections::HashMap::new(),
closure_source_text: std::collections::HashMap::new(),
async_generator_funcs: std::collections::HashSet::new(),
gen_param_prologue_len: std::collections::HashMap::new(),
}
}

/// `" x ".trim(1)` — String.trim is argless but JS ignores the extra arg.
/// Codegen must lower this without bailing (`String.trim takes no args`).
#[test]
fn string_trim_with_extra_arg_compiles() {
let stmt = Stmt::Expr(Expr::Call {
callee: Box::new(Expr::PropertyGet {
object: Box::new(Expr::String(" x ".to_string())),
property: "trim".to_string(),
}),
args: vec![Expr::Number(1.0)],
type_args: Vec::new(),
byte_offset: 0,
});
let ir = compile_module(&module_with_init(vec![stmt]), empty_opts())
.expect("\" x \".trim(1) must compile (extra arg ignored, not an error)");
let ir = String::from_utf8(ir).unwrap();
// The runtime trim helper must still be emitted — the arg is dropped, not
// routed into a different code path.
assert!(
ir.contains("js_string_trim"),
"expected trim lowering to emit js_string_trim"
);
}

/// `[1].pop(99)` — Array.pop is argless; the extra arg must be ignored.
#[test]
fn array_pop_with_extra_arg_compiles() {
let stmt = Stmt::Expr(Expr::Call {
callee: Box::new(Expr::PropertyGet {
object: Box::new(Expr::Array(vec![Expr::Number(1.0)])),
property: "pop".to_string(),
}),
args: vec![Expr::Number(99.0)],
type_args: Vec::new(),
byte_offset: 0,
});
compile_module(&module_with_init(vec![stmt]), empty_opts())
.expect("[1].pop(99) must compile (extra arg ignored, not an error)");
}
Loading