|
1 | 1 | #![warn(clippy::pedantic)] |
2 | 2 |
|
3 | | -//! # Inference Compiler |
| 3 | +//! # Inference Compiler CLI |
4 | 4 | //! |
5 | | -//! This is the entry point for the Inference compiler, which provides functionality to parse and |
6 | | -//! translate `.inf` source files into Coq code (`.v` files). |
| 5 | +//! Command line interface for the Inference toolchain. |
7 | 6 | //! |
8 | | -//! ## Modules |
| 7 | +//! 1. Parse (`--parse`) – build the typed AST. |
| 8 | +//! 2. Analyze (`--analyze`) – perform type / semantic inference and validation. |
| 9 | +//! 3. Codegen (`--codegen`) – emit WebAssembly, and optionally translate to V (`-o`). |
9 | 10 | //! |
10 | | -//! - `ast`: Contains types and builders for constructing the AST from parsed source `.inf` files. |
11 | | -//! - `cli`: Contains the command-line interface (CLI) parsing logic using the `clap` crate. |
12 | | -//! - `wasm_to_coq_translator`: Handles the translation of WebAssembly (`.wasm`) files to Coq code (`.v` files). |
| 11 | +//! At least one of the phase flags must be supplied; the phases that are requested will be |
| 12 | +//! executed in the canonical order even if specified out of order on the command line. |
13 | 13 | //! |
14 | | -//! ## Main Functionality |
| 14 | +//! Output artifacts are written to an `out/` directory relative to the current working directory. |
| 15 | +//! When `-o` is passed together with `--codegen` the produced WASM module is further translated |
| 16 | +//! into V source and saved as `out/out.v`. |
15 | 17 | //! |
16 | | -//! The main function parses command-line arguments to determine the operation mode: |
| 18 | +//! ## Exit codes |
| 19 | +//! * 0 – success. |
| 20 | +//! * 1 – usage / IO / phase failure. |
17 | 21 | //! |
18 | | -//! - If the `--wasm` flag is provided, the program will translate the specified `.wasm` file into `.v` code. |
19 | | -//! - Otherwise, the program will parse the specified `.inf` source file and generate an AST. |
| 22 | +//! ## Example |
| 23 | +//! ```bash |
| 24 | +//! infc examples/hello.inf --codegen -o |
| 25 | +//! ``` |
20 | 26 | //! |
21 | | -//! ### Functions |
22 | | -//! |
23 | | -//! - `main`: The entry point of the program. Handles argument parsing and dispatches to the appropriate function |
24 | | -//! based on the provided arguments. It handles parses specified in the first CLI argument |
25 | | -//! and saves the request to the `out/` directory. |
26 | | -//! |
27 | | -//! ### Tests |
28 | | -//! |
29 | | -//! The `test` suite is located in the `main_tests` module and contains tests for the main functionality |
| 27 | +//! ## Tests |
| 28 | +//! Integration tests exercise flag validation and the happy path compilation pipeline. |
30 | 29 |
|
31 | 30 | mod parser; |
32 | 31 | use clap::Parser; |
33 | | -use inference::{compile_to_wat, wasm_to_v, wat_to_wasm}; |
| 32 | +use inference::{analyze, codegen, parse, wasm_to_v}; |
34 | 33 | use parser::Cli; |
35 | 34 | use std::{ |
36 | 35 | fs, |
37 | 36 | path::PathBuf, |
38 | 37 | process::{self}, |
39 | 38 | }; |
40 | 39 |
|
41 | | -/// Inference compiler entry point |
| 40 | +/// Entry point for the CLI executable. |
42 | 41 | /// |
43 | | -/// This function parses the command-line arguments to determine whether to parse an `.inf` source file |
44 | | -/// or translate a `.wasm` file into Coq code. Depending on the `--wasm` flag, it either invokes the |
45 | | -/// `wasm_to_coq` function or the `parse_file` function. |
| 42 | +/// Responsibilities: |
| 43 | +/// * Parse flags. |
| 44 | +/// * Validate that the input path exists and at least one phase is selected. |
| 45 | +/// * Run requested phases (parse -> analyze -> codegen). |
| 46 | +/// * Optionally translate emitted WASM into V source when `-o` is set. |
| 47 | +/// |
| 48 | +/// On any failure a diagnostic is printed to stderr and the process exits with code `1`. |
46 | 49 | fn main() { |
47 | 50 | let args = Cli::parse(); |
48 | 51 | if !args.path.exists() { |
49 | 52 | eprintln!("Error: path not found"); |
50 | 53 | process::exit(1); |
51 | 54 | } |
52 | 55 |
|
53 | | - let output_path = match args.output { |
54 | | - Some(path) => { |
55 | | - if !path.exists() { |
56 | | - fs::create_dir_all(&path).expect("Error creating output directory"); |
| 56 | + let output_path = PathBuf::from("out"); |
| 57 | + let need_parse = args.parse; |
| 58 | + let need_analyze = args.analyze; |
| 59 | + let need_codegen = args.codegen; |
| 60 | + |
| 61 | + if !(need_parse || need_analyze || need_codegen) { |
| 62 | + eprintln!("Error: at least one of --parse, --analyze, or --codegen must be specified"); |
| 63 | + process::exit(1); |
| 64 | + } |
| 65 | + |
| 66 | + let source_code = fs::read_to_string(&args.path).expect("Error reading source file"); |
| 67 | + let mut t_ast = None; |
| 68 | + if need_codegen || need_analyze || need_parse { |
| 69 | + match parse(source_code.as_str()) { |
| 70 | + Ok(ast) => { |
| 71 | + println!("Parsed: {}", args.path.display()); |
| 72 | + t_ast = Some(ast); |
| 73 | + } |
| 74 | + Err(e) => { |
| 75 | + eprintln!("Parse error: {e}"); |
| 76 | + process::exit(1); |
57 | 77 | } |
58 | | - path |
59 | 78 | } |
60 | | - None => PathBuf::from("out"), |
61 | | - }; |
62 | | - let generate = match args.generate { |
63 | | - Some(g) => match g.as_str().to_lowercase().as_str() { |
64 | | - "wat" => "wat", |
65 | | - "wasm" => "wasm", |
66 | | - "f" | "full" => "f", |
67 | | - _ => "v", |
68 | | - }, |
69 | | - None => "v", |
70 | | - }; |
71 | | - let source = match args.source { |
72 | | - Some(s) => match s.as_str().to_lowercase().as_str() { |
73 | | - "wat" => "wat", |
74 | | - //"wasm" => "wasm", |
75 | | - _ => "inf", |
76 | | - }, |
77 | | - None => "inf", |
78 | | - }; |
79 | | - let source_code = fs::read_to_string(&args.path).expect("Error reading source file"); |
80 | | - let wat = if source == "inf" { |
81 | | - compile_to_wat(source_code.as_str()).unwrap() |
82 | | - } else { |
83 | | - source_code |
84 | | - }; |
85 | | - if generate == "wat" { |
86 | | - let wat_file_path = output_path.join("out.wat"); |
87 | | - fs::write(wat_file_path.clone(), wat).expect("Error writing WAT file"); |
88 | | - println!("WAT generated at: {}", wat_file_path.to_str().unwrap()); |
89 | | - process::exit(0); |
90 | 79 | } |
91 | | - let wasm = wat_to_wasm(wat.as_str()).unwrap(); |
92 | | - if generate == "wasm" { |
93 | | - let wasm_file_path = output_path.join("out.wasm"); |
94 | | - fs::write(wasm_file_path.clone(), wasm).expect("Error writing WASM file"); |
95 | | - println!("WASM generated at: {}", wasm_file_path.to_str().unwrap()); |
96 | | - process::exit(0); |
97 | | - } |
98 | | - let mod_name = args.path.file_stem().unwrap().to_str().unwrap().to_string(); |
99 | | - let v = wasm_to_v(mod_name.as_str(), &wasm).unwrap(); |
100 | | - if generate == "v" { |
101 | | - let v_file_path = output_path.join("out.v"); |
102 | | - fs::write(v_file_path.clone(), v).expect("Error writing V file"); |
103 | | - println!("V generated at: {}", v_file_path.to_str().unwrap()); |
104 | | - process::exit(0); |
| 80 | + |
| 81 | + let Some(t_ast) = t_ast else { |
| 82 | + unreachable!("Phase validation guarantees parse ran when required"); |
| 83 | + }; |
| 84 | + |
| 85 | + if need_codegen || need_analyze { |
| 86 | + if let Err(e) = analyze(&t_ast) { |
| 87 | + eprintln!("Analysis failed: {e}"); |
| 88 | + process::exit(1); |
| 89 | + } |
| 90 | + println!("Analyzed: {}", args.path.display()); |
105 | 91 | } |
106 | | - if generate == "f" { |
107 | | - let v_file_path = output_path.join("out.v"); |
108 | | - fs::write(v_file_path.clone(), v).expect("Error writing V file"); |
109 | | - println!("V generated at: {}", v_file_path.to_str().unwrap()); |
110 | | - let wat_file_path = output_path.join("out.wat"); |
111 | | - fs::write(wat_file_path.clone(), wat).expect("Error writing WAT file"); |
112 | | - println!("WAT generated at: {}", wat_file_path.to_str().unwrap()); |
113 | | - let wasm_file_path = output_path.join("out.wasm"); |
114 | | - fs::write(wasm_file_path.clone(), wasm).expect("Error writing WASM file"); |
115 | | - println!("WASM generated at: {}", wasm_file_path.to_str().unwrap()); |
116 | | - process::exit(0); |
| 92 | + |
| 93 | + if need_codegen { |
| 94 | + let wasm = match codegen(t_ast) { |
| 95 | + Ok(w) => w, |
| 96 | + Err(e) => { |
| 97 | + eprintln!("Codegen failed: {e}"); |
| 98 | + process::exit(1); |
| 99 | + } |
| 100 | + }; |
| 101 | + println!("WASM generated"); |
| 102 | + if args.generate_v_output { |
| 103 | + let mod_name = args |
| 104 | + .path |
| 105 | + .file_stem() |
| 106 | + .unwrap_or_else(|| std::ffi::OsStr::new("module")) |
| 107 | + .to_str() |
| 108 | + .unwrap(); |
| 109 | + match wasm_to_v(mod_name, &wasm) { |
| 110 | + Ok(v_output) => { |
| 111 | + let v_file_path = output_path.join("out.v"); |
| 112 | + if let Err(e) = fs::write(&v_file_path, v_output) { |
| 113 | + eprintln!("Failed to write V file: {e}"); |
| 114 | + process::exit(1); |
| 115 | + } |
| 116 | + println!("V generated at: {}", v_file_path.to_string_lossy()); |
| 117 | + } |
| 118 | + Err(e) => { |
| 119 | + eprintln!("WASM->V translation failed: {e}"); |
| 120 | + process::exit(1); |
| 121 | + } |
| 122 | + } |
| 123 | + } |
117 | 124 | } |
118 | | - eprintln!("Error: invalid generate option"); |
119 | | - process::exit(1); |
| 125 | + process::exit(0); |
120 | 126 | } |
121 | 127 |
|
122 | 128 | #[cfg(test)] |
|
0 commit comments