Skip to content

Commit c381e3e

Browse files
committed
Carry Atom in PhpType::Named
1 parent 8ed83dc commit c381e3e

68 files changed

Lines changed: 787 additions & 790 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/todo/eager-resolution.md

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -349,28 +349,44 @@ The problem is needing them for every runtime analysis thread.
349349
- No stack overflows on the full test suite or the largest available
350350
project.
351351

352-
#### Phase 4g: Carry `Atom` in `PhpType::Named`
353-
354-
**Impact: Low-Medium. Effort: Medium.**
355-
356-
Deferred from Phase 4d. With the FQN now cached, the residual
357-
per-substitution string cost is `PhpType::Named(cls.fqn().to_string())`
358-
(and the equivalent patterns in `template_subs.rs`, `return_types.rs`,
359-
`inheritance/mod.rs`, `forward_walk/scope_state.rs`,
360-
`rhs_resolution/mod.rs`, and the Laravel builder providers). Each of
361-
these turns a cached `Atom` back into an owned `String` because
362-
`PhpType::Named` holds a `String`.
363-
364-
Changing `PhpType::Named` (and the analogous per-member cache keys such
365-
as `format!("{}::{}", class_fqn, method.name)` in `target_cache.rs` /
366-
`property_access.rs`) to carry `Atom` would let the resolution pipeline
367-
thread interned names end-to-end without re-allocating. This touches a
368-
core enum used throughout the type engine, so it is a standalone
369-
refactor rather than part of the 4d fqn-cache change.
352+
#### Phase 4g: Carry `Atom` in `PhpType::Named`
353+
354+
Changed `PhpType::Named` to hold an `Atom` (interned string) instead of
355+
a `String`. `Named` values are drawn from a bounded set (class names,
356+
keywords, template parameters, `$this`) and are compared and cloned
357+
throughout the substitution hot path, so pointer-sized equality and
358+
free (`Copy`) cloning pay off; literal scalar values live in
359+
`PhpType::Literal`, so the interner is never fed unbounded free text.
360+
The substitution sites the FQN cache (Phase 4d) fed back into a fresh
361+
`String``PhpType::Named(cls.fqn().to_string())` in `template_subs.rs`,
362+
`return_types.rs`, `inheritance/mod.rs`, `rhs_resolution/mod.rs`, and the
363+
Laravel providers — now thread the cached `Atom` end-to-end
364+
(`PhpType::Named(cls.fqn())`) with no reallocation. `Atom`'s
365+
`Deref<Target = str>` means the ~160 read sites (comparisons, keyword
366+
checks, `base_name` extraction) were unaffected; only genuine
367+
`String`-typed sinks (`Vec<String>` keys, `Generic`'s `String` field)
368+
needed an explicit `.to_string()`.
369+
370+
#### Phase 4h: Intern composite per-member cache keys
371+
372+
**Impact: Low. Effort: Low.**
373+
374+
`target_cache.rs` (`BODY_INFER_MEMO`, the re-entry set) and
375+
`property_access.rs` key thread-local caches on
376+
`format!("{}::{}", class_fqn, method)`. These allocate a `String` per
377+
lookup. Interning them into a single `Atom` key would remove the
378+
allocation, but the key space is the `FQN × member` cartesian product,
379+
which is effectively unbounded — feeding it to the global `ustr`
380+
interner would leak one entry per distinct pair for the process
381+
lifetime (the exact "constructed once, never compared" case
382+
`atom.rs` warns against). Any fix here should therefore use a
383+
composite `(Atom, Atom)` tuple key (both halves already interned,
384+
no new interner entries) rather than interning the joined string.
370385

371386
**Success criteria:**
372-
- `PhpType::Named` carries an interned name (no `String`
373-
reallocation on the substitution hot path).
387+
- The `"FQN::member"` cache keys no longer allocate a `String` per
388+
lookup.
389+
- No net growth in the global interner.
374390
- No test regressions.
375391

376392
---

src/class_lookup.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! in [`crate::resolution`]; this module is the shared kernel that
88
//! resolution and the completion/diagnostics pipelines both call into.
99
10+
use crate::atom::atom;
1011
use std::sync::Arc;
1112

1213
use crate::types::ClassInfo;
@@ -263,7 +264,7 @@ pub(crate) fn is_subtype_of(
263264
/// class names instead of pre-constructed [`crate::php_type::PhpType`] values.
264265
///
265266
/// This avoids the boilerplate of wrapping each name in
266-
/// `PhpType::Named(name.to_string())` at call sites that already have
267+
/// `PhpType::Named(atom(name.as_ref()))` at call sites that already have
267268
/// `&str` class names.
268269
pub(crate) fn is_subtype_of_names(
269270
subtype_name: &str,
@@ -272,8 +273,8 @@ pub(crate) fn is_subtype_of_names(
272273
) -> bool {
273274
use crate::php_type::PhpType;
274275
is_subtype_of_typed(
275-
&PhpType::Named(subtype_name.to_string()),
276-
&PhpType::Named(supertype_name.to_string()),
276+
&PhpType::Named(atom(subtype_name)),
277+
&PhpType::Named(atom(supertype_name)),
277278
class_loader,
278279
)
279280
}
@@ -287,11 +288,7 @@ pub(crate) fn is_subtype_of_named(
287288
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
288289
) -> bool {
289290
use crate::php_type::PhpType;
290-
is_subtype_of_typed(
291-
subtype,
292-
&PhpType::Named(supertype_name.to_string()),
293-
class_loader,
294-
)
291+
is_subtype_of_typed(subtype, &PhpType::Named(atom(supertype_name)), class_loader)
295292
}
296293

297294
/// Check whether `subtype` is a subtype of `supertype`, combining
@@ -437,7 +434,7 @@ pub(crate) fn is_subtype_of_typed(
437434
// name is a class-string of some object.
438435
if let (PhpType::ClassString(sub_inner), PhpType::ClassString(sup_inner)) = (subtype, supertype)
439436
{
440-
let object_bound = PhpType::Named("object".to_string());
437+
let object_bound = PhpType::Named(atom("object"));
441438
let sub = sub_inner.as_deref().unwrap_or(&object_bound);
442439
let sup = sup_inner.as_deref().unwrap_or(&object_bound);
443440
return is_subtype_of_typed(sub, sup, class_loader);

src/completion/phpdoc/generation/build.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//! from a classified declaration, plus the type-enrichment helpers
33
//! that decide when a native type hint needs a PHPDoc tag at all.
44
5+
use crate::atom::atom;
56
use std::collections::HashMap;
67
use std::sync::Arc;
78

@@ -209,7 +210,7 @@ pub(crate) fn enrichment_plain_typed(
209210
let args: Vec<PhpType> = cls
210211
.template_params
211212
.iter()
212-
.map(|s| PhpType::Named(s.to_string()))
213+
.map(|s| PhpType::Named(atom(s.as_ref())))
213214
.collect();
214215
return Some(PhpType::Generic(name.to_string(), args));
215216
}

src/completion/source/helpers.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
/// All functions in this module are free functions (not methods on
2121
/// `Backend`). Cross-module dependencies that previously used `Self::`
2222
/// are called via their canonical module paths.
23+
use crate::atom::atom;
2324
use std::sync::Arc;
2425

2526
use crate::php_type::PhpType;
@@ -508,8 +509,8 @@ fn infer_type_from_simple_expr(expr: &str) -> Option<PhpType> {
508509
"int" | "integer" => return Some(PhpType::int()),
509510
"float" | "double" | "real" => return Some(PhpType::float()),
510511
"bool" | "boolean" => return Some(PhpType::bool()),
511-
"array" => return Some(PhpType::Named("array".to_string())),
512-
"object" => return Some(PhpType::Named("object".to_string())),
512+
"array" => return Some(PhpType::Named(atom("array"))),
513+
"object" => return Some(PhpType::Named(atom("object"))),
513514
_ => {} // might be a parenthesized expression, not a cast
514515
}
515516
}
@@ -722,13 +723,10 @@ pub(crate) fn resolve_first_class_callable_return_type(
722723
if classes.is_empty() {
723724
None
724725
} else if classes.len() == 1 {
725-
Some(PhpType::Named(classes[0].fqn().to_string()))
726+
Some(PhpType::Named(classes[0].fqn()))
726727
} else {
727728
Some(PhpType::Union(
728-
classes
729-
.iter()
730-
.map(|c| PhpType::Named(c.fqn().to_string()))
731-
.collect(),
729+
classes.iter().map(|c| PhpType::Named(c.fqn())).collect(),
732730
))
733731
}
734732
})

src/completion/source/throws_analysis/cross_file.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//! - `new ClassName(…)` — the class is loaded and the constructor's
1616
//! `@throws` tags are propagated.
1717
18+
use crate::atom::atom;
1819
use std::collections::HashMap;
1920
use std::sync::Arc;
2021

@@ -194,7 +195,7 @@ pub(crate) fn find_uncaught_throw_types_with_context(
194195
} else {
195196
resolved
196197
};
197-
Some(PhpType::Named(fqn))
198+
Some(PhpType::Named(atom(&fqn)))
198199
}
199200
}
200201

@@ -443,15 +444,12 @@ pub(crate) fn parse_param_type_map(signature: &str) -> Vec<(String, PhpType)> {
443444
let parsed = PhpType::parse(type_token);
444445
let non_null = parsed.non_null_type().unwrap_or_else(|| parsed.clone());
445446
if let Some(name) = non_null.base_name() {
446-
result.push((var_name.to_string(), PhpType::Named(name.to_string())));
447+
result.push((var_name.to_string(), PhpType::Named(atom(name))));
447448
} else {
448449
// Fallback for scalars and other non-class types.
449450
let cleaned_type = type_token.trim_start_matches('?').trim_start_matches('\\');
450451
if !cleaned_type.is_empty() {
451-
result.push((
452-
var_name.to_string(),
453-
PhpType::Named(cleaned_type.to_string()),
454-
));
452+
result.push((var_name.to_string(), PhpType::Named(atom(cleaned_type))));
455453
}
456454
}
457455
}
@@ -482,11 +480,11 @@ pub(crate) fn find_cross_file_propagated_throws(
482480
.into_iter()
483481
.map(|(name, ty)| {
484482
let resolved = if let Some(base) = ty.base_name() {
485-
PhpType::Named(crate::util::resolve_to_fqn(
483+
PhpType::Named(atom(&crate::util::resolve_to_fqn(
486484
base,
487485
ctx.use_map,
488486
ctx.file_namespace,
489-
))
487+
)))
490488
} else {
491489
ty
492490
};

src/completion/source/throws_analysis/scanning.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! - Look up a method's `@throws` tags from its docblock
1111
//! - Extract the function body following a docblock
1212
13+
use crate::atom::atom;
1314
use crate::completion::source::comment_position::position_to_byte_offset;
1415
use crate::php_type::PhpType;
1516
use crate::text_scan::{
@@ -80,7 +81,7 @@ pub(crate) fn find_throw_statements(body: &str) -> Vec<ThrowInfo> {
8081
let type_name = &after_new[..type_end];
8182
if !type_name.is_empty() {
8283
results.push(ThrowInfo {
83-
type_name: PhpType::Named(type_name.to_string()),
84+
type_name: PhpType::Named(atom(type_name)),
8485
offset: pos,
8586
});
8687
}
@@ -298,7 +299,7 @@ pub(crate) fn find_inline_throws_annotations(body: &str) -> Vec<ThrowInfo> {
298299
.trim_end_matches('/');
299300
if !clean.is_empty() && !clean.starts_with('$') {
300301
results.push(ThrowInfo {
301-
type_name: PhpType::Named(clean.to_string()),
302+
type_name: PhpType::Named(atom(clean)),
302303
offset: doc_start,
303304
});
304305
}
@@ -357,7 +358,7 @@ pub(crate) fn find_method_return_type(file_content: &str, method_name: &str) ->
357358
let parsed = PhpType::parse(type_str);
358359
let non_null = parsed.non_null_type().unwrap_or_else(|| parsed.clone());
359360
if let Some(name) = non_null.base_name() {
360-
return Some(PhpType::Named(name.to_string()));
361+
return Some(PhpType::Named(atom(name)));
361362
}
362363
}
363364
}
@@ -381,7 +382,7 @@ pub(crate) fn find_method_return_type(file_content: &str, method_name: &str) ->
381382
&& !parsed.is_mixed()
382383
&& !parsed.is_self_like()
383384
{
384-
return Some(PhpType::Named(name.to_string()));
385+
return Some(PhpType::Named(atom(name)));
385386
}
386387
}
387388
}
@@ -434,7 +435,7 @@ pub(crate) fn find_method_throws_tags(file_content: &str, method_name: &str) ->
434435
.trim_end_matches('*')
435436
.trim_start_matches('\\');
436437
if !clean.is_empty() {
437-
throws.push(PhpType::Named(clean.to_string()));
438+
throws.push(PhpType::Named(atom(clean)));
438439
}
439440
}
440441
}

0 commit comments

Comments
 (0)