feat(resolver): implement cross-file template resolution

Enable characters to inherit from templates defined in different files
across the project structure.

- Add file_index field to NameEntry to track declaration source files
- Update NameTable::from_files() to set file indices when merging tables
- Change conversion pipeline to pass &[ast::File] instead of flat arrays
- Update merge functions to use two-level indexing:
  all_files[entry.file_index].declarations[entry.decl_index]
- Update all affected tests to use new signatures
This commit is contained in:
2026-02-08 15:45:30 +00:00
parent 9c20dd4092
commit 4c89c80748
9 changed files with 136 additions and 62 deletions

View File

@@ -42,7 +42,7 @@ use crate::{
/// Returns the fully merged fields for this template
pub fn resolve_template_includes(
template: &Template,
declarations: &[Declaration],
all_files: &[crate::syntax::ast::File],
name_table: &NameTable,
visited: &mut HashSet<String>,
) -> Result<Vec<Field>> {
@@ -69,8 +69,8 @@ pub fn resolve_template_includes(
suggestion: name_table.find_suggestion(include_name),
})?;
// Get the template declaration
let included_template = match &declarations[entry.decl_index] {
// Get the template declaration using two-level indexing
let included_template = match &all_files[entry.file_index].declarations[entry.decl_index] {
| Declaration::Template(t) => t,
| _ => {
return Err(ResolveError::ValidationError {
@@ -88,7 +88,7 @@ pub fn resolve_template_includes(
// Recursively resolve the included template
let included_fields =
resolve_template_includes(included_template, declarations, name_table, visited)?;
resolve_template_includes(included_template, all_files, name_table, visited)?;
// Merge included fields (replacing any existing fields with same name)
merged_fields = merge_field_lists(merged_fields, included_fields);
@@ -114,7 +114,7 @@ pub fn resolve_template_includes(
/// Returns the fully merged fields for this character
pub fn merge_character_templates(
character: &Character,
declarations: &[Declaration],
all_files: &[crate::syntax::ast::File],
name_table: &NameTable,
) -> Result<Vec<Field>> {
let mut merged_fields = Vec::new();
@@ -131,8 +131,8 @@ pub fn merge_character_templates(
suggestion: name_table.find_suggestion(template_name),
})?;
// Get the template declaration
let template = match &declarations[entry.decl_index] {
// Get the template declaration using two-level indexing
let template = match &all_files[entry.file_index].declarations[entry.decl_index] {
| Declaration::Template(t) => t,
| _ => {
return Err(ResolveError::ValidationError {
@@ -156,7 +156,7 @@ pub fn merge_character_templates(
// Resolve template (which handles includes recursively)
let mut visited = HashSet::new();
let template_fields =
resolve_template_includes(template, declarations, name_table, &mut visited)?;
resolve_template_includes(template, all_files, name_table, &mut visited)?;
// Merge template fields into accumulated fields
merged_fields = merge_field_lists(merged_fields, template_fields);
@@ -506,6 +506,7 @@ mod tests {
fn make_character(name: &str, fields: Vec<Field>, templates: Vec<&str>) -> Character {
Character {
name: name.to_string(),
species: None,
fields,
template: if templates.is_empty() {
None
@@ -523,8 +524,9 @@ mod tests {
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let files = vec![make_file(declarations.clone())];
let result =
resolve_template_includes(&template, &declarations, &name_table, &mut visited).unwrap();
resolve_template_includes(&template, &files, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
@@ -543,8 +545,9 @@ mod tests {
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let files = vec![make_file(declarations.clone())];
let result =
resolve_template_includes(&derived, &declarations, &name_table, &mut visited).unwrap();
resolve_template_includes(&derived, &files, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().any(|f| f.name == "age"));
@@ -565,8 +568,8 @@ mod tests {
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result =
resolve_template_includes(&top, &declarations, &name_table, &mut visited).unwrap();
let files = vec![make_file(declarations.clone())];
let result = resolve_template_includes(&top, &files, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 3);
assert!(result.iter().any(|f| f.name == "alive"));
@@ -591,8 +594,9 @@ mod tests {
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let files = vec![make_file(declarations.clone())];
let result =
resolve_template_includes(&derived, &declarations, &name_table, &mut visited).unwrap();
resolve_template_includes(&derived, &files, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
@@ -611,7 +615,8 @@ mod tests {
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table).unwrap();
let files = vec![make_file(declarations.clone())];
let result = merge_character_templates(&character, &files, &name_table).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
@@ -636,7 +641,8 @@ mod tests {
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table).unwrap();
let files = vec![make_file(declarations.clone())];
let result = merge_character_templates(&character, &files, &name_table).unwrap();
assert_eq!(result.len(), 2);
assert!(result
@@ -664,7 +670,8 @@ mod tests {
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table).unwrap();
let files = vec![make_file(declarations.clone())];
let result = merge_character_templates(&character, &files, &name_table).unwrap();
assert_eq!(result.len(), 2);
assert!(result
@@ -686,7 +693,8 @@ mod tests {
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table);
let files = vec![make_file(declarations.clone())];
let result = merge_character_templates(&character, &files, &name_table);
assert!(result.is_ok());
}
@@ -710,7 +718,8 @@ mod tests {
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table);
let files = vec![make_file(declarations.clone())];
let result = merge_character_templates(&character, &files, &name_table);
assert!(result.is_err());
if let Err(ResolveError::ValidationError { message, .. }) = result {
assert!(message.contains("strict template"));
@@ -727,7 +736,8 @@ mod tests {
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result = resolve_template_includes(&a, &declarations, &name_table, &mut visited);
let files = vec![make_file(declarations.clone())];
let result = resolve_template_includes(&a, &files, &name_table, &mut visited);
assert!(result.is_err());
if let Err(ResolveError::CircularDependency { .. }) = result {
// Expected