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#[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 }
40
41#[derive(FromArgs, Debug)]
43#[argh(subcommand, name = "info")]
44struct Info {}
45
46#[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#[derive(FromArgs, Debug)]
66#[argh(subcommand, name = "build")]
67#[allow(clippy::struct_excessive_bools)]
69pub(crate) struct BuildArguments {
70 #[argh(positional)]
72 pub input: String,
73 #[argh(positional)]
75 pub output: Option<PathBuf>,
76 #[argh(option, short = 'd')]
78 pub definition_file: Option<PathBuf>,
79
80 #[argh(switch, short = 'm')]
82 pub minify: bool,
83 #[argh(switch)]
85 pub source_maps: bool,
86 #[argh(switch)]
88 pub compact_diagnostics: bool,
89 #[argh(switch)]
91 pub tree_shake: bool,
92 #[argh(option, default = "MaxDiagnostics::default()")]
94 pub max_diagnostics: MaxDiagnostics,
95
96 #[cfg(not(target_family = "wasm"))]
97 #[argh(switch)]
99 pub timings: bool,
100 }
104
105#[derive(FromArgs, Debug)]
107#[argh(subcommand, name = "check")]
108pub(crate) struct CheckArguments {
109 #[argh(positional)]
111 pub input: String,
112 #[argh(option, short = 'd')]
114 pub definition_file: Option<PathBuf>,
115 #[argh(switch)]
117 pub watch: bool,
118 #[argh(switch)]
120 pub timings: bool,
121 #[argh(switch)]
123 pub compact_diagnostics: bool,
124 #[argh(switch)]
126 pub advanced_numbers: bool,
127 #[argh(option, default = "MaxDiagnostics::default()")]
129 pub max_diagnostics: MaxDiagnostics,
130}
131
132#[derive(FromArgs, PartialEq, Debug)]
134#[argh(subcommand, name = "format")]
135pub(crate) struct FormatArguments {
136 #[argh(positional)]
138 pub path: PathBuf,
139 #[argh(switch)]
141 pub check: bool,
142}
143
144#[derive(FromArgs, PartialEq, Debug)]
146#[argh(subcommand, name = "upgrade")]
147#[cfg(not(target_family = "wasm"))]
148pub(crate) struct UpgradeArguments {}
149
150#[allow(unused)]
168fn file_system_resolver(path: &Path) -> Option<String> {
169 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 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 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 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 ExitCode::SUCCESS
489 }
490 CompilerSubCommand::Repl(argument) => {
491 crate::repl::run_repl(argument);
492 ExitCode::SUCCESS
494 } }
528}
529
530#[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}