Skip to content
Closed
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 changelog.d/8826-readonly-set-has.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Speed up calls to `ReadonlySet.has` with an exact runtime Set-brand check and
ordinary method dispatch fallback. Type-only class imports now retain the
field metadata needed for this guarded optimization without creating runtime
module bindings or initialization edges.
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ mod call_spread_short;
mod call_spread_short_tests;
#[cfg(test)]
mod issue7628_rooting_tests;
#[cfg(test)]
mod readonly_collection_tests;
pub(crate) mod shadow_slot;
#[cfg(test)]
mod slice7_rooting_tests;
Expand Down
282 changes: 282 additions & 0 deletions crates/perry-codegen/src/expr/readonly_collection_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
use crate::{compile_module, CompileOptions, ImportedClass};
use perry_hir::types::Type;
use perry_hir::{Class, ClassField, Expr, Function, Module, Param, Stmt};

fn number_param(id: u32, name: &str) -> Param {
Param {
id,
name: name.to_string(),
ty: Type::Number,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
}
}

fn has_method() -> Function {
Function {
id: 2,
name: "hasComponent".to_string(),
type_params: Vec::new(),
params: vec![number_param(1, "componentType")],
return_type: Type::Boolean,
body: vec![Stmt::Return(Some(Expr::Call {
callee: Box::new(Expr::PropertyGet {
object: Box::new(Expr::PropertyGet {
object: Box::new(Expr::This),
property: "componentTypeSet".to_string(),
byte_offset: 0,
}),
property: "has".to_string(),
byte_offset: 0,
}),
args: vec![Expr::LocalGet(1)],
type_args: Vec::new(),
byte_offset: 0,
}))],
is_async: false,
is_generator: false,
is_strict: true,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
was_plain_async: false,
was_unrolled: false,
}
}

fn archetype_class() -> Class {
Class {
id: 1,
name: "Archetype".to_string(),
type_params: Vec::new(),
extends: None,
extends_name: None,
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
fields: vec![ClassField {
name: "componentTypeSet".to_string(),
key_expr: None,
ty: Type::Generic {
base: "ReadonlySet".to_string(),
type_args: vec![Type::Number],
},
init: None,
is_private: false,
is_readonly: true,
decorators: Vec::new(),
}],
constructor: None,
methods: vec![has_method()],
getters: Vec::new(),
setters: Vec::new(),
static_accessor_names: Vec::new(),
static_accessor_fn_ids: Vec::new(),
computed_members: Vec::new(),
static_fields: Vec::new(),
static_methods: Vec::new(),
decorators: Vec::new(),
is_exported: false,
aliases: Vec::new(),
is_nested: false,
alloc_width_hint: 0,
specialized_from: None,
}
}

fn executor_class() -> Class {
Class {
id: 3,
name: "CommandExecutor".to_string(),
type_params: Vec::new(),
extends: None,
extends_name: None,
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
fields: Vec::new(),
constructor: None,
methods: vec![Function {
id: 4,
name: "contains".to_string(),
type_params: Vec::new(),
params: vec![
Param {
id: 1,
name: "archetype".to_string(),
ty: Type::Union(vec![Type::Named("Archetype".to_string()), Type::Void]),
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
},
number_param(2, "componentType"),
],
return_type: Type::Boolean,
body: vec![Stmt::Return(Some(Expr::Call {
callee: Box::new(Expr::PropertyGet {
object: Box::new(Expr::PropertyGet {
object: Box::new(Expr::LocalGet(1)),
property: "componentTypeSet".to_string(),
byte_offset: 0,
}),
property: "has".to_string(),
byte_offset: 0,
}),
args: vec![Expr::LocalGet(2)],
type_args: Vec::new(),
byte_offset: 0,
}))],
is_async: false,
is_generator: false,
is_strict: true,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
was_plain_async: false,
was_unrolled: false,
}],
getters: Vec::new(),
setters: Vec::new(),
static_accessor_names: Vec::new(),
static_accessor_fn_ids: Vec::new(),
computed_members: Vec::new(),
static_fields: Vec::new(),
static_methods: Vec::new(),
decorators: Vec::new(),
is_exported: false,
aliases: Vec::new(),
is_nested: false,
alloc_width_hint: 0,
specialized_from: None,
}
}

fn compile_has_ir() -> String {
let mut module = Module::new("readonly_set_field.ts");
module.classes.push(archetype_class());
module.classes.push(executor_class());
String::from_utf8(
compile_module(
&module,
CompileOptions {
emit_ir_only: true,
..Default::default()
},
)
.expect("ReadonlySet field call compiles"),
)
.expect("LLVM IR is UTF-8")
}

fn imported_archetype() -> ImportedClass {
ImportedClass {
name: "Archetype".to_string(),
local_alias: None,
source_prefix: "archetype_ts".to_string(),
constructor_param_count: 0,
has_own_constructor: true,
constructor_has_rest: false,
has_instance_fields: true,
method_names: Vec::new(),
proven_this_method_names: Vec::new(),
proven_this_tower_method_names: Vec::new(),
method_return_types: Vec::new(),
method_param_counts: Vec::new(),
method_has_rest: Vec::new(),
method_has_synthetic_arguments: Vec::new(),
static_field_names: Vec::new(),
static_method_names: Vec::new(),
static_method_return_types: Vec::new(),
static_method_param_counts: Vec::new(),
static_method_has_rest: Vec::new(),
static_method_has_user_rest: Vec::new(),
static_method_has_synthetic_arguments: Vec::new(),
getter_names: Vec::new(),
getter_return_types: Vec::new(),
setter_names: Vec::new(),
parent_name: None,
field_names: vec!["componentTypeSet".to_string()],
field_types: vec![Type::Generic {
base: "ReadonlySet".to_string(),
type_args: vec![Type::Number],
}],
source_class_id: Some(1),
return_shape_imports: Vec::new(),
object_literal: None,
}
}

fn compile_imported_has_ir() -> String {
let mut module = Module::new("command_executor.ts");
module.classes.push(executor_class());
let mut options = CompileOptions {
emit_ir_only: true,
..Default::default()
};
options.imported_classes.push(imported_archetype());
String::from_utf8(
compile_module(&module, options).expect("imported ReadonlySet field compiles"),
)
.expect("LLVM IR is UTF-8")
}

fn method_ir<'a>(ir: &'a str, owner: &str, method: &str) -> &'a str {
let suffix = format!("__{owner}__{method}(");
let suffix_start = ir.find(&suffix).expect("requested method is present");
let start = ir[..suffix_start]
.rfind("define double @perry_method_")
.expect("requested method has a definition");
let method_and_rest = &ir[start..];
let end = method_and_rest
.find("\n}\n")
.expect("requested method has a closing brace");
&method_and_rest[..end + 3]
}

#[test]
fn readonly_set_field_has_uses_branded_collection_fast_path() {
let ir = compile_has_ir();
let method_ir = method_ir(&ir, "Archetype", "hasComponent");

assert!(
method_ir.contains("call double @js_readonly_set_has("),
"ReadonlySet.has must use the branded native-Set fast path with a structural-object fallback:\n{method_ir}"
);
assert!(
!method_ir.contains("call double @js_typed_feedback_native_call_method_by_id("),
"the common native-Set case must not enter the full generic dispatch tower:\n{method_ir}"
);
}

#[test]
fn nullable_class_receiver_readonly_set_field_uses_branded_fast_path() {
let ir = compile_has_ir();
let method_ir = method_ir(&ir, "CommandExecutor", "contains");

assert!(
method_ir.contains("call double @js_readonly_set_has("),
"a ReadonlySet field reached through `Archetype | undefined` must retain the branded candidate:\n{method_ir}"
);
assert!(
!method_ir.contains("call double @js_typed_feedback_native_call_method_by_id("),
"the nullable owner type must not force every native Set through generic method dispatch:\n{method_ir}"
);
}

#[test]
fn imported_class_readonly_set_field_uses_branded_fast_path() {
let ir = compile_imported_has_ir();
let method_ir = method_ir(&ir, "CommandExecutor", "contains");

assert!(
method_ir.contains("call double @js_readonly_set_has("),
"an imported class's published ReadonlySet field type must remain a branded candidate:\n{method_ir}"
);
assert!(
!method_ir.contains("call double @js_typed_feedback_native_call_method_by_id("),
"cross-module field metadata must not force native Sets through generic dispatch:\n{method_ir}"
);
}
19 changes: 18 additions & 1 deletion crates/perry-codegen/src/lower_call/property_get/map_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ use perry_hir::Expr;
use crate::expr::{lower_expr, unbox_to_i64, FnCtx};
use crate::nanbox::double_literal;
use crate::rooting;
use crate::type_analysis::{is_map_expr, is_set_expr, is_url_search_params_expr};
use crate::type_analysis::{
is_map_expr, is_readonly_set_expr, is_set_expr, is_url_search_params_expr,
};
use crate::types::{DOUBLE, I64};

/// Map/Set methods on PropertyGet receivers. The HIR only folds
Expand All @@ -42,6 +44,21 @@ pub(crate) fn try_lower_map_set_methods(
property: &str,
args: &[Expr],
) -> Result<Option<String>> {
// `ReadonlySet<T>` is a structural interface, not a native-layout proof.
// The runtime helper brand-checks the overwhelmingly common genuine Set
// and otherwise preserves JavaScript dispatch (custom interface objects,
// proxies, and Set subclasses with overrides).
if is_readonly_set_expr(ctx, object) && property == "has" && args.len() == 1 {
return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| {
let receiver = vals[0].clone();
let value = vals[1].clone();
Ok(Some(ctx.block().call(
DOUBLE,
"js_readonly_set_has",
&[(DOUBLE, &receiver), (DOUBLE, &value)],
)))
});
}
if is_map_expr(ctx, object) {
match property {
"set" if args.len() == 2 => {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_set_add_f32", I64, &[I64, F32]);
module.declare_function("js_set_add_bool", I64, &[I64, I32]);
module.declare_function("js_set_has", I32, &[I64, DOUBLE]);
module.declare_function("js_readonly_set_has", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_set_has_string", I32, &[I64, I64]);
module.declare_function("js_set_has_number", I32, &[I64, DOUBLE]);
module.declare_function("js_set_has_i32", I32, &[I64, I32]);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/type_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub(crate) use refine::{
};
pub(crate) use strings::{
class_name_extends_url_search_params, is_declared_string_expr, is_definitely_string_expr,
is_map_expr, is_set_expr, is_string_expr, is_url_search_params_expr,
is_map_expr, is_readonly_set_expr, is_set_expr, is_string_expr, is_url_search_params_expr,
is_url_search_params_subclass_expr, map_static_type_args, set_static_type_args,
string_proof_is_declared_only, string_value_is_runtime_guaranteed,
};
Expand Down
Loading