Skip to content

Commit 0722f88

Browse files
Reuse contextual template occurrence facts
1 parent 07cfdd7 commit 0722f88

4 files changed

Lines changed: 268 additions & 83 deletions

File tree

ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Project meaning. This is where observed Project Facts meet the parsed template,
8181

8282
Each Template Library contributes two independently backdatable semantic products: `LibraryTagSpecs`, which fuses extracted Tag Rules and Block Specs with builtin and configured fallback meaning, and `LibraryFilterSpecs`, which carries extracted Filter Arity. The products are keyed by Template Library identity, so changing one library or one fact category does not rebuild project-global semantic inventories.
8383

84-
For a project-backed Template, semantic analysis builds one tracked `TemplateAnalysisProjection`. A fixed-point loop resolves only Tag and Filter occurrences in the source, discovers effective loader occurrences by `TagRole`, and converges the ordered loaded-library state with a sparse occurrence grammar. The explicit projectless structure seam instead builds its sparse grammar and `TemplateTree` directly in one pass; it does not construct project-correlated Tag or Filter facts. The converged project-backed product correlates:
84+
For a project-backed Template, semantic analysis builds one tracked `TemplateAnalysisProjection`. A fixed-point loop resolves only Tag and Filter occurrences in the source, discovers effective loader occurrences by `TagRole`, and converges the ordered loaded-library state with a sparse occurrence grammar. Final fact collection resolves each Tag or Filter once per semantic `(visible load prefix, symbol name)` and reuses that contextual result across repeated occurrences, while retaining occurrence-specific structure, arguments, spans, and source identity. The explicit projectless structure seam instead builds its sparse grammar and `TemplateTree` directly in one pass; it does not construct project-correlated Tag or Filter facts. The converged project-backed product correlates:
8585

8686
- the `TemplateTree` and captured closing occurrences;
8787
- ordered Loaded Libraries;

crates/djls-semantic/src/scoping.rs

Lines changed: 162 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,55 @@ impl FilterOccurrenceKey {
5151
}
5252
}
5353

54+
/// Deduplicates contextual resolution without leaking cache identity into load scoping.
55+
/// A visible statement count fully identifies the semantic load prefix within one Template.
56+
#[derive(Debug)]
57+
struct ContextualFactCache<T> {
58+
by_load_prefix: BTreeMap<usize, BTreeMap<String, T>>,
59+
}
60+
61+
impl<T> Default for ContextualFactCache<T> {
62+
fn default() -> Self {
63+
Self {
64+
by_load_prefix: BTreeMap::new(),
65+
}
66+
}
67+
}
68+
69+
impl<T: Clone> ContextualFactCache<T> {
70+
fn resolve(
71+
&mut self,
72+
load_state: LoadState<'_>,
73+
symbol_name: &str,
74+
resolve: impl FnOnce() -> T,
75+
) -> T {
76+
let facts_by_name = self
77+
.by_load_prefix
78+
.entry(load_state.visible_statement_count())
79+
.or_default();
80+
if let Some(fact) = facts_by_name.get(symbol_name) {
81+
return fact.clone();
82+
}
83+
84+
let fact = resolve();
85+
facts_by_name.insert(symbol_name.to_string(), fact.clone());
86+
fact
87+
}
88+
}
89+
90+
#[derive(Clone, Debug, PartialEq, Eq)]
91+
struct ContextualTagFact {
92+
availability: SymbolAvailability,
93+
unknown_load_can_shadow: bool,
94+
}
95+
96+
#[derive(Clone, Debug, PartialEq)]
97+
struct ContextualFilterFact {
98+
availability: SymbolAvailability,
99+
arity: Option<FilterArity>,
100+
unknown_load_can_shadow: bool,
101+
}
102+
54103
#[derive(Clone, Debug, PartialEq, Eq)]
55104
pub(crate) struct LoaderArgumentFact {
56105
pub(crate) argument: crate::scoping::loads::LoadArgument,
@@ -176,6 +225,8 @@ pub(crate) fn template_analysis_projection_for_file_in_scope<'db>(
176225

177226
let mut tag_facts = BTreeMap::new();
178227
let mut filter_facts = BTreeMap::new();
228+
let mut tag_context_cache = ContextualFactCache::default();
229+
let mut filter_context_cache = ContextualFactCache::default();
179230
let mut load_cursor = loaded.cursor();
180231
for node in &active_nodes {
181232
match node {
@@ -185,20 +236,25 @@ pub(crate) fn template_analysis_projection_for_file_in_scope<'db>(
185236
};
186237
let spec = occurrence_spec(&grammar, *tag);
187238
let load_state = load_cursor.advance_to(tag.span.start());
188-
let availability = if project.is_none() {
189-
if grammar_fact.spec.is_some() {
190-
SymbolAvailability::Available
191-
} else {
192-
SymbolAvailability::Unknown
193-
}
194-
} else {
195-
resolve_occurrence_availability(
196-
environment,
197-
&load_state,
198-
tag.tag,
199-
TemplateSymbolKind::Tag,
200-
)
201-
};
239+
let contextual_fact =
240+
tag_context_cache.resolve(load_state, tag.tag, || ContextualTagFact {
241+
availability: if project.is_none() {
242+
if grammar_fact.spec.is_some() {
243+
SymbolAvailability::Available
244+
} else {
245+
SymbolAvailability::Unknown
246+
}
247+
} else {
248+
resolve_occurrence_availability(
249+
environment,
250+
&load_state,
251+
tag.tag,
252+
TemplateSymbolKind::Tag,
253+
)
254+
},
255+
unknown_load_can_shadow: load_state
256+
.unknown_load_can_shadow_symbol(tag.tag, environment),
257+
});
202258
let loader_arguments =
203259
if spec.and_then(TagSpec::role) == Some(TagRole::TemplateLibraryLoader) {
204260
LoadKind::from_loader_bits(tag.bits).map_or_else(Vec::new, |kind| {
@@ -220,7 +276,7 @@ pub(crate) fn template_analysis_projection_for_file_in_scope<'db>(
220276
TagOccurrenceKey::from_name_span(tag.name_span),
221277
ScopedTagFact {
222278
spec: spec.cloned(),
223-
availability,
279+
availability: contextual_fact.availability,
224280
structure_accepts_spelling: matches!(
225281
tag.structural_meaning,
226282
StructuralOccurrenceMeaning::CapturedIntermediate
@@ -229,57 +285,56 @@ pub(crate) fn template_analysis_projection_for_file_in_scope<'db>(
229285
grammar_fact.classification,
230286
TagClassification::Inconclusive
231287
),
232-
unknown_load_can_shadow: loaded
233-
.has_unknown_load_that_can_shadow_symbol_before(
234-
tag.span.start(),
235-
tag.tag,
236-
environment,
237-
),
288+
unknown_load_can_shadow: contextual_fact.unknown_load_can_shadow,
238289
loader_arguments,
239290
},
240291
);
241292
}
242293
ActiveTemplateNode::Variable(variable) => {
243294
let load_state = load_cursor.advance_to(variable.span.start());
244295
for filter in variable.filters {
245-
let (availability, arity) = if project.is_none() {
246-
let arity = db
247-
.projectless_filter_arity_specs()
248-
.get(&filter.name)
249-
.cloned();
250-
let availability = if arity.is_some() {
251-
SymbolAvailability::Available
252-
} else {
253-
SymbolAvailability::Unknown
254-
};
255-
(availability, arity)
256-
} else {
257-
(
258-
resolve_occurrence_availability(
259-
environment,
260-
&load_state,
261-
&filter.name,
262-
TemplateSymbolKind::Filter,
263-
),
264-
crate::filters::effective_filter_arity_in_environment(
265-
db,
266-
environment,
267-
&filter.name,
268-
&load_state,
269-
),
270-
)
271-
};
296+
let contextual_fact =
297+
filter_context_cache.resolve(load_state, &filter.name, || {
298+
let (availability, arity) = if project.is_none() {
299+
let arity = db
300+
.projectless_filter_arity_specs()
301+
.get(&filter.name)
302+
.cloned();
303+
let availability = if arity.is_some() {
304+
SymbolAvailability::Available
305+
} else {
306+
SymbolAvailability::Unknown
307+
};
308+
(availability, arity)
309+
} else {
310+
(
311+
resolve_occurrence_availability(
312+
environment,
313+
&load_state,
314+
&filter.name,
315+
TemplateSymbolKind::Filter,
316+
),
317+
crate::filters::effective_filter_arity_in_environment(
318+
db,
319+
environment,
320+
&filter.name,
321+
&load_state,
322+
),
323+
)
324+
};
325+
ContextualFilterFact {
326+
availability,
327+
arity,
328+
unknown_load_can_shadow: load_state
329+
.unknown_load_can_shadow_symbol(&filter.name, environment),
330+
}
331+
});
272332
filter_facts.insert(
273333
FilterOccurrenceKey::from_filter(filter),
274334
ScopedFilterFact {
275-
availability,
276-
arity,
277-
unknown_load_can_shadow: loaded
278-
.has_unknown_load_that_can_shadow_symbol_before(
279-
variable.span.start(),
280-
&filter.name,
281-
environment,
282-
),
335+
availability: contextual_fact.availability,
336+
arity: contextual_fact.arity,
337+
unknown_load_can_shadow: contextual_fact.unknown_load_can_shadow,
283338
},
284339
);
285340
}
@@ -426,3 +481,54 @@ fn effective_definitions_agree(
426481
&& matches!(left.1.symbol.definition, SymbolDefinition::Unknown)
427482
&& matches!(right.1.symbol.definition, SymbolDefinition::Unknown))
428483
}
484+
485+
#[cfg(test)]
486+
mod tests {
487+
use std::cell::Cell;
488+
489+
use djls_source::Span;
490+
491+
use super::ContextualFactCache;
492+
use super::LoadKind;
493+
use super::LoadStatement;
494+
use super::LoadedLibraries;
495+
use crate::scoping::loads::LoadArgument;
496+
497+
#[test]
498+
fn contextual_fact_cache_resolves_once_per_visible_prefix_and_symbol() {
499+
let loaded = LoadedLibraries::new(vec![LoadStatement::new(
500+
Span::new(10, 10),
501+
LoadKind::FullLoad {
502+
libraries: vec![LoadArgument::from("extras")],
503+
},
504+
)]);
505+
let resolutions = Cell::new(0);
506+
let mut cache = ContextualFactCache::default();
507+
let mut resolve = || {
508+
resolutions.set(resolutions.get() + 1);
509+
resolutions.get()
510+
};
511+
512+
assert_eq!(
513+
cache.resolve(loaded.available_at(0), "shared", &mut resolve),
514+
1
515+
);
516+
assert_eq!(
517+
cache.resolve(loaded.available_at(5), "shared", &mut resolve),
518+
1
519+
);
520+
assert_eq!(
521+
cache.resolve(loaded.available_at(5), "other", &mut resolve),
522+
2
523+
);
524+
assert_eq!(
525+
cache.resolve(loaded.available_at(30), "shared", &mut resolve),
526+
3
527+
);
528+
assert_eq!(
529+
cache.resolve(loaded.available_at(40), "shared", &mut resolve),
530+
3
531+
);
532+
assert_eq!(resolutions.get(), 3);
533+
}
534+
}

crates/djls-semantic/src/scoping/loads.rs

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,30 @@ impl<'a> LoadState<'a> {
227227
self.statement_end
228228
}
229229

230+
#[must_use]
231+
pub(crate) fn unknown_load_can_shadow_symbol(
232+
self,
233+
symbol: &str,
234+
environment: djls_project::TemplateEnvironment<'_>,
235+
) -> bool {
236+
self.statements()
237+
.iter()
238+
.any(|statement| match &statement.kind {
239+
LoadKind::FullLoad { libraries } => libraries.iter().any(|library| {
240+
matches!(
241+
environment.loadable_library_str(library.as_str()),
242+
LoadableLibraryLookup::Inconclusive(_)
243+
)
244+
}),
245+
LoadKind::SelectiveImport { symbols, library } => {
246+
matches!(
247+
environment.loadable_library_str(library.as_str()),
248+
LoadableLibraryLookup::Inconclusive(_)
249+
) && symbols.iter().any(|loaded| loaded.as_str() == symbol)
250+
}
251+
})
252+
}
253+
230254
#[must_use]
231255
pub(crate) fn libraries_loading_symbol(&self, symbol: &str) -> Vec<&'a str> {
232256
let mut libraries = Vec::new();
@@ -360,32 +384,6 @@ impl LoadedLibraries {
360384
Self { statements, index }
361385
}
362386

363-
#[must_use]
364-
pub(crate) fn has_unknown_load_that_can_shadow_symbol_before(
365-
&self,
366-
position: u32,
367-
symbol: &str,
368-
environment: djls_project::TemplateEnvironment<'_>,
369-
) -> bool {
370-
self.available_at(position)
371-
.statements()
372-
.iter()
373-
.any(|stmt| match &stmt.kind {
374-
LoadKind::FullLoad { libraries } => libraries.iter().any(|library| {
375-
matches!(
376-
environment.loadable_library_str(library.as_str()),
377-
LoadableLibraryLookup::Inconclusive(_)
378-
)
379-
}),
380-
LoadKind::SelectiveImport { symbols, library } => {
381-
matches!(
382-
environment.loadable_library_str(library.as_str()),
383-
LoadableLibraryLookup::Inconclusive(_)
384-
) && symbols.iter().any(|loaded| loaded.as_str() == symbol)
385-
}
386-
})
387-
}
388-
389387
/// Borrow the ordered load-statement prefix visible at `position`.
390388
#[must_use]
391389
pub(crate) fn available_at(&self, position: u32) -> LoadState<'_> {

0 commit comments

Comments
 (0)