voltaic-cli 0.2.0

The Voltaic compiler: .vlt in, .vrs containing ordinary Rust out.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
use crate::run::usage;

use voltaic_lattice::walk;

use voltaic_parser::parse_source;

use std::path::Path;

use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SigArgs {
  pub(crate) inputs: Vec<PathBuf>,
  pub(crate) why: Option<String>,
  pub(crate) verify: bool,
  pub(crate) emit: bool,
  pub(crate) require: bool,
  pub(crate) against: Option<PathBuf>,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct Agreement {
  pub(crate) checked: usize,
  pub(crate) agreed: usize,
  pub(crate) differed: Vec<(String, String, String)>,
}

impl Agreement {
  pub(crate) fn new() -> Self {
    Agreement {
      checked: 0,
      agreed: 0,
      differed: Vec::new(),
    }
  }

  pub(crate) fn absorb(&mut self, other: &Agreement) {
    self.checked += other.checked;
    self.agreed += other.agreed;
    for row in other.differed.iter() {
      self.differed.push(row.clone());
    }
    ();
  }
}

pub(crate) fn parse_sig_args(args: &[String]) -> Result<SigArgs, String> {
  let mut inputs = Vec::new();
  let mut why = None;
  let mut verify = false;
  let mut emit = false;
  let mut require = false;
  let mut against = None;
  let mut index = 0;
  while index < args.len() {
    let arg = &args[index];
    if arg == "--require-signatures" {
      require = true;
      index += 1;
    } else if arg == "--emit" {
      emit = true;
      index += 1;
    } else if arg == "--semver" {
      if index + 1 >= args.len() {
        return Err("error: `--semver` needs the sidecar to compare against".to_string());
      }
      against = Some(PathBuf::from(&args[index + 1]));
      index += 2;
    } else if arg == "--verify" {
      verify = true;
      index += 1;
    } else if arg == "--why" {
      if index + 1 >= args.len() {
        return Err("error: `--why` needs the name of a definition".to_string());
      }
      why = Some(args[index + 1].clone());
      index += 2;
    } else {
      if arg.starts_with('-') {
        let help = usage();
        return Err(format!("error: unknown flag `{}`\n\n{}", arg, help));
      }
      inputs.push(PathBuf::from(arg));
      index += 1;
    }
  }
  if inputs.is_empty() {
    let help = usage();
    return Err(format!("error: sig needs at least one .vlt\n\n{}", help));
  }
  Ok(SigArgs {
    inputs,
    why,
    verify,
    emit,
    require,
    against,
  })
}

pub(crate) fn sig_command(args: &[String]) -> Result<(), String> {
  let parsed = parse_sig_args(args)?;
  if let Some(older) = parsed.against {
    if parsed.inputs.len() != 1 {
      return Err("error: `--semver` compares one .vlt against one sidecar".to_string());
    }
    return semver(&parsed.inputs[0], &older);
  }
  if parsed.require {
    let mut missing = 0;
    for input in parsed.inputs.iter() {
      missing += require_one(input)?;
    }
    if missing == 0 {
      println!("every published definition carries a written signature");
      return Ok(());
    }
    return Err(format!(
      "error: {} published definition(s) have no written signature",
      missing
    ));
  }
  if parsed.emit {
    for input in parsed.inputs.iter() {
      emit_one(input)?;
    }
    return Ok(());
  }
  if parsed.verify {
    let mut total = Agreement::new();
    for input in parsed.inputs.iter() {
      let found = verify_one(input)?;
      total.absorb(&found);
    }
    for row in total.differed.iter() {
      println!(
        "differs: {}\n  written  {}\n  inferred {}",
        row.0, row.1, row.2
      );
    }
    println!(
      "verified {}/{} written signatures",
      total.agreed, total.checked
    );
    return Ok(());
  }
  for input in parsed.inputs.iter() {
    report(input, parsed.why.as_deref())?;
  }
  Ok(())
}

pub(crate) fn sidecar_path(input: &Path) -> PathBuf {
  let mut shown = input.as_os_str().to_os_string();
  shown.push(".sig");
  PathBuf::from(shown)
}

pub(crate) fn built(input: &PathBuf) -> Result<voltaic_lattice::Sidecar, String> {
  let shown = input.display().to_string();
  let source =
    std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
  let program =
    parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
  let found = walk(&program);
  let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
  Ok(voltaic_lattice::of_report(&found, &outcome))
}

pub(crate) fn write_sidecar(input: &PathBuf) -> Result<(), String> {
  let sidecar = built(input)?;
  let target = sidecar_path(input);
  let text = voltaic_lattice::render_sidecar(&sidecar);
  std::fs::write(&target, text)
    .map_err(|e| format!("error: cannot write {}: {}", target.display(), e))?;
  Ok(())
}

fn emit_one(input: &PathBuf) -> Result<(), String> {
  let sidecar = built(input)?;
  write_sidecar(input)?;
  let target = sidecar_path(input);
  println!("{}: {} published", target.display(), sidecar.len());
  Ok(())
}

fn require_one(input: &PathBuf) -> Result<usize, String> {
  let shown = input.display().to_string();
  let source =
    std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
  let program =
    parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
  let found = walk(&program);
  let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
  let missing = voltaic_lattice::unwritten(&found, &outcome);
  for one in missing.iter() {
    let at = voltaic_diag::line_col(&source, one.span.0);
    eprintln!(
      "{}:{}:{}: `{}` is published with no written signature",
      shown, at.0, at.1, one.name
    );
    eprintln!("  write  {} : {}", one.name, one.inferred);
  }
  Ok(missing.len())
}

fn semver(input: &PathBuf, older: &PathBuf) -> Result<(), String> {
  let recorded = std::fs::read_to_string(older)
    .map_err(|e| format!("error: cannot read {}: {}", older.display(), e))?;
  let before = voltaic_lattice::read_sidecar(&recorded);
  let after = built(input)?;
  let changes = voltaic_lattice::compare(&before, &after);
  if changes.is_empty() {
    println!("no API change");
    return Ok(());
  }
  for change in changes.iter() {
    println!("{}", change);
  }
  let count = voltaic_lattice::breaking(changes.as_slice());
  if count == 0 {
    println!("{} change(s), none breaking", changes.len());
    Ok(())
  } else {
    Err(format!(
      "error: {} breaking change(s) of {}",
      count,
      changes.len()
    ))
  }
}

fn self_free(
  written: &voltaic_lattice::Ty,
  definition: &voltaic_lattice::Inferred,
) -> voltaic_lattice::Ty {
  if let Some(target) = definition.owner.as_deref() {
    return voltaic_lattice::with_owner(written, target);
  }
  written.clone()
}

fn verify_one(input: &PathBuf) -> Result<Agreement, String> {
  let shown = input.display().to_string();
  let source =
    std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
  let program =
    parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
  let found = walk(&program);
  let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
  let mut tally = Agreement::new();
  for definition in found.definitions.iter() {
    if let Some(written) = definition.written.as_ref() {
      tally.checked += 1;
      let inferred = definition.judged(&outcome);
      let expanded = found.known.expand(&self_free(written, definition));
      if voltaic_lattice::congruent(&inferred, &expanded) {
        tally.agreed += 1;
      } else {
        tally.differed.push((
          format!("{}: {}", shown, definition.name),
          expanded.to_string(),
          inferred.to_string(),
        ));
      }
    }
  }
  Ok(tally)
}

fn report(input: &PathBuf, why: Option<&str>) -> Result<(), String> {
  let shown = input.display().to_string();
  let source =
    std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
  let program =
    parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
  let found = walk(&program);
  let outcome = voltaic_lattice::solve_with(&found.store, &found.known);

  for definition in found.definitions.iter() {
    let resolved = definition.judged(&outcome);
    let shown_row = if definition.row.is_empty() {
      String::new()
    } else {
      format!(" ! {}", definition.row)
    };
    let shown_modes = if definition.modes.is_empty() {
      String::new()
    } else {
      let every: Vec<String> = definition
        .modes
        .iter()
        .map(|m| format!("{} {}", m.0, m.1))
        .collect();
      format!(" @ {}", every.as_slice().join(", "))
    };
    println!(
      "{} : {}{}{}",
      definition.name, resolved, shown_row, shown_modes
    );
  }

  if let Some(name) = why {
    if let Some(definition) = found.find(name) {
      println!("---");
      println!("why {} : {}", definition.name, definition.judged(&outcome));
      for at in touching(definition, &found.store) {
        print!("{}", voltaic_lattice::rendered(at, &found.store, &source));
      }
    } else {
      return Err(format!(
        "error: {} has no definition called `{}`",
        shown, name
      ));
    }
  }

  let conflicting = outcome.contradictions(&found.store);
  for failure in conflicting.iter() {
    let held = found.store.at(failure.at).unwrap();
    eprintln!(
      "error: {}: {}",
      shown,
      voltaic_lattice::summarise(&held.obligation)
    );
    if why.is_none() {
      eprint!(
        "{}",
        voltaic_lattice::rendered(failure.at, &found.store, &source)
      );
    }
  }
  let undischarged = outcome.failures.len() - conflicting.len();
  if undischarged > 0 {
    eprintln!(
      "note: {} obligation(s) Lattice could not discharge, which is a gap in Lattice rather than in {}",
      undischarged, shown
    );
  }
  if conflicting.is_empty() {
    Ok(())
  } else {
    Err(format!(
      "error: {}: {} contradiction(s)",
      shown,
      conflicting.len()
    ))
  }
}

fn touching(definition: &voltaic_lattice::Inferred, store: &voltaic_lattice::Store) -> Vec<usize> {
  let wanted = definition.ty.holes();
  let mut hits = Vec::new();
  for constraint in store.all().iter().enumerate() {
    for hole in constraint.1.holes() {
      if wanted.contains(&hole) && !hits.contains(&constraint.0) {
        hits.push(constraint.0);
      }
    }
  }
  hits
}

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

  #[test]
  fn sig_refuses_an_empty_argument_list() {
    let parsed = parse_sig_args(&Vec::new());
    assert!(parsed.is_err());
    assert!(parsed.unwrap_err().contains("at least one"));
  }

  #[test]
  fn sig_takes_one_or_more_paths() {
    let args = vec!["a.vlt".to_string(), "b.vlt".to_string()];
    let parsed = parse_sig_args(args.as_slice()).expect("should parse");
    assert_eq!(parsed.inputs.len(), 2);
    assert_eq!(parsed.why, None);
  }

  #[test]
  fn why_takes_the_name_after_it() {
    let args = vec!["a.vlt".to_string(), "--why".to_string(), "peek".to_string()];
    let parsed = parse_sig_args(args.as_slice()).expect("should parse");
    assert_eq!(parsed.why, Some("peek".to_string()));
    assert_eq!(parsed.inputs.len(), 1);
  }

  #[test]
  fn why_without_a_name_is_an_error_rather_than_a_silent_no_op() {
    let args = vec!["a.vlt".to_string(), "--why".to_string()];
    let parsed = parse_sig_args(args.as_slice());
    assert!(parsed.is_err());
    assert!(parsed.unwrap_err().contains("needs the name"));
  }

  #[test]
  fn an_unknown_flag_is_refused_rather_than_taken_as_a_path() {
    let args = vec!["--nope".to_string()];
    let parsed = parse_sig_args(args.as_slice());
    assert!(parsed.is_err());
    assert!(parsed.unwrap_err().contains("unknown flag"));
  }
}