bend/
lib.rs

1use crate::{
2  fun::{book_to_hvm, net_to_term::net_to_term, term_to_net::Labels, Book, Ctx, Term},
3  hvm::{
4    add_recursive_priority::add_recursive_priority,
5    check_net_size::{check_net_sizes, MAX_NET_SIZE_CUDA},
6    eta_reduce::eta_reduce_hvm_net,
7    hvm_book_show_pretty,
8    inline::inline_hvm_book,
9    mutual_recursion,
10    prune::prune_hvm_book,
11  },
12};
13use diagnostics::{Diagnostics, DiagnosticsConfig, ERR_INDENT_SIZE};
14use net::hvm_to_net::hvm_to_net;
15
16pub mod diagnostics;
17// `Name` triggers this warning, but it's safe because we're not using its internal mutability.
18#[allow(clippy::mutable_key_type)]
19pub mod fun;
20pub mod hvm;
21pub mod imp;
22pub mod imports;
23pub mod net;
24mod utils;
25
26pub use fun::load_book::{load_file_to_book, load_to_book};
27
28pub const ENTRY_POINT: &str = "main";
29pub const HVM1_ENTRY_POINT: &str = "Main";
30pub const HVM_OUTPUT_END_MARKER: &str = "Result: ";
31
32pub fn check_book(
33  book: &mut Book,
34  diagnostics_cfg: DiagnosticsConfig,
35  compile_opts: CompileOpts,
36) -> Result<Diagnostics, Diagnostics> {
37  // TODO: Do the checks without having to do full compilation
38  let res = compile_book(book, compile_opts, diagnostics_cfg, None)?;
39  Ok(res.diagnostics)
40}
41
42pub fn compile_book(
43  book: &mut Book,
44  opts: CompileOpts,
45  diagnostics_cfg: DiagnosticsConfig,
46  args: Option<Vec<Term>>,
47) -> Result<CompileResult, Diagnostics> {
48  let mut diagnostics = desugar_book(book, opts.clone(), diagnostics_cfg, args)?;
49
50  let (mut hvm_book, labels) = book_to_hvm(book, &mut diagnostics)?;
51
52  if opts.eta {
53    hvm_book.defs.values_mut().for_each(eta_reduce_hvm_net);
54  }
55
56  mutual_recursion::check_cycles(&hvm_book, &mut diagnostics)?;
57
58  if opts.eta {
59    hvm_book.defs.values_mut().for_each(eta_reduce_hvm_net);
60  }
61
62  if opts.inline {
63    if let Err(e) = inline_hvm_book(&mut hvm_book) {
64      diagnostics.add_book_error(format!("During inlining:\n{:ERR_INDENT_SIZE$}{}", "", e));
65    }
66    diagnostics.fatal(())?;
67  }
68
69  if opts.prune {
70    let prune_entrypoints = vec![book.hvm_entrypoint().to_string()];
71    prune_hvm_book(&mut hvm_book, &prune_entrypoints);
72  }
73
74  if opts.check_net_size {
75    check_net_sizes(&hvm_book, &mut diagnostics, &opts.target_architecture)?;
76  }
77
78  add_recursive_priority(&mut hvm_book);
79
80  Ok(CompileResult { hvm_book, labels, diagnostics })
81}
82
83pub fn desugar_book(
84  book: &mut Book,
85  opts: CompileOpts,
86  diagnostics_cfg: DiagnosticsConfig,
87  args: Option<Vec<Term>>,
88) -> Result<Diagnostics, Diagnostics> {
89  let mut ctx = Ctx::new(book, diagnostics_cfg);
90
91  ctx.check_shared_names();
92
93  ctx.set_entrypoint();
94
95  ctx.book.encode_adts(opts.adt_encoding);
96
97  ctx.fix_match_defs()?;
98
99  ctx.apply_args(args)?;
100
101  ctx.desugar_open()?;
102
103  ctx.book.encode_builtins();
104
105  ctx.resolve_refs()?;
106
107  ctx.desugar_match_defs()?;
108
109  ctx.fix_match_terms()?;
110
111  ctx.book.lift_local_defs();
112
113  ctx.desugar_bend()?;
114  ctx.desugar_fold()?;
115  ctx.desugar_with_blocks()?;
116
117  ctx.check_unbound_vars()?;
118
119  // Auto match linearization
120  ctx.book.make_var_names_unique();
121  ctx.book.desugar_use();
122
123  match opts.linearize_matches {
124    OptLevel::Disabled => (),
125    OptLevel::Alt => ctx.book.linearize_match_binds(),
126    OptLevel::Enabled => ctx.book.linearize_matches(),
127  }
128  // Manual match linearization
129  ctx.book.linearize_match_with();
130
131  if opts.type_check {
132    type_check_book(&mut ctx)?;
133  }
134
135  ctx.book.encode_matches(opts.adt_encoding);
136
137  // sanity check
138  ctx.check_unbound_vars()?;
139
140  ctx.book.make_var_names_unique();
141  ctx.book.desugar_use();
142
143  ctx.book.make_var_names_unique();
144  ctx.book.linearize_vars();
145
146  // sanity check
147  ctx.check_unbound_vars()?;
148
149  if opts.float_combinators {
150    ctx.book.float_combinators(MAX_NET_SIZE_CUDA);
151  }
152  // sanity check
153  ctx.check_unbound_refs()?;
154
155  // Optimizing passes
156  ctx.prune(opts.prune);
157  if opts.merge {
158    ctx.book.merge_definitions();
159  }
160
161  ctx.book.expand_main();
162
163  ctx.book.make_var_names_unique();
164
165  if !ctx.info.has_errors() {
166    Ok(ctx.info)
167  } else {
168    Err(ctx.info)
169  }
170}
171
172pub fn type_check_book(ctx: &mut Ctx) -> Result<(), Diagnostics> {
173  ctx.check_untyped_terms()?;
174  ctx.resolve_type_ctrs()?;
175  ctx.type_check()?;
176  Ok(())
177}
178
179pub fn run_book(
180  mut book: Book,
181  run_opts: RunOpts,
182  compile_opts: CompileOpts,
183  diagnostics_cfg: DiagnosticsConfig,
184  args: Option<Vec<Term>>,
185  cmd: &str,
186) -> Result<Option<(Term, String, Diagnostics)>, Diagnostics> {
187  let CompileResult { hvm_book: core_book, labels, diagnostics } =
188    compile_book(&mut book, compile_opts.clone(), diagnostics_cfg, args)?;
189
190  // TODO: Printing should be taken care by the cli module, but we'd
191  // like to print any warnings before running so that the user can
192  // cancel the run if a problem is detected.
193  eprint!("{diagnostics}");
194
195  let out = run_hvm(&core_book, cmd, &run_opts)?;
196  let (net, stats) = parse_hvm_output(&out)?;
197  let (term, diags) =
198    readback_hvm_net(&net, &book, &labels, run_opts.linear_readback, compile_opts.adt_encoding);
199
200  Ok(Some((term, stats, diags)))
201}
202
203pub fn readback_hvm_net(
204  net: &::hvm::ast::Net,
205  book: &Book,
206  labels: &Labels,
207  linear: bool,
208  adt_encoding: AdtEncoding,
209) -> (Term, Diagnostics) {
210  let mut diags = Diagnostics::default();
211  let net = hvm_to_net(net);
212  let mut term = net_to_term(&net, book, labels, linear, &mut diags);
213  #[allow(clippy::mutable_key_type)] // Safe to allow, we know how `Name` works.
214  let recursive_defs = book.recursive_defs();
215  term.expand_generated(book, &recursive_defs);
216  term.resugar_strings(adt_encoding);
217  term.resugar_lists(adt_encoding);
218  (term, diags)
219}
220
221/// Runs an HVM book by invoking HVM as a subprocess.
222fn run_hvm(book: &::hvm::ast::Book, cmd: &str, run_opts: &RunOpts) -> Result<String, String> {
223  let out_path = ".out.hvm";
224  std::fs::write(out_path, hvm_book_show_pretty(book)).map_err(|x| x.to_string())?;
225  let mut process = std::process::Command::new(run_opts.hvm_path.clone())
226    .arg(cmd)
227    .arg(out_path)
228    .stdout(std::process::Stdio::piped())
229    .stderr(std::process::Stdio::inherit())
230    .spawn()
231    .map_err(|e| format!("Failed to start hvm process.\n{e}"))?;
232
233  let child_out = std::mem::take(&mut process.stdout).expect("Failed to attach to hvm output");
234  let thread_out = std::thread::spawn(move || filter_hvm_output(child_out, std::io::stdout()));
235
236  let _ = process.wait().expect("Failed to wait on hvm subprocess");
237  if let Err(e) = std::fs::remove_file(out_path) {
238    eprintln!("Error removing HVM output file. {e}");
239  }
240
241  let result = thread_out.join().map_err(|_| "HVM output thread panicked.".to_string())??;
242  Ok(result)
243}
244
245/// Reads the final output from HVM and separates the extra information.
246fn parse_hvm_output(out: &str) -> Result<(::hvm::ast::Net, String), String> {
247  let Some((result, stats)) = out.split_once('\n') else {
248    return Err(format!(
249      "Failed to parse result from HVM (unterminated result).\nOutput from HVM was:\n{:?}",
250      out
251    ));
252  };
253  let mut p = ::hvm::ast::CoreParser::new(result);
254  let Ok(net) = p.parse_net() else {
255    return Err(format!("Failed to parse result from HVM (invalid net).\nOutput from HVM was:\n{:?}", out));
256  };
257  Ok((net, stats.to_string()))
258}
259
260/// Filters the output from HVM, separating user output from the
261/// result, used for readback and displaying stats.
262///
263/// Buffers the output from HVM to try to parse it.
264fn filter_hvm_output(
265  mut stream: impl std::io::Read + Send,
266  mut output: impl std::io::Write + Send,
267) -> Result<String, String> {
268  let mut capturing = false;
269  let mut result = String::new();
270  let mut buf = [0u8; 1024];
271  loop {
272    let num_read = match stream.read(&mut buf) {
273      Ok(n) => n,
274      Err(e) => {
275        eprintln!("{e}");
276        break;
277      }
278    };
279    if num_read == 0 {
280      break;
281    }
282    let new_buf = &buf[..num_read];
283    // TODO: Does this lead to broken characters if printing too much at once?
284    let new_str = String::from_utf8_lossy(new_buf);
285    if capturing {
286      // Store the result
287      result.push_str(&new_str);
288    } else if let Some((before, after)) = new_str.split_once(HVM_OUTPUT_END_MARKER) {
289      // If result started in the middle of the buffer, print what came before and start capturing.
290      if let Err(e) = output.write_all(before.as_bytes()) {
291        eprintln!("Error writing HVM output. {e}");
292      };
293      result.push_str(after);
294      capturing = true;
295    } else {
296      // Otherwise, don't capture anything
297      if let Err(e) = output.write_all(new_buf) {
298        eprintln!("Error writing HVM output. {e}");
299      }
300    }
301  }
302
303  if capturing {
304    Ok(result)
305  } else {
306    output.flush().map_err(|e| format!("Error flushing HVM output. {e}"))?;
307    let msg = "HVM output had no result (An error likely occurred)".to_string();
308    Err(msg)
309  }
310}
311
312#[derive(Clone, Debug)]
313pub struct RunOpts {
314  pub linear_readback: bool,
315  pub pretty: bool,
316  pub hvm_path: String,
317}
318
319impl Default for RunOpts {
320  fn default() -> Self {
321    RunOpts { linear_readback: false, pretty: false, hvm_path: "hvm".to_string() }
322  }
323}
324
325#[derive(Clone, Copy, Debug, Default)]
326pub enum OptLevel {
327  Disabled,
328  #[default]
329  Enabled,
330  Alt,
331}
332
333impl OptLevel {
334  pub fn enabled(&self) -> bool {
335    !matches!(self, OptLevel::Disabled)
336  }
337
338  pub fn is_extra(&self) -> bool {
339    matches!(self, OptLevel::Enabled)
340  }
341}
342
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub enum CompilerTarget {
345  C,
346  Cuda,
347  Unknown,
348}
349
350#[derive(Clone, Debug)]
351pub struct CompileOpts {
352  /// The Compiler target architecture
353  pub target_architecture: CompilerTarget,
354
355  /// Enables [hvm::eta_reduce].
356  pub eta: bool,
357
358  /// Enables [fun::transform::definition_pruning] and [hvm::prune].
359  pub prune: bool,
360
361  /// Enables [fun::transform::linearize_matches].
362  pub linearize_matches: OptLevel,
363
364  /// Enables [fun::transform::float_combinators].
365  pub float_combinators: bool,
366
367  /// Enables [fun::transform::definition_merge]
368  pub merge: bool,
369
370  /// Enables [hvm::inline].
371  pub inline: bool,
372
373  /// Enables [hvm::check_net_size].
374  pub check_net_size: bool,
375
376  /// Enables [type_check_book].
377  pub type_check: bool,
378
379  /// Determines the encoding of constructors and matches.
380  pub adt_encoding: AdtEncoding,
381}
382
383impl CompileOpts {
384  /// Set all optimizing options as true
385  #[must_use]
386  pub fn set_all(self) -> Self {
387    Self {
388      target_architecture: self.target_architecture,
389      eta: true,
390      prune: true,
391      float_combinators: true,
392      merge: true,
393      linearize_matches: OptLevel::Enabled,
394      type_check: true,
395      inline: true,
396      check_net_size: self.check_net_size,
397      adt_encoding: self.adt_encoding,
398    }
399  }
400
401  /// Set all optimizing options as false
402  #[must_use]
403  pub fn set_no_all(self) -> Self {
404    Self {
405      target_architecture: self.target_architecture,
406      eta: false,
407      prune: false,
408      linearize_matches: OptLevel::Disabled,
409      float_combinators: false,
410      merge: false,
411      inline: false,
412      type_check: self.type_check,
413      check_net_size: self.check_net_size,
414      adt_encoding: self.adt_encoding,
415    }
416  }
417
418  pub fn check_for_strict(&self) {
419    if !self.float_combinators {
420      println!(
421        "Warning: Running in strict mode without enabling the float_combinators pass can lead to some functions expanding infinitely."
422      );
423    }
424    if !self.linearize_matches.enabled() {
425      println!(
426        "Warning: Running in strict mode without enabling the linearize_matches pass can lead to some functions expanding infinitely."
427      );
428    }
429  }
430}
431
432impl Default for CompileOpts {
433  /// Enables eta, linearize_matches, float_combinators.
434  /// Uses num-scott ADT encoding.
435  fn default() -> Self {
436    Self {
437      target_architecture: CompilerTarget::Unknown,
438      eta: true,
439      prune: false,
440      linearize_matches: OptLevel::Enabled,
441      float_combinators: true,
442      merge: false,
443      inline: false,
444      check_net_size: true,
445      type_check: true,
446      adt_encoding: AdtEncoding::NumScott,
447    }
448  }
449}
450
451#[derive(Clone, Copy, Debug)]
452pub enum AdtEncoding {
453  Scott,
454  NumScott,
455}
456
457impl std::fmt::Display for AdtEncoding {
458  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459    match self {
460      AdtEncoding::Scott => write!(f, "Scott"),
461      AdtEncoding::NumScott => write!(f, "NumScott"),
462    }
463  }
464}
465
466pub struct CompileResult {
467  pub diagnostics: Diagnostics,
468  pub hvm_book: ::hvm::ast::Book,
469  pub labels: Labels,
470}
471
472fn maybe_grow<R, F>(f: F) -> R
473where
474  F: FnOnce() -> R,
475{
476  stacker::maybe_grow(1024 * 32, 1024 * 1024, f)
477}