Skip to content

Commit 2052a52

Browse files
author
Ralph Küpper
committed
perf(codegen): specialize undefined loop filters
Lands #8740. Versions eligible instance methods with one private exact-`undefined` body when an immutable optional parameter guards work inside a loop, retaining the public boxed ABI and branching on the live argument's exact TAG_UNDEFINED bits. All other values run the unchanged generic body. The TypeScript optional annotation only nominates a candidate and is never consumed as a runtime proof: the private clone receives `Type::Void` only behind the public wrapper's live bit-compare, which is emitted as `icmp_eq(I64, &arg_bits, TAG_UNDEFINED_I64)` and pinned by a test. Candidate discovery rejects async/generator methods, rest and `arguments` parameters, every user-authored parameter write, and closure capture. The linkage interaction with #8731 is resolved. #8731 narrowed the module-local condition from `is_pshape_clone` to `ptr_array_cache_clone` because plain `$pshape` clones became producer-published capabilities needing external linkage. Undefined-filter candidacy now excludes index and array-cache clones rather than forcing itself module-local, three `debug_assert!` invariants pin that a guarded-undefined clone is never also one of those, and a new test asserts the pshape family's guard wrapper stays a published capability carrying both `$undef0` and `$generic`. One fix on top: `pshape_symbol_reachability` scans the source tree for `$pshape` fragments outside a 7-entry allowlist, so that the clone symbol can never reach a runtime vtable. The PR's new `guarded_undefined_method_tests.rs` names the fragment in its wrapper assertions, exactly as the two test files already on that list do. It is allowlisted with a rationale; the gate's emission-site coverage is unchanged. No version bump.
1 parent 32f0eac commit 2052a52

16 files changed

Lines changed: 1032 additions & 281 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Versioned eligible instance methods for an exact `undefined` optional argument.
2+
The public boxed-ABI wrapper validates the live argument bits once and sends only
3+
that value to a private specialized body; functions and every other falsey or
4+
non-callable value retain the ordinary JavaScript path. Mutation, closure capture,
5+
async/generator bodies, and oversized methods are conservatively excluded.
6+
7+
This removes the per-entity optional-filter truthiness and callback arm from
8+
codehz/ecs's 10k-entity accumulation loop. On an Apple M1 Mac mini, 11
9+
alternating process pairs measured 0.179437 ms versus 0.195817 ms on the exact
10+
parent, an 8.376% median paired improvement with 11/11 wins and all output
11+
oracles passing.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Borrowed inputs for the module artifact-emission phase.
2+
3+
use std::collections::{HashMap, HashSet};
4+
5+
use perry_hir::Module as HirModule;
6+
7+
use crate::module::LlModule;
8+
use crate::strings::StringPool;
9+
10+
use super::opts::CrossModuleCtx;
11+
12+
/// Read-only view of the `CompileOptions` fields that artifact emission still
13+
/// references after the pipeline has moved other fields into `CrossModuleCtx`.
14+
pub(super) struct OptsView<'a> {
15+
pub(super) import_function_prefixes: &'a HashMap<String, String>,
16+
pub(super) imported_classes: &'a [super::opts::ImportedClass],
17+
pub(super) is_entry_module: bool,
18+
pub(super) non_entry_module_prefixes: &'a [String],
19+
pub(super) output_type: &'a str,
20+
}
21+
22+
/// Data computed by the `compile_module` prelude and borrowed by the artifact
23+
/// tail. Keeping it together avoids a second oversized compiler entry module.
24+
pub(super) struct ModuleArtifactsCtx<'a> {
25+
pub progress: &'a super::CompileProgress,
26+
pub llmod: &'a mut LlModule,
27+
pub target_triple: &'a str,
28+
pub strings: &'a mut StringPool,
29+
pub hir: &'a HirModule,
30+
pub import_function_prefixes: &'a HashMap<String, String>,
31+
pub imported_classes: &'a [super::opts::ImportedClass],
32+
pub is_entry_module: bool,
33+
pub non_entry_module_prefixes: &'a [String],
34+
pub output_type: &'a str,
35+
pub module_prefix: &'a String,
36+
pub class_table: &'a HashMap<String, &'a perry_hir::Class>,
37+
pub class_ids: &'a HashMap<String, u32>,
38+
pub enum_table: &'a HashMap<(String, String), perry_hir::EnumValue>,
39+
pub module_globals: &'a HashMap<u32, String>,
40+
pub module_global_types: &'a HashMap<u32, perry_hir::types::Type>,
41+
pub static_field_globals: &'a HashMap<(String, String), String>,
42+
pub method_names: &'a HashMap<(String, String), String>,
43+
pub func_names: &'a HashMap<u32, String>,
44+
pub func_signatures: &'a HashMap<u32, (usize, bool, bool, bool)>,
45+
pub func_synthetic_arguments: &'a HashSet<u32>,
46+
pub module_boxed_vars: &'a HashSet<u32>,
47+
/// Typed-ABI capture oracle: module-wide local types minus boxed ids.
48+
pub module_local_types: &'a HashMap<u32, perry_hir::types::Type>,
49+
/// Source-type metadata for closure receivers; not a representation proof.
50+
pub module_receiver_types: &'a HashMap<u32, perry_hir::types::Type>,
51+
pub closure_rest_params: &'a HashMap<u32, usize>,
52+
pub closure_synthetic_arguments: &'a HashSet<u32>,
53+
pub closure_rest_and_arguments: &'a HashSet<u32>,
54+
pub closure_arities: &'a HashMap<u32, u32>,
55+
pub closure_lengths: &'a HashMap<u32, u32>,
56+
pub closure_arrow_functions: &'a HashSet<u32>,
57+
pub trusted_box_closures: &'a HashMap<u32, super::closure_collect::TrustedBoxClosure>,
58+
pub closures: &'a [(perry_hir::types::FuncId, perry_hir::Expr)],
59+
pub class_keys_init_data: &'a [(String, String, u32, Vec<u64>, Vec<u64>)],
60+
/// Keys global to `(class id, packed GcHeader word)` for inline `new`.
61+
pub class_header_image_inits: &'a HashMap<String, (u32, u64)>,
62+
pub imported_class_stubs: &'a [perry_hir::Class],
63+
pub cross_module: &'a CrossModuleCtx,
64+
}

crates/perry-codegen/src/codegen/artifacts.rs

Lines changed: 84 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ use std::collections::{HashMap, HashSet};
66
use std::time::Instant;
77

88
use anyhow::{Context, Result};
9-
use perry_hir::Module as HirModule;
109

11-
use crate::module::LlModule;
12-
use crate::strings::StringPool;
1310
use crate::types::{LlvmType, DOUBLE, I64, VOID};
1411

12+
use super::artifact_context::{ModuleArtifactsCtx, OptsView};
1513
use super::closure::{
1614
compile_closure, compile_typed_f64_closure, compile_typed_i1_closure,
1715
compile_typed_i32_closure, compile_typed_string_closure,
@@ -27,78 +25,9 @@ use super::method::{
2725
compile_typed_string_method,
2826
};
2927
use super::native_namespace_exports::emit_native_namespace_reexport_getters;
30-
use super::opts::CrossModuleCtx;
3128
use super::spec_function_length;
32-
use super::typed_abi::TypedFunctionTrampolineKind;
33-
34-
/// Read-only view of the `CompileOptions` fields that the artifact
35-
/// emission step references via `opts.X`. Bundled into a struct so the
36-
/// moved block (originally written against `let opts = …;` of type
37-
/// `CompileOptions`) can keep its `opts.X` syntax without holding a
38-
/// `&CompileOptions` borrow — that borrow is unavailable at the call
39-
/// site, because `compile_module`'s prelude moves several `opts`
40-
/// fields into `CrossModuleCtx` before invoking this function.
41-
struct OptsView<'a> {
42-
import_function_prefixes: &'a std::collections::HashMap<String, String>,
43-
imported_classes: &'a [super::opts::ImportedClass],
44-
is_entry_module: bool,
45-
non_entry_module_prefixes: &'a [String],
46-
output_type: &'a str,
47-
}
4829
use super::string_pool::emit_string_pool;
49-
50-
/// All the data computed by the prelude of `compile_module` that the
51-
/// tail half (this file) needs. Bundled so the call from
52-
/// `compile_module` stays a single line; field names mirror the
53-
/// in-prelude local names so the moved block reads unchanged once
54-
/// destructured.
55-
pub(super) struct ModuleArtifactsCtx<'a> {
56-
pub progress: &'a super::CompileProgress,
57-
pub llmod: &'a mut LlModule,
58-
pub target_triple: &'a str,
59-
pub strings: &'a mut StringPool,
60-
pub hir: &'a HirModule,
61-
pub import_function_prefixes: &'a std::collections::HashMap<String, String>,
62-
pub imported_classes: &'a [super::opts::ImportedClass],
63-
pub is_entry_module: bool,
64-
pub non_entry_module_prefixes: &'a [String],
65-
pub output_type: &'a str,
66-
pub module_prefix: &'a String,
67-
pub class_table: &'a HashMap<String, &'a perry_hir::Class>,
68-
pub class_ids: &'a HashMap<String, u32>,
69-
pub enum_table: &'a HashMap<(String, String), perry_hir::EnumValue>,
70-
pub module_globals: &'a HashMap<u32, String>,
71-
pub module_global_types: &'a HashMap<u32, perry_hir::types::Type>,
72-
pub static_field_globals: &'a HashMap<(String, String), String>,
73-
pub method_names: &'a HashMap<(String, String), String>,
74-
pub func_names: &'a HashMap<u32, String>,
75-
pub func_signatures: &'a HashMap<u32, (usize, bool, bool, bool)>,
76-
pub func_synthetic_arguments: &'a std::collections::HashSet<u32>,
77-
pub module_boxed_vars: &'a std::collections::HashSet<u32>,
78-
/// Typed-ABI capture-representation oracle: module-wide `Stmt::Let` types
79-
/// MINUS boxed ids (#5869). Only the typed closure clones read this.
80-
pub module_local_types: &'a HashMap<u32, perry_hir::types::Type>,
81-
/// #6369: receiver-type oracle for closure bodies — the same module-wide
82-
/// `Stmt::Let` types with no representation filtering, mirroring the
83-
/// `module_global_types` seed that `compile_function` / `compile_method`
84-
/// already use. Feeds `FnCtx.local_types` only.
85-
pub module_receiver_types: &'a HashMap<u32, perry_hir::types::Type>,
86-
pub closure_rest_params: &'a HashMap<u32, usize>,
87-
pub closure_synthetic_arguments: &'a std::collections::HashSet<u32>,
88-
pub closure_rest_and_arguments: &'a std::collections::HashSet<u32>,
89-
pub closure_arities: &'a HashMap<u32, u32>,
90-
pub closure_lengths: &'a HashMap<u32, u32>,
91-
pub closure_arrow_functions: &'a std::collections::HashSet<u32>,
92-
pub trusted_box_closures:
93-
&'a std::collections::HashMap<u32, super::closure_collect::TrustedBoxClosure>,
94-
pub closures: &'a [(perry_hir::types::FuncId, perry_hir::Expr)],
95-
pub class_keys_init_data: &'a [(String, String, u32, Vec<u64>, Vec<u64>)],
96-
/// #8122: keys global → (class id, packed GcHeader word) for the classes
97-
/// whose inline-`new` header image module init must compose.
98-
pub class_header_image_inits: &'a std::collections::HashMap<String, (u32, u64)>,
99-
pub imported_class_stubs: &'a [perry_hir::Class],
100-
pub cross_module: &'a CrossModuleCtx,
101-
}
30+
use super::typed_abi::TypedFunctionTrampolineKind;
10231

10332
/// Emit the artifact tail: bodies, wrappers, namespace globals, entry
10433
/// function, string pool. Mirrors the in-prelude execution order of
@@ -393,6 +322,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
393322
None,
394323
Some(nonnegative_index_params),
395324
false,
325+
false,
396326
)
397327
.with_context(|| {
398328
format!(
@@ -427,8 +357,46 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
427357
None,
428358
None,
429359
false,
360+
false,
430361
)
431362
.with_context(|| format!("lowering method '{}::{}'", class.name, method.name))?;
363+
if cross_module
364+
.guarded_undefined_method_params
365+
.contains_key(&(class.name.clone(), method.name.clone()))
366+
{
367+
compile_method(
368+
llmod,
369+
class,
370+
method,
371+
func_names,
372+
strings,
373+
class_table,
374+
method_names,
375+
module_globals,
376+
module_global_types,
377+
opts.import_function_prefixes,
378+
enum_table,
379+
static_field_globals,
380+
class_ids,
381+
func_signatures,
382+
func_synthetic_arguments,
383+
module_boxed_vars,
384+
closure_rest_params,
385+
cross_module,
386+
None,
387+
false,
388+
None,
389+
None,
390+
false,
391+
true,
392+
)
393+
.with_context(|| {
394+
format!(
395+
"lowering exact-undefined clone of method '{}::{}'",
396+
class.name, method.name
397+
)
398+
})?;
399+
}
432400
// Representation-selection Phase 5a: the additive `internal`
433401
// proven-`this` clone. Same HIR, same ABI, same shadow-bound
434402
// tagged-at-rest receiver slot — only `this.field` lowering
@@ -464,13 +432,51 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
464432
Some(fact.clone()),
465433
None,
466434
false,
435+
false,
467436
)
468437
.with_context(|| {
469438
format!(
470439
"lowering proven-`this` clone of method '{}::{}'",
471440
class.name, method.name
472441
)
473442
})?;
443+
if cross_module
444+
.guarded_undefined_method_params
445+
.contains_key(&(class.name.clone(), method.name.clone()))
446+
{
447+
compile_method(
448+
llmod,
449+
class,
450+
method,
451+
func_names,
452+
strings,
453+
class_table,
454+
method_names,
455+
module_globals,
456+
module_global_types,
457+
opts.import_function_prefixes,
458+
enum_table,
459+
static_field_globals,
460+
class_ids,
461+
func_signatures,
462+
func_synthetic_arguments,
463+
module_boxed_vars,
464+
closure_rest_params,
465+
cross_module,
466+
None,
467+
false,
468+
Some(fact.clone()),
469+
None,
470+
false,
471+
true,
472+
)
473+
.with_context(|| {
474+
format!(
475+
"lowering proven-`this` exact-undefined clone of method '{}::{}'",
476+
class.name, method.name
477+
)
478+
})?;
479+
}
474480

475481
// #8607: a second, stricter clone for the Phase 3b
476482
// provenance+containment route. Its synthetic immutable
@@ -505,6 +511,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
505511
Some(fact.clone()),
506512
None,
507513
true,
514+
false,
508515
)
509516
.with_context(|| {
510517
format!(
@@ -544,6 +551,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
544551
None,
545552
None,
546553
false,
554+
false,
547555
)
548556
.with_context(|| {
549557
format!(
@@ -611,6 +619,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
611619
None,
612620
None,
613621
false,
622+
false,
614623
)
615624
.with_context(|| format!("lowering getter '{}::{}'", class.name, prop))?;
616625
}
@@ -666,6 +675,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
666675
None,
667676
None,
668677
false,
678+
false,
669679
)
670680
.with_context(|| format!("lowering setter '{}::{}'", class.name, prop))?;
671681
}
@@ -763,6 +773,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
763773
None,
764774
None,
765775
false,
776+
false,
766777
)
767778
.with_context(|| format!("lowering constructor for '{}'", class.name))?;
768779
}

crates/perry-codegen/src/codegen/closure_collect.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ fn count_stmt_nodes(stmt: &perry_hir::Stmt) -> usize {
241241
count
242242
}
243243

244+
pub(super) fn count_body_nodes(body: &[perry_hir::Stmt]) -> usize {
245+
body.iter().map(count_stmt_nodes).sum()
246+
}
247+
244248
pub(crate) fn select_trusted_box_closures(
245249
closures: &[(perry_hir::types::FuncId, perry_hir::Expr)],
246250
direct_call_closures: &std::collections::HashSet<u32>,
@@ -298,7 +302,7 @@ pub(crate) fn select_trusted_box_closures(
298302
if boxed_capture_mask == 0 {
299303
return None;
300304
}
301-
let cost = body.iter().map(count_stmt_nodes).sum::<usize>();
305+
let cost = count_body_nodes(body);
302306
(cost <= MAX_TRUSTED_BOX_CLONE_NODES).then_some((
303307
cost,
304308
*func_id,

0 commit comments

Comments
 (0)