mimispec 0.2.0

A high-density intent description language for human-AI collaboration
Documentation
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::ast::File;
use crate::cache::ImportCache;
use crate::error::ResolveError;
use crate::parse;

const MAX_RESOLVE_DEPTH: u32 = 32;

pub struct Resolver {
    cache: ImportCache,
    resolving: Vec<PathBuf>,
    files: HashMap<PathBuf, Arc<File>>,
    errors: Vec<(PathBuf, ResolveError)>,
    root_dir: PathBuf,
    depth: u32,
}

impl Resolver {
    pub fn new(root_dir: PathBuf) -> Self {
        Self {
            cache: ImportCache::new(),
            resolving: Vec::new(),
            files: HashMap::new(),
            errors: Vec::new(),
            root_dir,
            depth: 0,
        }
    }

    pub fn resolve(&mut self, path: &Path) -> Option<Arc<File>> {
        if self.depth > MAX_RESOLVE_DEPTH {
            self.errors.push((
                path.to_path_buf(),
                ResolveError::ImportCycle {
                    chain: self.resolving.clone(),
                },
            ));
            return None;
        }

        let canonical = self.canonicalize(path)?;

        if self.files.contains_key(&canonical) {
            return self.files.get(&canonical).cloned();
        }

        if self.resolving.contains(&canonical) {
            let chain = self.resolving.clone();
            self.errors
                .push((canonical, ResolveError::ImportCycle { chain }));
            return None;
        }

        let current_mtime = fs::metadata(&canonical).ok().and_then(|m| m.modified().ok());

        if let Some(cached) = self.cache.get(&canonical, current_mtime.as_ref()) {
            self.files.insert(canonical, cached.clone());
            return Some(cached);
        }

        let source = match fs::read_to_string(&canonical) {
            Ok(s) => s,
            Err(e) => {
                self.errors.push((
                    canonical.clone(),
                    ResolveError::IoError {
                        path: canonical,
                        message: e.to_string(),
                    },
                ));
                return None;
            }
        };

        let parse_result = parse(&source);
        let file = parse_result.file;

        if !parse_result.errors.is_empty() {
            self.errors.push((
                canonical.clone(),
                ResolveError::ParseFailed {
                    path: canonical.clone(),
                    errors: parse_result.errors,
                },
            ));
        }

        let file_arc = self.cache.insert(canonical.clone(), file, current_mtime);

        self.depth += 1;
        self.resolving.push(canonical.clone());
        let import_paths: Vec<String> = file_arc.imports.clone();
        for import_path in &import_paths {
            let parent = canonical.parent().unwrap_or(Path::new("."));
            let resolved = parent.join(import_path);
            self.resolve(&resolved);
        }
        self.resolving.pop();
        self.depth -= 1;

        self.files.insert(canonical, file_arc.clone());
        Some(file_arc)
    }

    pub fn files(&self) -> &HashMap<PathBuf, Arc<File>> {
        &self.files
    }

    pub fn take_errors(&mut self) -> Vec<(PathBuf, ResolveError)> {
        std::mem::take(&mut self.errors)
    }

    pub fn take_files(&mut self) -> HashMap<PathBuf, Arc<File>> {
        std::mem::take(&mut self.files)
    }

    fn canonicalize(&mut self, path: &Path) -> Option<PathBuf> {
        if path.is_absolute() {
            if path.exists() {
                path.canonicalize().ok()
            } else {
                self.errors
                    .push((path.to_path_buf(), ResolveError::FileNotFound { path: path.to_path_buf() }));
                None
            }
        } else {
            let candidate = self.root_dir.join(path);
            if candidate.exists() {
                candidate.canonicalize().ok()
            } else {
                self.errors.push((
                    path.to_path_buf(),
                    ResolveError::FileNotFound {
                        path: candidate,
                    },
                ));
                None
            }
        }
    }
}