voltaic-driver 0.2.0

Incremental compilation driver for the Voltaic compiler.
Documentation
use voltaic_ast::Item;

use voltaic_ast::Program;

use voltaic_lattice::Declarations;

use voltaic_lattice::MemberKind;

use std::path::Path;

use std::path::PathBuf;

pub const FOREIGN: &[&str] = &["std", "core", "alloc", "crate", "super", "self"];

pub fn crate_of(path: &str) -> Option<String> {
  let pair = path.split_once("::").unwrap_or((path, ""));
  let head = pair.0;
  if head.is_empty() || FOREIGN.contains(&head) {
    return None;
  }
  Some(head.replace("_", "-"))
}

pub fn workspace_above(from: &Path) -> Option<PathBuf> {
  let mut here = from.canonicalize().unwrap_or_else(|_| from.to_path_buf());
  let mut climbed = 0;
  while climbed < 8 {
    if here.join("crates").is_dir() && here.join("Cargo.toml").is_file() {
      return Some(here);
    }
    here = here.parent()?.to_path_buf();
    climbed += 1;
  }
  None
}

pub fn sidecars_of(root: &Path, named: &str) -> Vec<PathBuf> {
  let mut found = Vec::new();
  let dir = root.join("crates").join(named).join("vlt");
  gather(&dir, &mut found);
  found.sort();
  found
}

fn gather(dir: &Path, into: &mut Vec<PathBuf>) {
  let Ok(entries) = std::fs::read_dir(dir) else {
    return ();
  };
  for found in entries.flatten() {
    let path = found.path();
    if path.is_dir() {
      gather(&path, into);
      continue;
    }
    if path.to_string_lossy().ends_with(".vlt.sig") {
      into.push(path);
    }
  }
  ();
}

pub fn upstream(program: &Program, from: &Path) -> Declarations {
  let mut known = Declarations::empty();
  let Some(root) = workspace_above(from) else {
    return known;
  };
  let mut seen: Vec<String> = Vec::new();
  for item in program.items.iter() {
    let Item::Import(decl) = item else {
      continue;
    };
    let Some(named) = crate_of(decl.path.as_str()) else {
      continue;
    };
    if seen.as_slice().contains(&named) {
      continue;
    }
    seen.push(named.clone());
    for path in sidecars_of(&root, named.as_str()) {
      let Ok(text) = std::fs::read_to_string(&path) else {
        continue;
      };
      let published = voltaic_lattice::read_sidecar(&text);
      for m in published.members.iter() {
        let kind = if m.is_field {
          MemberKind::Field
        } else {
          MemberKind::Method
        };
        known.members.push(voltaic_lattice::Member {
          owner: m.owner.clone(),
          name: m.name.clone(),
          ty: voltaic_lattice::Ty::Nil,
          kind,
        });
        if !known.types.as_slice().contains(&m.owner.clone()) {
          known.types.push(m.owner.clone());
        }
      }
    }
  }
  known
}

#[cfg(test)]
mod __vlt_tests {
  use super::*;

  #[test]
  fn an_import_names_a_candidate_crate_unless_its_head_is_one_of_rust_s_own_roots() {
    assert_eq!(
      crate_of("voltaic_ast::TypeExpr"),
      Some("voltaic-ast".to_string())
    );
    assert_eq!(
      crate_of("voltaic_lattice::Ty"),
      Some("voltaic-lattice".to_string())
    );
    assert_eq!(
      crate_of("anything_else::Thing"),
      Some("anything-else".to_string())
    );
    for root in FOREIGN {
      assert_eq!(crate_of(&format!("{}::Thing", root)), None);
    }
  }

  #[test]
  fn the_workspace_is_found_by_climbing_to_the_directory_holding_crates() {
    let here = std::env::current_dir().expect("a test runs somewhere");
    assert!(workspace_above(&here).is_some());
  }

  #[test]
  fn a_relative_path_that_does_not_exist_cannot_climb_and_says_so() {
    assert_eq!(workspace_above(Path::new("no/such/place")), None);
  }

  #[test]
  fn a_path_outside_any_workspace_finds_none_rather_than_climbing_forever() {
    assert_eq!(workspace_above(Path::new("/")), None);
  }

  #[test]
  fn a_program_with_no_crate_qualified_import_resolves_nothing() {
    let source = "import std::path::Path\n\nf : u64 -> u64\nf n = n\n";
    let program = voltaic_parser::parse_source(source).expect("should parse");
    let known = upstream(&program, Path::new("crates/voltaic-driver/vlt/resolve.vlt"));
    assert!(known.is_empty());
  }
}