Skip to main content

ezno_lib/
cli.rs

1#[allow(unused)]
2use std::{
3	collections::HashSet,
4	env, fs,
5	path::{Path, PathBuf},
6	process::Command,
7	process::ExitCode,
8	time::{Duration, Instant},
9};
10
11use crate::{
12	build::{build, BuildConfig, BuildOutput, FailedBuildOutput},
13	check::check,
14	reporting::report_diagnostics_to_cli,
15	utilities::{print_to_cli, MaxDiagnostics},
16};
17use argh::FromArgs;
18use checker::{CheckOutput, TypeCheckOptions};
19use parser::ParseOptions;
20
21/// The Ezno type-checker & compiler
22#[derive(FromArgs, Debug)]
23struct TopLevel {
24	#[argh(subcommand)]
25	nested: CompilerSubCommand,
26}
27
28#[derive(FromArgs, Debug)]
29#[argh(subcommand)]
30enum CompilerSubCommand {
31	Info(Info),
32	ASTExplorer(crate::ast_explorer::ExplorerArguments),
33	Check(CheckArguments),
34	Experimental(ExperimentalArguments),
35	Repl(crate::repl::ReplArguments),
36	// Run(RunArguments),
37	// #[cfg(debug_assertions)]
38	// Pack(Pack),
39}
40
41/// Display Ezno information
42#[derive(FromArgs, Debug)]
43#[argh(subcommand, name = "info")]
44struct Info {}
45
46/// Experimental Ezno features
47#[derive(FromArgs, Debug)]
48#[argh(subcommand, name = "experimental")]
49pub(crate) struct ExperimentalArguments {
50	#[argh(subcommand)]
51	nested: ExperimentalSubcommand,
52}
53
54#[derive(FromArgs, Debug)]
55#[argh(subcommand)]
56pub(crate) enum ExperimentalSubcommand {
57	Build(BuildArguments),
58	Format(FormatArguments),
59	#[cfg(not(target_family = "wasm"))]
60	Upgrade(UpgradeArguments),
61}
62
63// TODO definition file as list
64/// Build project
65#[derive(FromArgs, Debug)]
66#[argh(subcommand, name = "build")]
67// TODO: Can be refactored with bit to reduce memory
68#[allow(clippy::struct_excessive_bools)]
69pub(crate) struct BuildArguments {
70	/// path to input file (accepts glob)
71	#[argh(positional)]
72	pub input: String,
73	/// path to output
74	#[argh(positional)]
75	pub output: Option<PathBuf>,
76	/// paths to definition files
77	#[argh(option, short = 'd')]
78	pub definition_file: Option<PathBuf>,
79
80	/// whether to minify build output
81	#[argh(switch, short = 'm')]
82	pub minify: bool,
83	/// build source maps
84	#[argh(switch)]
85	pub source_maps: bool,
86	/// compact diagnostics
87	#[argh(switch)]
88	pub compact_diagnostics: bool,
89	/// enable optimising transforms (warning can currently break code)
90	#[argh(switch)]
91	pub tree_shake: bool,
92	/// maximum diagnostics to print (defaults to 30, pass `all` for all and `0` to count)
93	#[argh(option, default = "MaxDiagnostics::default()")]
94	pub max_diagnostics: MaxDiagnostics,
95
96	#[cfg(not(target_family = "wasm"))]
97	/// whether to display compile times
98	#[argh(switch)]
99	pub timings: bool,
100	// /// whether to re-build on file changes
101	// #[argh(switch)]
102	// watch: bool,
103}
104
105/// Type check project
106#[derive(FromArgs, Debug)]
107#[argh(subcommand, name = "check")]
108pub(crate) struct CheckArguments {
109	/// path to input file (accepts glob)
110	#[argh(positional)]
111	pub input: String,
112	/// paths to definition files
113	#[argh(option, short = 'd')]
114	pub definition_file: Option<PathBuf>,
115	/// whether to re-check on file changes
116	#[argh(switch)]
117	pub watch: bool,
118	/// whether to display check time
119	#[argh(switch)]
120	pub timings: bool,
121	/// compact diagnostics
122	#[argh(switch)]
123	pub compact_diagnostics: bool,
124	/// more behavior for numbers
125	#[argh(switch)]
126	pub advanced_numbers: bool,
127	/// maximum diagnostics to print (defaults to 30, pass `all` for all and `0` to count)
128	#[argh(option, default = "MaxDiagnostics::default()")]
129	pub max_diagnostics: MaxDiagnostics,
130}
131
132/// Formats file in-place
133#[derive(FromArgs, PartialEq, Debug)]
134#[argh(subcommand, name = "format")]
135pub(crate) struct FormatArguments {
136	/// path to input file
137	#[argh(positional)]
138	pub path: PathBuf,
139	/// check whether file is formatted
140	#[argh(switch)]
141	pub check: bool,
142}
143
144/// Upgrade/update the ezno binary to the latest version
145#[derive(FromArgs, PartialEq, Debug)]
146#[argh(subcommand, name = "upgrade")]
147#[cfg(not(target_family = "wasm"))]
148pub(crate) struct UpgradeArguments {}
149
150// /// Run project using Deno
151// #[derive(FromArgs, PartialEq, Debug)]
152// #[argh(subcommand, name = "run")]
153// struct RunArguments {
154// 	/// path to input file
155// 	#[argh(positional)]
156// 	input: PathBuf,
157
158// 	/// path to output
159// 	#[argh(positional)]
160// 	output: PathBuf,
161
162// 	/// whether to re-run on file changes
163// 	#[argh(switch)]
164// 	watch: bool,
165// }
166
167#[allow(unused)]
168fn file_system_resolver(path: &Path) -> Option<String> {
169	// Cheaty
170	if path.to_str() == Some("BLANK") {
171		return Some(String::new());
172	}
173	match fs::read_to_string(path) {
174		Ok(source) => Some(source),
175		Err(_) => None,
176	}
177}
178
179fn run_checker<T: crate::ReadFromFS>(
180	entry_points: Vec<PathBuf>,
181	read_file: &T,
182	timings: bool,
183	definition_file: Option<PathBuf>,
184	max_diagnostics: MaxDiagnostics,
185	type_check_options: TypeCheckOptions,
186	compact_diagnostics: bool,
187) -> ExitCode {
188	let result = check(entry_points, read_file, definition_file.as_deref(), type_check_options);
189
190	let CheckOutput { diagnostics, module_contents, chronometer, types, .. } = result;
191
192	let diagnostics_count = diagnostics.count();
193	let current = timings.then(std::time::Instant::now);
194
195	let result = if diagnostics.contains_error() {
196		if let MaxDiagnostics::FixedTo(0) = max_diagnostics {
197			let count = diagnostics.into_iter().count();
198			print_to_cli(format_args!(
199				"Found {count} type errors and warnings {}",
200				console::Emoji(" 😬", ":/")
201			))
202		} else {
203			report_diagnostics_to_cli(
204				diagnostics,
205				&module_contents,
206				compact_diagnostics,
207				max_diagnostics,
208			)
209			.unwrap();
210		}
211		ExitCode::FAILURE
212	} else {
213		// May be warnings or information here
214		report_diagnostics_to_cli(
215			diagnostics,
216			&module_contents,
217			compact_diagnostics,
218			max_diagnostics,
219		)
220		.unwrap();
221		print_to_cli(format_args!("No type errors found {}", console::Emoji("🎉", ":)")));
222		ExitCode::SUCCESS
223	};
224
225	#[cfg(not(target_family = "wasm"))]
226	if timings {
227		let reporting = current.unwrap().elapsed();
228		eprintln!("---\n");
229		eprintln!("Diagnostics:\t{}", diagnostics_count);
230		eprintln!("Types:      \t{}", types.count_of_types());
231		eprintln!("Lines:      \t{}", chronometer.lines);
232		eprintln!("Cache read: \t{:?}", chronometer.cached);
233		eprintln!("FS read:    \t{:?}", chronometer.fs);
234		eprintln!("Parsed in:  \t{:?}", chronometer.parse);
235		eprintln!("Checked in: \t{:?}", chronometer.check);
236		eprintln!("Reporting:  \t{:?}", reporting);
237	}
238
239	result
240}
241
242pub fn run_cli<T: crate::ReadFromFS, U: crate::WriteToFS>(
243	cli_arguments: &[&str],
244	read_file: T,
245	write_file: U,
246) -> ExitCode {
247	let command = match FromArgs::from_args(&["ezno-cli"], cli_arguments) {
248		Ok(TopLevel { nested }) => nested,
249		Err(err) => {
250			print_to_cli(format_args!("{}", err.output));
251			return ExitCode::FAILURE;
252		}
253	};
254
255	match command {
256		CompilerSubCommand::Info(_) => {
257			crate::utilities::print_info();
258			ExitCode::SUCCESS
259		}
260		CompilerSubCommand::Check(check_arguments) => {
261			let CheckArguments {
262				input,
263				watch,
264				definition_file,
265				timings,
266				compact_diagnostics,
267				max_diagnostics,
268				advanced_numbers,
269			} = check_arguments;
270
271			let type_check_options: TypeCheckOptions = if cfg!(target_family = "wasm") {
272				Default::default()
273			} else {
274				TypeCheckOptions {
275					measure_time: timings,
276					advanced_numbers,
277					..TypeCheckOptions::default()
278				}
279			};
280
281			let entry_points = match get_entry_points(input) {
282				Ok(entry_points) => entry_points,
283				Err(_) => {
284					print_to_cli(format_args!("Entry point error"));
285					return ExitCode::FAILURE;
286				}
287			};
288
289			// run_checker is written three times because cloning
290			if watch {
291				#[cfg(target_family = "wasm")]
292				panic!("'watch' mode not supported on WASM");
293
294				#[cfg(not(target_family = "wasm"))]
295				{
296					use notify::Watcher;
297					use notify_debouncer_full::new_debouncer;
298
299					let (tx, rx) = std::sync::mpsc::channel();
300					let mut debouncer =
301						new_debouncer(Duration::from_millis(200), None, tx).unwrap();
302
303					for e in &entry_points {
304						debouncer.watcher().watch(e, notify::RecursiveMode::Recursive).unwrap();
305					}
306
307					let _ = run_checker(
308						entry_points.clone(),
309						&read_file,
310						timings,
311						definition_file.clone(),
312						max_diagnostics,
313						type_check_options.clone(),
314						compact_diagnostics,
315					);
316
317					for res in rx {
318						match res {
319							Ok(_e) => {
320								let _out = run_checker(
321									entry_points.clone(),
322									&read_file,
323									timings,
324									definition_file.clone(),
325									max_diagnostics,
326									type_check_options.clone(),
327									compact_diagnostics,
328								);
329							}
330							Err(error) => eprintln!("Error: {error:?}"),
331						}
332					}
333
334					unreachable!()
335				}
336			} else {
337				run_checker(
338					entry_points,
339					&read_file,
340					timings,
341					definition_file,
342					max_diagnostics,
343					type_check_options,
344					compact_diagnostics,
345				)
346			}
347		}
348		CompilerSubCommand::Experimental(ExperimentalArguments {
349			nested: ExperimentalSubcommand::Build(build_config),
350		}) => {
351			let output_path = build_config.output.unwrap_or("ezno.out.js".into());
352
353			let entry_points = match get_entry_points(build_config.input) {
354				Ok(entry_points) => entry_points,
355				Err(_) => {
356					print_to_cli(format_args!("Entry point error"));
357					return ExitCode::FAILURE;
358				}
359			};
360
361			#[cfg(not(target_family = "wasm"))]
362			let start = build_config.timings.then(std::time::Instant::now);
363
364			let config = BuildConfig {
365				tree_shake: build_config.tree_shake,
366				strip_whitespace: build_config.minify,
367				source_maps: build_config.source_maps,
368				type_definition_module: build_config.definition_file,
369				// TODO not sure
370				output_path,
371				other_transformers: None,
372				lsp_mode: false,
373			};
374
375			let output = build(entry_points, &read_file, config);
376
377			#[cfg(not(target_family = "wasm"))]
378			if let Some(start) = start {
379				eprintln!("Checked & built in {:?}", start.elapsed());
380			};
381
382			let compact_diagnostics = build_config.compact_diagnostics;
383
384			match output {
385				Ok(BuildOutput {
386					artifacts,
387					check_output: CheckOutput { module_contents, diagnostics, .. },
388				}) => {
389					for output in artifacts {
390						write_file(output.output_path.as_path(), output.content);
391					}
392					report_diagnostics_to_cli(
393						diagnostics,
394						&module_contents,
395						compact_diagnostics,
396						build_config.max_diagnostics,
397					)
398					.unwrap();
399					print_to_cli(format_args!(
400						"Project built successfully {}",
401						console::Emoji("🎉", ":)")
402					));
403					ExitCode::SUCCESS
404				}
405				Err(FailedBuildOutput(CheckOutput { module_contents, diagnostics, .. })) => {
406					report_diagnostics_to_cli(
407						diagnostics,
408						&module_contents,
409						compact_diagnostics,
410						build_config.max_diagnostics,
411					)
412					.unwrap();
413					ExitCode::FAILURE
414				}
415			}
416		}
417		CompilerSubCommand::Experimental(ExperimentalArguments {
418			nested: ExperimentalSubcommand::Format(FormatArguments { path, check }),
419		}) => {
420			use parser::{source_map::FileSystem, ASTNode, Module, ToStringOptions};
421
422			let input = match fs::read_to_string(&path) {
423				Ok(string) => string,
424				Err(err) => {
425					print_to_cli(format_args!("{err:?}"));
426					return ExitCode::FAILURE;
427				}
428			};
429			let mut files =
430				parser::source_map::MapFileStore::<parser::source_map::NoPathMap>::default();
431			let source_id = files.new_source_id(path.clone(), input.clone());
432			let res = Module::from_string(
433				input.clone(),
434				ParseOptions { retain_blank_lines: true, ..Default::default() },
435			);
436			match res {
437				Ok(module) => {
438					let options = ToStringOptions {
439						trailing_semicolon: true,
440						include_type_annotations: true,
441						..Default::default()
442					};
443					let output = module.to_string(&options);
444					if check {
445						if input == output {
446							ExitCode::SUCCESS
447						} else {
448							print_to_cli(format_args!(
449								"{}",
450								pretty_assertions::StrComparison::new(&input, &output)
451							));
452							ExitCode::FAILURE
453						}
454					} else {
455						let _ = fs::write(path.clone(), output);
456						print_to_cli(format_args!("Formatted {}", path.display()));
457						ExitCode::SUCCESS
458					}
459				}
460				Err(err) => {
461					report_diagnostics_to_cli(
462						std::iter::once((err, source_id).into()),
463						&files,
464						false,
465						MaxDiagnostics::All,
466					)
467					.unwrap();
468					ExitCode::FAILURE
469				}
470			}
471		}
472		#[cfg(not(target_family = "wasm"))]
473		CompilerSubCommand::Experimental(ExperimentalArguments {
474			nested: ExperimentalSubcommand::Upgrade(UpgradeArguments {}),
475		}) => match crate::utilities::upgrade_self() {
476			Ok(name) => {
477				print_to_cli(format_args!("Successfully updated to {name}"));
478				std::process::ExitCode::SUCCESS
479			}
480			Err(err) => {
481				print_to_cli(format_args!("Error: {err}\nCould not upgrade binary. Retry manually from {repository}/releases", repository=env!("CARGO_PKG_REPOSITORY")));
482				std::process::ExitCode::FAILURE
483			}
484		},
485		CompilerSubCommand::ASTExplorer(mut repl) => {
486			repl.run(&read_file);
487			// TODO not always true
488			ExitCode::SUCCESS
489		}
490		CompilerSubCommand::Repl(argument) => {
491			crate::repl::run_repl(argument);
492			// TODO not always true
493			ExitCode::SUCCESS
494		} // CompilerSubCommand::Run(run_arguments) => {
495		  // 	let build_arguments = BuildArguments {
496		  // 		input: run_arguments.input,
497		  // 		output: Some(run_arguments.output.clone()),
498		  // 		minify: true,
499		  // 		no_comments: true,
500		  // 		source_maps: false,
501		  // 		watch: false,
502		  // 		timings: false,
503		  // 	};
504		  // 	let output = build(build_arguments);
505
506		  // 	if output.is_ok() {
507		  // 		Command::new("deno")
508		  // 			.args(["run", "--allow-all", run_arguments.output.to_str().unwrap()])
509		  // 			.spawn()
510		  // 			.unwrap()
511		  // 			.wait()
512		  // 			.unwrap();
513		  // 	}
514		  // }
515		  // #[cfg(debug_assertions)]
516		  // CompilerSubCommand::Pack(Pack { input, output }) => {
517		  // 	let file = checker::definition_file_to_buffer(
518		  // 		&file_system_resolver,
519		  // 		&env::current_dir().unwrap(),
520		  // 		&input,
521		  // 	)
522		  // 	.unwrap();
523
524		  // 	let _root_ctx = checker::root_context_from_bytes(file);
525		  // 	println!("Registered {} types", _root_ctx.types.len();
526		  // }
527	}
528}
529
530// `glob` library does not work on WASM :(
531#[cfg(target_family = "wasm")]
532fn get_entry_points(input: String) -> Result<Vec<PathBuf>, ()> {
533	Ok(vec![input.into()])
534}
535
536#[cfg(not(target_family = "wasm"))]
537fn get_entry_points(input: String) -> Result<Vec<PathBuf>, ()> {
538	match glob::glob(&input) {
539		Ok(files) => {
540			let files = files
541				.into_iter()
542				.collect::<Result<Vec<PathBuf>, glob::GlobError>>()
543				.map_err(|err| {
544					eprintln!("{err:?}");
545				})?;
546
547			if files.is_empty() {
548				eprintln!("Input {input:?} matched no files");
549				Err(())
550			} else {
551				Ok(files)
552			}
553		}
554		Err(err) => {
555			eprintln!("{err:?}");
556			Err(())
557		}
558	}
559}