voltaic-driver 0.2.0

Incremental compilation driver for the Voltaic compiler.
Documentation
use crate::check::diagnose;

use voltaic_diag::Diagnostic;

use std::path::Path;

pub fn transpile(path: &Path, text: &str) -> Result<String, Vec<Diagnostic>> {
  let mut program =
    voltaic_parser::parse_source(text).map_err(|failure| diagnose(path, text, &failure))?;
  let analysis = crate::annotate::analyse_at(&mut program, path);
  if !analysis.open.is_empty() {
    return Err(crate::annotate::refusals(
      path,
      text,
      analysis.open.as_slice(),
    ));
  }
  let shown = path.display().to_string();
  let scope = voltaic_emit::MethodScope::enter(voltaic_emit::MethodTable::from_spans(
    analysis.methods.as_slice(),
  ));
  let emitted = voltaic_emit::emit_with_map(&program, text, &shown);
  drop(scope);
  Ok(emitted.0)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
  Original,
  Migrated,
}
#[allow(unused_imports)]
use Side::*;

#[derive(Debug, Clone)]
pub enum Gate {
  Identical,
  Differs {
    line: usize,
    was: String,
    now: String,
  },
  Unreadable {
    side: Side,
    problems: Vec<Diagnostic>,
  },
}
#[allow(unused_imports)]
use Gate::*;

pub fn gate(path: &Path, original: &str, migrated: &str) -> Gate {
  match transpile(path, original) {
    Err(problems) => Gate::Unreadable {
      side: Side::Original,
      problems,
    },
    Ok(before) => match transpile(path, migrated) {
      Err(problems) => Gate::Unreadable {
        side: Side::Migrated,
        problems,
      },
      Ok(after) => match first_difference(&before, &after) {
        Some(found) => Gate::Differs {
          line: found.0,
          was: found.1,
          now: found.2,
        },
        None => Gate::Identical,
      },
    },
  }
}

pub fn first_difference(before: &str, after: &str) -> Option<(usize, String, String)> {
  let mine: Vec<&str> = before.lines().collect();
  let theirs: Vec<&str> = after.lines().collect();
  let mut i = 0;
  while i < mine.len() || i < theirs.len() {
    let left = mine.get(i).copied().unwrap_or("");
    let right = theirs.get(i).copied().unwrap_or("");
    if left != right {
      return Some((i + 1, left.to_string(), right.to_string()));
    }
    i += 1;
  }
  None
}

impl Gate {
  pub fn passed(&self) -> bool {
    match self {
      Gate::Identical => true,
      _ => false,
    }
  }

  pub fn explain(&self) -> String {
    match self {
      Gate::Identical => "the generated Rust is byte-identical".to_string(),
      Gate::Differs { line, was, now } => format!(
        "the generated Rust differs at line {}:\n  was: {}\n  now: {}",
        line, was, now
      ),
      Gate::Unreadable { side, problems } => {
        let which = match side {
          Side::Original => "the original",
          Side::Migrated => "the migrated source",
        };
        format!("{} does not compile: {} diagnostics", which, problems.len())
      }
    }
  }
}

pub fn apply(path: &Path, migrated: &str) -> Result<Gate, String> {
  let shown = path.display().to_string();
  let original =
    std::fs::read_to_string(path).map_err(|e| format!("cannot read {}: {}", shown, e))?;
  let verdict = gate(path, &original, migrated);
  if !verdict.passed() {
    return Ok(verdict);
  }
  std::fs::write(path, migrated).map_err(|e| format!("cannot write {}: {}", shown, e))?;
  Ok(verdict)
}

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

  #[test]
  fn an_unchanged_file_passes_the_gate() {
    let source = "twice : u64 -> u64\ntwice n = n + n\n";
    gate(Path::new("probe.vlt"), source, source).passed();
  }

  #[test]
  fn a_changed_body_fails_the_gate_and_names_the_line() {
    let before = "twice : u64 -> u64\ntwice n = n + n\n";
    let after = "twice : u64 -> u64\ntwice n = n * 2\n";
    match gate(Path::new("probe.vlt"), before, after) {
      Gate::Differs { line, .. } => line > 0,
      _ => false,
    };
  }

  #[test]
  fn a_spelling_change_with_the_same_lowering_passes() {
    let before = "pick : u64 -> u64\npick n = n |> .max 3\n";
    let after = "pick : u64 -> u64\npick n = .max 3 n\n";
    gate(Path::new("probe.vlt"), before, after).passed();
  }

  #[test]
  fn source_the_compiler_cannot_read_is_rejected_not_compared() {
    let before = "twice : u64 -> u64\ntwice n = n + n\n";
    match gate(Path::new("probe.vlt"), before, "twice n = = =\n") {
      Gate::Unreadable { side, .. } => side == Side::Migrated,
      _ => false,
    };
  }
}