1use std::ffi::{OsStr, OsString};
2use std::path::PathBuf;
3
4use pico_args::Arguments;
5use thiserror::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct CliConfig {
10 pub input_path: Option<PathBuf>,
12 pub emit_rust_path: Option<PathBuf>,
14 pub emit_wat_path: Option<PathBuf>,
16 pub emit_wasm_path: Option<PathBuf>,
18 pub run: bool,
20 pub dump_ast: bool,
22 pub dump_hir: bool,
24 pub dump_tokens: bool,
26 pub dump_pass_timings: bool,
28 pub dump_pass_snapshots: bool,
30 pub dump_pass_snapshots_json: bool,
32 pub show_help: bool,
34 pub show_version: bool,
36}
37
38#[derive(Debug, Clone, Error, PartialEq, Eq)]
40pub enum CliError {
41 #[error("unknown option: {0}")]
43 UnknownOption(String),
44 #[error("missing input file path")]
46 MissingInputPath,
47 #[error("too many input file paths provided")]
49 TooManyInputPaths,
50 #[error("missing value for --emit-rust=<path>")]
52 MissingEmitRustPath,
53 #[error("missing value for --emit-wat=<path>")]
55 MissingEmitWatPath,
56 #[error("missing value for --emit-wasm=<path>")]
58 MissingEmitWasmPath,
59}
60
61pub fn parse_env() -> Result<CliConfig, CliError> {
63 parse_from(std::env::args_os().skip(1).collect())
64}
65
66pub fn parse_from(args: Vec<OsString>) -> Result<CliConfig, CliError> {
79 let emit_config = parse_emit_flags(args)?;
80 let mut pargs = Arguments::from_vec(emit_config.remaining_args);
81
82 let show_help = pargs.contains(["-h", "--help"]);
83 let show_version = pargs.contains(["-V", "--version"]);
84 let dump_ast = pargs.contains("--dump-ast");
85 let dump_hir = pargs.contains("--dump-hir");
86 let dump_tokens = pargs.contains("--dump-tokens");
87 let dump_pass_timings = pargs.contains("--dump-pass-timings");
88 let dump_pass_snapshots = pargs.contains("--dump-pass-snapshots");
89 let dump_pass_snapshots_json = pargs.contains("--dump-pass-snapshots-json");
90 let run = pargs.contains("--run");
91
92 let remaining = pargs.finish();
93
94 if let Some(option) = remaining.iter().find(|arg| is_option_like(arg)) {
95 return Err(CliError::UnknownOption(
96 option.to_string_lossy().into_owned(),
97 ));
98 }
99
100 let mut paths = remaining
101 .into_iter()
102 .map(PathBuf::from)
103 .collect::<Vec<PathBuf>>();
104
105 let input_path = if show_help || show_version {
106 None
107 } else {
108 match paths.len() {
109 0 => return Err(CliError::MissingInputPath),
110 1 => paths.pop(),
111 _ => return Err(CliError::TooManyInputPaths),
112 }
113 };
114
115 Ok(CliConfig {
116 input_path,
117 emit_rust_path: emit_config.emit_rust_path,
118 emit_wat_path: emit_config.emit_wat_path,
119 emit_wasm_path: emit_config.emit_wasm_path,
120 run,
121 dump_ast,
122 dump_hir,
123 dump_tokens,
124 dump_pass_timings,
125 dump_pass_snapshots,
126 dump_pass_snapshots_json,
127 show_help,
128 show_version,
129 })
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133struct EmitConfig {
134 remaining_args: Vec<OsString>,
135 emit_rust_path: Option<PathBuf>,
136 emit_wat_path: Option<PathBuf>,
137 emit_wasm_path: Option<PathBuf>,
138}
139
140fn parse_emit_flags(args: Vec<OsString>) -> Result<EmitConfig, CliError> {
142 let mut remaining_args = Vec::new();
143 let mut emit_rust_path = None;
144 let mut emit_wat_path = None;
145 let mut emit_wasm_path = None;
146
147 for arg in args {
148 let text = arg.to_string_lossy();
149
150 if text == "--emit-rust" {
151 return Err(CliError::MissingEmitRustPath);
152 }
153
154 if let Some(value) = text.strip_prefix("--emit-rust=") {
155 if value.is_empty() {
156 return Err(CliError::MissingEmitRustPath);
157 }
158 emit_rust_path = Some(PathBuf::from(value));
159 continue;
160 }
161
162 if text == "--emit-wat" {
163 return Err(CliError::MissingEmitWatPath);
164 }
165
166 if let Some(value) = text.strip_prefix("--emit-wat=") {
167 if value.is_empty() {
168 return Err(CliError::MissingEmitWatPath);
169 }
170 emit_wat_path = Some(PathBuf::from(value));
171 continue;
172 }
173
174 if text == "--emit-wasm" {
175 return Err(CliError::MissingEmitWasmPath);
176 }
177
178 if let Some(value) = text.strip_prefix("--emit-wasm=") {
179 if value.is_empty() {
180 return Err(CliError::MissingEmitWasmPath);
181 }
182 emit_wasm_path = Some(PathBuf::from(value));
183 continue;
184 }
185
186 remaining_args.push(arg);
187 }
188
189 Ok(EmitConfig {
190 remaining_args,
191 emit_rust_path,
192 emit_wat_path,
193 emit_wasm_path,
194 })
195}
196
197fn is_option_like(arg: &OsStr) -> bool {
199 arg.to_string_lossy().starts_with('-')
200}
201
202pub fn help_text() -> String {
204 format!(
205 "Usage:\n rukalang [OPTIONS] <file>\n\nOptions:\n --dump-ast Print AST output\n --dump-hir Print HIR output\n --dump-tokens Print lexer token output\n --dump-pass-timings Print per-pass timing output\n --dump-pass-snapshots Print per-pass snapshot output\n --dump-pass-snapshots-json Print per-pass snapshots as JSON lines\n --emit-rust=<path> Emit Rust source to path\n --emit-wat=<path> Emit WAT source to path\n --emit-wasm=<path> Emit WASM binary to path\n --run Compile and run emitted Rust source\n -h, --help Print help\n -V, --version Print version\n"
206 )
207}
208
209pub fn version_text() -> String {
216 format!("rukalang {}", env!("CARGO_PKG_VERSION"))
217}
218
219#[cfg(test)]
220mod tests;