Skip to content

Commit 11d13e8

Browse files
timfennisclaude
andcommitted
Merge triple scope walk into single-pass resolve_function_binding
Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent dfe00a9 commit 11d13e8

2 files changed

Lines changed: 61 additions & 99 deletions

File tree

ndc_analyser/REVIEW.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,3 @@
1818
`Binding::Resolved` for index operations — it always falls through to `Binding::Dynamic`. Add a
1919
comment explaining this is intentional, or pass actual types for better static resolution.
2020

21-
## Medium: Performance
22-
23-
- **Triple scope walk for function resolution** (`src/scope.rs` ~line 338)
24-
`resolve_function_binding` tries `resolve_function`, then `resolve_function_dynamic`, then
25-
`get_all_bindings_by_name` — each performing a full scope walk. For dynamic dispatch cases (e.g.,
26-
operators with `Any` types), this means 2–3 redundant traversals. Merging into a single pass that
27-
collects exact matches, loose matches, and all-by-name simultaneously would improve compile-time
28-
performance for programs with many overloads.
29-

ndc_analyser/src/scope.rs

Lines changed: 61 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -275,125 +275,96 @@ impl ScopeTree {
275275
}
276276
}
277277

278-
pub(crate) fn resolve_function_dynamic(
279-
&mut self,
280-
ident: &str,
281-
sig: &[StaticType],
282-
) -> Vec<ResolvedVar> {
278+
/// Resolve a function call binding in a single scope-chain walk.
279+
///
280+
/// Combines what were previously three separate walks (`resolve_function`,
281+
/// `resolve_function_dynamic`, `get_all_bindings_by_name`) into one pass.
282+
/// At each scope the priorities are:
283+
/// 1. Exact type match or upvalue → return `Binding::Resolved` immediately
284+
/// 2. Compatible-type candidates → remember first set found (for `Binding::Dynamic`)
285+
/// 3. All same-named bindings → accumulate as last-resort fallback
286+
pub(crate) fn resolve_function_binding(&mut self, ident: &str, sig: &[StaticType]) -> Binding {
283287
let mut scope_ptr = self.current_scope_idx;
284288
let mut env_scopes: Vec<usize> = Vec::default();
289+
let mut loose_candidates: Option<Vec<ResolvedVar>> = None;
290+
let mut all_by_name: Vec<ResolvedVar> = Vec::new();
285291

286292
loop {
287-
let candidates = self.scopes[scope_ptr].find_function_candidates(ident, sig);
288-
if !candidates.is_empty() {
289-
return candidates
290-
.into_iter()
291-
.map(|slot| self.resolve_found_local(ident, slot, &env_scopes))
292-
.collect();
293-
} else if let Some(slot) = self.scopes[scope_ptr].find_upvalue(ident) {
294-
return vec![self.resolve_found_upvalue(ident, slot, &env_scopes)];
295-
} else if let Some(parent_idx) = self.scopes[scope_ptr].parent_idx {
296-
if self.scopes[scope_ptr].creates_environment {
297-
env_scopes.push(scope_ptr);
298-
}
299-
300-
scope_ptr = parent_idx;
301-
} else {
302-
return self
303-
.global_scope
304-
.find_function_candidates(ident, sig)
305-
.into_iter()
306-
.map(|slot| ResolvedVar::Global { slot })
307-
.collect();
293+
// 1. Exact match → return immediately
294+
if let Some(slot) = self.scopes[scope_ptr].find_function(ident, sig) {
295+
return Binding::Resolved(self.resolve_found_local(ident, slot, &env_scopes));
308296
}
309-
}
310-
}
311297

312-
pub(crate) fn resolve_function_binding(&mut self, ident: &str, sig: &[StaticType]) -> Binding {
313-
self.resolve_function(ident, sig)
314-
.map(Binding::Resolved)
315-
.or_else(|| {
316-
let loose_bindings = self.resolve_function_dynamic(ident, sig);
317-
318-
if loose_bindings.is_empty() {
319-
return None;
320-
}
298+
// 2. Upvalue with matching name → return immediately
299+
// (matches prior behavior where resolve_function returned upvalues
300+
// without type-checking; the correctness issue is tracked separately)
301+
if let Some(slot) = self.scopes[scope_ptr].find_upvalue(ident) {
302+
return Binding::Resolved(self.resolve_found_upvalue(ident, slot, &env_scopes));
303+
}
321304

322-
Some(Binding::Dynamic(loose_bindings))
323-
})
324-
// If we can't find any function in scope that could match, fall back to all same-named
325-
// bindings so runtime dynamic dispatch (including vectorization) can pick the right one.
326-
.or_else(|| {
327-
let all_bindings = self.get_all_bindings_by_name(ident);
328-
if all_bindings.is_empty() {
329-
return None;
305+
// 3. Compatible candidates (keep only the first scope's matches — shadowing)
306+
if loose_candidates.is_none() {
307+
let candidates = self.scopes[scope_ptr].find_function_candidates(ident, sig);
308+
if !candidates.is_empty() {
309+
loose_candidates = Some(
310+
candidates
311+
.into_iter()
312+
.map(|slot| self.resolve_found_local(ident, slot, &env_scopes))
313+
.collect(),
314+
);
330315
}
331-
Some(Binding::Dynamic(all_bindings))
332-
})
333-
.unwrap_or(Binding::None)
334-
}
335-
336-
pub(crate) fn get_all_bindings_by_name(&mut self, ident: &str) -> Vec<ResolvedVar> {
337-
let mut results = Vec::new();
338-
let mut scope_ptr = self.current_scope_idx;
339-
let mut env_scopes: Vec<usize> = Vec::default();
316+
}
340317

341-
loop {
318+
// 4. All same-named bindings (accumulate across all scopes)
342319
let slots = self.scopes[scope_ptr].find_all_slots_by_name(ident);
343-
results.extend(
320+
all_by_name.extend(
344321
slots
345322
.into_iter()
346323
.map(|slot| self.resolve_found_local(ident, slot, &env_scopes)),
347324
);
348325

349-
if let Some(slot) = self.scopes[scope_ptr].find_upvalue(ident) {
350-
results.push(self.resolve_found_upvalue(ident, slot, &env_scopes));
351-
}
352-
326+
// Advance to parent scope
353327
if let Some(parent_idx) = self.scopes[scope_ptr].parent_idx {
354328
if self.scopes[scope_ptr].creates_environment {
355329
env_scopes.push(scope_ptr);
356330
}
357-
358331
scope_ptr = parent_idx;
359332
} else {
360-
let global_slots = self.global_scope.find_all_slots_by_name(ident);
361-
results.extend(
362-
global_slots
333+
// Fall through to globals
334+
if let Some(slot) = self.global_scope.find_function(ident, sig) {
335+
return Binding::Resolved(ResolvedVar::Global { slot });
336+
}
337+
338+
if loose_candidates.is_none() {
339+
let candidates = self.global_scope.find_function_candidates(ident, sig);
340+
if !candidates.is_empty() {
341+
loose_candidates = Some(
342+
candidates
343+
.into_iter()
344+
.map(|slot| ResolvedVar::Global { slot })
345+
.collect(),
346+
);
347+
}
348+
}
349+
350+
all_by_name.extend(
351+
self.global_scope
352+
.find_all_slots_by_name(ident)
363353
.into_iter()
364354
.map(|slot| ResolvedVar::Global { slot }),
365355
);
356+
366357
break;
367358
}
368359
}
369360

370-
results
371-
}
372-
373-
pub(crate) fn resolve_function(
374-
&mut self,
375-
ident: &str,
376-
arg_types: &[StaticType],
377-
) -> Option<ResolvedVar> {
378-
let mut scope_ptr = self.current_scope_idx;
379-
let mut env_scopes: Vec<usize> = Vec::default();
380-
381-
loop {
382-
if let Some(slot) = self.scopes[scope_ptr].find_function(ident, arg_types) {
383-
return Some(self.resolve_found_local(ident, slot, &env_scopes));
384-
} else if let Some(slot) = self.scopes[scope_ptr].find_upvalue(ident) {
385-
return Some(self.resolve_found_upvalue(ident, slot, &env_scopes));
386-
} else if let Some(parent_idx) = self.scopes[scope_ptr].parent_idx {
387-
if self.scopes[scope_ptr].creates_environment {
388-
env_scopes.push(scope_ptr);
389-
}
390-
scope_ptr = parent_idx;
391-
} else {
392-
return Some(ResolvedVar::Global {
393-
slot: self.global_scope.find_function(ident, arg_types)?,
394-
});
395-
}
361+
if let Some(candidates) = loose_candidates {
362+
return Binding::Dynamic(candidates);
363+
}
364+
if !all_by_name.is_empty() {
365+
return Binding::Dynamic(all_by_name);
396366
}
367+
Binding::None
397368
}
398369

399370
pub(crate) fn create_local_binding(&mut self, ident: String, typ: StaticType) -> ResolvedVar {

0 commit comments

Comments
 (0)