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
128 changes: 102 additions & 26 deletions crates/perry-hir/src/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ impl LoweringContext {
classes_index: HashMap::new(),
imported_functions_index: HashMap::new(),
builtin_module_aliases_index: HashMap::new(),
native_instances_index: HashMap::new(),
module_native_instances_index: HashMap::new(),
func_return_native_instances_index: HashMap::new(),
native_modules_index: HashMap::new(),
class_statics_index: HashMap::new(),
weakref_locals: HashSet::new(),
finreg_locals: HashSet::new(),
weakmap_locals: HashSet::new(),
Expand Down Expand Up @@ -156,6 +161,7 @@ impl LoweringContext {
optional_require_try_depth: 0,
fn_ctor_env: super::fn_ctor_env::FnCtorEnv::default(),
expr_lower_depth: 0,
prelowered_member_receiver: None,
}
}

Expand Down Expand Up @@ -620,23 +626,26 @@ impl LoweringContext {
static_fields: Vec<String>,
static_methods: Vec<String>,
) {
// Forward-scan (first-match-wins): index keeps the FIRST entry per name.
let idx = self.class_statics.len();
self.class_statics_index
.entry(class_name.clone())
.or_insert(idx);
self.class_statics
.push((class_name, static_fields, static_methods));
}

pub(crate) fn has_static_field(&self, class_name: &str, field_name: &str) -> bool {
self.class_statics
.iter()
.find(|(cn, _, _)| cn == class_name)
.map(|(_, fields, _)| fields.contains(&field_name.to_string()))
self.class_statics_index
.get(class_name)
.map(|&idx| self.class_statics[idx].1.iter().any(|f| f == field_name))
.unwrap_or(false)
}

pub(crate) fn has_static_method(&self, class_name: &str, method_name: &str) -> bool {
self.class_statics
.iter()
.find(|(cn, _, _)| cn == class_name)
.map(|(_, _, methods)| methods.contains(&method_name.to_string()))
self.class_statics_index
.get(class_name)
.map(|&idx| self.class_statics[idx].2.iter().any(|m| m == method_name))
.unwrap_or(false)
}

Expand Down Expand Up @@ -1040,15 +1049,21 @@ impl LoweringContext {
module_name: String,
method_name: Option<String>,
) {
// Forward-scan (first-match-wins) semantics: only record the FIRST
// index for a name so lookups match the old `.iter().find()`.
let idx = self.native_modules.len();
self.native_modules_index
.entry(local_name.clone())
.or_insert(idx);
self.native_modules
.push((local_name, module_name, method_name));
}

pub(crate) fn lookup_native_module(&self, name: &str) -> Option<(&str, Option<&str>)> {
self.native_modules
.iter()
.find(|(n, _, _)| n == name)
.map(|(_, m, method)| (m.as_str(), method.as_ref().map(|s| s.as_str())))
self.native_modules_index.get(name).map(|&idx| {
let (_, m, method) = &self.native_modules[idx];
(m.as_str(), method.as_ref().map(|s| s.as_str()))
})
}

pub(crate) fn register_builtin_module_alias(
Expand Down Expand Up @@ -1103,10 +1118,39 @@ impl LoweringContext {
if is_compile_package_override(&module_name) {
return;
}
// Push the new index onto this name's shadow stack (innermost last).
let idx = self.native_instances.len();
self.native_instances_index
.entry(local_name.clone())
.or_default()
.push(idx);
self.native_instances
.push((local_name, module_name, class_name));
}

/// Truncate `native_instances` back to `mark`, keeping the
/// `native_instances_index` shadow stacks in sync: every recorded index
/// `>= mark` is popped (these belong to bindings whose scope is exiting),
/// re-exposing any earlier (outer-scope) binding of the same name. Empty
/// stacks are removed to keep the map small. Use this everywhere
/// `native_instances.truncate(..)` was previously called directly.
pub(crate) fn truncate_native_instances(&mut self, mark: usize) {
if self.native_instances.len() <= mark {
return;
}
self.native_instances.truncate(mark);
// Drop indices >= mark from each name's shadow stack, re-exposing any
// earlier (outer-scope) binding. The map is keyed by distinct
// native-instance names (bounded, not proportional to class count), so
// this stays cheap.
self.native_instances_index.retain(|_, stack| {
while stack.last().is_some_and(|&i| i >= mark) {
stack.pop();
}
!stack.is_empty()
});
}

/// #1483: resolve a parameter's declared type name to a perry/ui widget
/// class that uses handle-based instance dispatch (Canvas, State, ...).
/// Returns the canonical widget name (e.g. "Canvas") when `type_name`
Expand Down Expand Up @@ -1147,10 +1191,17 @@ impl LoweringContext {
// inner `res` always resolved to the outer `("http",
// "ServerResponse")` tag and `res.on('data')` misrouted
// through ServerResponse dispatch instead of IncomingMessage.)
self.native_instances
.iter()
.rev()
.find(|(n, _, _)| n == name)
//
// Indexed (#5271): `native_instances_index[name]` is the shadow stack
// of indices for this name, innermost (last) on top — so the top index
// is exactly the entry the old `.rev().find()` would have selected.
// The `exposes_plain_object_fields` filter is then applied to THAT
// entry only (matching `.find().filter()`, which never falls through to
// an earlier match when the top one is filtered out).
self.native_instances_index
.get(name)
.and_then(|stack| stack.last())
.map(|&idx| &self.native_instances[idx])
// `node:repl` constructors allocate real heap objects/errors with
// bound methods; routing them through handle-dispatch native
// getters turns ordinary fields like `Recoverable.err` into
Expand All @@ -1159,11 +1210,11 @@ impl LoweringContext {
.map(|(_, module, class)| (module.as_str(), class.as_str()))
.or_else(|| {
// Check module-level instances (survive scope exits).
// Same last-match-wins rule for consistency.
self.module_native_instances
.iter()
.rev()
.find(|(n, _, _)| n == name)
// Same last-match-wins rule for consistency — the index stores
// the LAST pushed entry per name.
self.module_native_instances_index
.get(name)
.map(|&idx| &self.module_native_instances[idx])
.filter(|(_, module, class)| !exposes_plain_object_fields(module, class))
.map(|(_, module, class)| (module.as_str(), class.as_str()))
})
Expand All @@ -1173,10 +1224,35 @@ impl LoweringContext {
&self,
func_name: &str,
) -> Option<(&str, &str)> {
self.func_return_native_instances
.iter()
.find(|(n, _, _)| n == func_name)
.map(|(_, module, class)| (module.as_str(), class.as_str()))
// Forward-scan (first-match-wins): index keeps the FIRST pushed entry.
self.func_return_native_instances_index
.get(func_name)
.map(|&idx| {
let (_, module, class) = &self.func_return_native_instances[idx];
(module.as_str(), class.as_str())
})
}

/// Push a function-return native instance (push-only, never truncated) and
/// update its perf index. `lookup_func_return_native_instance` scanned
/// FORWARD (first-match-wins), so the index keeps the FIRST pushed entry.
pub(crate) fn push_func_return_native_instance(&mut self, entry: (String, String, String)) {
let idx = self.func_return_native_instances.len();
self.func_return_native_instances_index
.entry(entry.0.clone())
.or_insert(idx);
self.func_return_native_instances.push(entry);
}

/// Push a module-level native instance (module-scoped, never truncated)
/// and update its perf index. `lookup_native_instance`'s fallback arm
/// scans these in reverse (last-match-wins), so the index stores the LAST
/// pushed entry per name (overwrite).
pub(crate) fn push_module_native_instance(&mut self, entry: (String, String, String)) {
let idx = self.module_native_instances.len();
self.module_native_instances_index
.insert(entry.0.clone(), idx);
self.module_native_instances.push(entry);
}
}

Expand Down Expand Up @@ -1259,7 +1335,7 @@ impl LoweringContext {
}
self.locals.extend(kept);
}
self.native_instances.truncate(mark.1);
self.truncate_native_instances(mark.1);
// Remove index entries for functions being truncated, then restore any
// earlier entries that were shadowed by the removed ones.
for i in mark.2..self.functions.len() {
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-hir/src/lower/expr_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pub(super) fn lower_assign(ctx: &mut LoweringContext, assign: &ast::AssignExpr)
_ => Some("Instance"),
};
if let Some(class_name) = class_name {
ctx.module_native_instances.push((
ctx.push_module_native_instance((
var_name.clone(),
module_name.to_string(),
class_name.to_string(),
Expand All @@ -217,7 +217,7 @@ pub(super) fn lower_assign(ctx: &mut LoweringContext, assign: &ast::AssignExpr)
module_name.clone(),
class_name_str.to_string(),
);
ctx.module_native_instances.push((
ctx.push_module_native_instance((
var_name.clone(),
module_name,
class_name_str.to_string(),
Expand All @@ -230,7 +230,7 @@ pub(super) fn lower_assign(ctx: &mut LoweringContext, assign: &ast::AssignExpr)
if let ast::Expr::Ident(rhs_ident) = inner_rhs {
let rhs_name = rhs_ident.sym.as_ref();
if let Some((module, class)) = ctx.lookup_native_instance(rhs_name) {
ctx.module_native_instances.push((
ctx.push_module_native_instance((
var_name,
module.to_string(),
class.to_string(),
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-hir/src/lower/expr_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,21 @@ pub(super) fn lower_call(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Res
// are likewise out of scope once we unwind past their owning
// call). `truncate` is a no-op when nothing was added.
if ctx.native_instances.len() > ni_mark {
ctx.native_instances.truncate(ni_mark);
ctx.truncate_native_instances(ni_mark);
}
result
}

fn lower_call_inner(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Result<Expr> {
// Safety net for the receiver-lowering memo (see
// `LoweringContext::prelowered_member_receiver`): the memo is set by
// `try_static_method_and_instance` only to be consumed by the immediately
// following fall-through tail's `lower_member_inner`. It is single-shot and
// span-keyed, but in case a code path sets it without the tail consuming it,
// drop any stale entry here so it can never leak across calls. This runs
// before the callee tail (which lowers a Member, not a Call), so it never
// clobbers an in-flight memo for the call currently being lowered.
ctx.prelowered_member_receiver = None;
// Check if any argument has spread
let has_spread = call.args.iter().any(|arg| arg.spread.is_some());

Expand Down
29 changes: 28 additions & 1 deletion crates/perry-hir/src/lower/expr_call/static_and_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

use super::stream::is_stream_api_method;
Expand Down Expand Up @@ -345,7 +346,27 @@ pub(super) fn try_static_method_and_instance(
if !may_lower_to_native_method_call(ctx, &member.obj) {
return Ok(Err(args));
}
// Lower the object expression first
// Lower the object expression once.
//
// Perf (O(n²)/exponential → O(n) on long native-fluent chains): this
// is a FULL recursive lowering of the entire receiver prefix, done
// speculatively to inspect whether the inner call lowered to a
// `NativeMethodCall` of a recognized fluent module. On a chain like
// `K.name(..).description(..).option(..)…` (commander / minified CLI
// builders) where `may_lower_to_native_method_call` over-approximates
// to `true` but the inner call actually lowers to a *generic* `Call`,
// every arm below misses, `object_expr` is discarded, and we return
// `Err(args)` — whereupon the `lower_call_inner` fall-through tail
// re-lowers the same member callee (and thus this whole prefix)
// again. Repeated per chain level that is exponential blowup.
//
// To make the tail reuse this lowering instead of redoing it, stash
// it keyed by the receiver's source span. `lower_member_inner` (the
// tail's receiver-lowering site) consumes it for the matching span.
// Reuse is sound: lowering a receiver is idempotent in the value it
// produces, and the fluent-success arms below already reuse this very
// `object_expr`.
let obj_span = member.obj.as_ref().span();
let object_expr = lower_expr(ctx, &member.obj)?;
// Check if it's a NativeMethodCall for a fluent-API native module
if let Expr::NativeMethodCall {
Expand Down Expand Up @@ -488,6 +509,12 @@ pub(super) fn try_static_method_and_instance(
}));
}
}
// No fluent arm matched: we are about to return `Err(args)` and the
// `lower_call_inner` fall-through tail will re-lower this same member
// callee. Hand it the receiver we just lowered so it doesn't repeat
// the (potentially whole-prefix) work. Keyed by span so the tail only
// reuses it for the exact receiver subtree.
ctx.prelowered_member_receiver = Some(((obj_span.lo.0, obj_span.hi.0), object_expr));
}
}

Expand Down
Loading
Loading