erg_common/
config.rs

1//! defines a command-line parser for `ergc`.
2//!
3//! コマンドオプション(パーサー)を定義する
4use std::env;
5use std::fmt;
6use std::io::{stdin, IsTerminal, Read};
7use std::path::PathBuf;
8use std::process;
9use std::str::FromStr;
10
11use crate::help_messages::{command_message, mode_message, OPTIONS};
12use crate::io::{Input, Output};
13use crate::levenshtein::get_similar_name;
14use crate::normalize_path;
15use crate::python_util::{detect_magic_number, get_python_version, PythonVersion};
16use crate::serialize::{get_magic_num_from_bytes, get_ver_from_magic_num};
17use crate::ArcArray;
18
19#[cfg(not(feature = "pylib"))]
20use erg_proc_macros::{new, pyclass, pymethods};
21#[cfg(feature = "pylib")]
22use pyo3::prelude::*;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum ErgMode {
26    Lex,
27    Parse,
28    Desugar,
29    TypeCheck,
30    FullCheck,
31    Compile,
32    Transpile,
33    Execute,
34    LanguageServer,
35    Lint,
36    Read,
37    Pack,
38}
39
40impl TryFrom<&str> for ErgMode {
41    type Error = ();
42    fn try_from(s: &str) -> Result<Self, ()> {
43        match s {
44            "lex" | "lexer" => Ok(Self::Lex),
45            "parse" | "parser" => Ok(Self::Parse),
46            "desugar" | "desugarer" => Ok(Self::Desugar),
47            "typecheck" | "lower" | "tc" => Ok(Self::TypeCheck),
48            "fullcheck" | "check" | "checker" => Ok(Self::FullCheck),
49            "comp" | "compile" | "compiler" => Ok(Self::Compile),
50            "trans" | "transpile" | "transpiler" => Ok(Self::Transpile),
51            "run" | "execute" => Ok(Self::Execute),
52            "server" | "language-server" => Ok(Self::LanguageServer),
53            "lint" | "linter" => Ok(Self::Lint),
54            "byteread" | "read" | "reader" | "dis" => Ok(Self::Read),
55            "pack" | "package" => Ok(Self::Pack),
56            _ => Err(()),
57        }
58    }
59}
60
61impl From<ErgMode> for &str {
62    fn from(mode: ErgMode) -> Self {
63        match mode {
64            ErgMode::Lex => "lex",
65            ErgMode::Parse => "parse",
66            ErgMode::Desugar => "desugar",
67            ErgMode::TypeCheck => "typecheck",
68            ErgMode::FullCheck => "fullcheck",
69            ErgMode::Compile => "compile",
70            ErgMode::Transpile => "transpile",
71            ErgMode::Execute => "execute",
72            ErgMode::LanguageServer => "language-server",
73            ErgMode::Lint => "lint",
74            ErgMode::Read => "read",
75            ErgMode::Pack => "pack",
76        }
77    }
78}
79
80impl fmt::Display for ErgMode {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "{}", <&str>::from(*self))
83    }
84}
85
86impl ErgMode {
87    pub const fn is_language_server(&self) -> bool {
88        matches!(self, Self::LanguageServer)
89    }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum TranspileTarget {
94    Python,
95    Json,
96    Toml,
97}
98
99impl From<&str> for TranspileTarget {
100    fn from(s: &str) -> Self {
101        match s {
102            "python" | "py" => Self::Python,
103            "json" => Self::Json,
104            "toml" => Self::Toml,
105            _ => panic!("unsupported transpile target: {s}"),
106        }
107    }
108}
109
110#[pyclass]
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub struct Package {
113    pub name: &'static str,
114    pub as_name: &'static str,
115    pub version: &'static str,
116    pub path: Option<&'static str>,
117}
118
119impl Package {
120    pub const fn new(
121        name: &'static str,
122        as_name: &'static str,
123        version: &'static str,
124        path: Option<&'static str>,
125    ) -> Self {
126        Self {
127            name,
128            as_name,
129            version,
130            path,
131        }
132    }
133}
134
135#[pymethods]
136impl Package {
137    #[new]
138    fn _new(name: String, as_name: String, version: String, path: Option<String>) -> Self {
139        Self {
140            name: Box::leak(name.into_boxed_str()),
141            as_name: Box::leak(as_name.into_boxed_str()),
142            version: Box::leak(version.into_boxed_str()),
143            path: path.map(|s| Box::leak(s.into_boxed_str()) as &'static str),
144        }
145    }
146}
147
148#[derive(Debug, Clone)]
149pub struct ErgConfig {
150    pub mode: ErgMode,
151    /// optimization level.
152    /// * 0: no optimization
153    /// * 1 (default): e.g. constant folding, dead code elimination
154    /// * 2: e.g. static dispatching, inlining, peephole
155    /// * 3: e.g. JIT compiling
156    pub opt_level: u8,
157    pub no_std: bool,
158    pub py_magic_num: Option<u32>, // the magic number cannot be uniquely determined from `target_version`
159    pub py_command: Option<&'static str>,
160    pub target_version: Option<PythonVersion>,
161    pub transpile_target: Option<TranspileTarget>,
162    pub py_server_timeout: u64,
163    pub quiet_repl: bool,
164    pub show_type: bool,
165    pub input: Input,
166    pub output: Output,
167    pub dist_dir: Option<&'static str>,
168    /// module name to be executed
169    pub module: &'static str,
170    /// verbosity level for system messages.
171    /// * 0: display errors, warns
172    /// * 1 (default): display errors, warnings and hints
173    /// * 2: display errors, warnings, hints and progress
174    pub verbose: u8,
175    /// needed for `jupyter-erg`
176    pub ps1: &'static str,
177    pub ps2: &'static str,
178    pub runtime_args: ArcArray<&'static str>,
179    pub packages: ArcArray<Package>,
180    /* ↓ options for pylyzer ↓ */
181    pub effect_check: bool,
182    pub ownership_check: bool,
183    pub use_pylyzer: bool,
184    pub no_infer_fn_type: bool,
185    pub fast_error_report: bool,
186    pub do_not_show_ext_errors: bool,
187    pub respect_pyi: bool,
188}
189
190impl Default for ErgConfig {
191    #[inline]
192    fn default() -> Self {
193        Self {
194            mode: ErgMode::Execute,
195            opt_level: 1,
196            no_std: false,
197            py_magic_num: None,
198            py_command: None,
199            target_version: None,
200            transpile_target: None,
201            py_server_timeout: 10,
202            quiet_repl: false,
203            show_type: false,
204            input: Input::repl(),
205            output: Output::stdout(),
206            dist_dir: None,
207            module: "<module>",
208            verbose: 1,
209            ps1: ">>> ",
210            ps2: "... ",
211            runtime_args: ArcArray::from([]),
212            packages: ArcArray::from([]),
213            effect_check: true,
214            ownership_check: true,
215            use_pylyzer: false,
216            no_infer_fn_type: false,
217            fast_error_report: false,
218            do_not_show_ext_errors: false,
219            respect_pyi: true,
220        }
221    }
222}
223
224impl ErgConfig {
225    pub fn with_main_path(path: PathBuf) -> Self {
226        let path = normalize_path(path);
227        Self {
228            module: "<module>",
229            input: Input::file(path),
230            ..ErgConfig::default()
231        }
232    }
233
234    pub fn string(src: String) -> Self {
235        Self {
236            input: Input::str(src),
237            ..ErgConfig::default()
238        }
239    }
240
241    /// clone alias (since the actual clone cost is low)
242    #[inline]
243    pub fn copy(&self) -> Self {
244        self.clone()
245    }
246
247    pub fn dump_path(&self) -> PathBuf {
248        if let Some(output) = &self.dist_dir {
249            PathBuf::from(format!("{output}/{}", self.input.filename()))
250        } else {
251            self.input.full_path().to_path_buf()
252        }
253    }
254
255    pub fn dump_filename(&self) -> String {
256        if let Some(output) = &self.dist_dir {
257            format!("{output}/{}", self.input.filename())
258        } else {
259            self.input.filename()
260        }
261    }
262
263    pub fn dump_pyc_path(&self) -> PathBuf {
264        let mut dump_path = self.dump_path();
265        dump_path.set_extension("pyc");
266        dump_path
267    }
268
269    pub fn dump_pyc_filename(&self) -> String {
270        let dump_filename = self.dump_filename();
271        if dump_filename.ends_with(".er") {
272            dump_filename.replace(".er", ".pyc")
273        } else {
274            dump_filename + ".pyc"
275        }
276    }
277
278    pub fn inherit(&self, path: PathBuf) -> Self {
279        let path = normalize_path(path);
280        Self {
281            module: Box::leak(path.to_str().unwrap().to_string().into_boxed_str()),
282            input: Input::file(path),
283            ..self.copy()
284        }
285    }
286
287    pub fn parse() -> Self {
288        let mut args = env::args();
289        args.next(); // "ergc"
290        let mut cfg = Self::default();
291        let mut runtime_args: Vec<&'static str> = vec![];
292        let mut packages = vec![];
293        // not `for` because we need to consume the next argument
294        while let Some(arg) = args.next() {
295            match &arg[..] {
296                /* Options */
297                "--" => {
298                    for arg in args {
299                        runtime_args.push(Box::leak(arg.into_boxed_str()));
300                    }
301                    break;
302                }
303                "-c" | "--code" => {
304                    cfg.input = Input::str(args.next().expect("the value of `-c` is not passed"));
305                }
306                "--no-std" => {
307                    cfg.no_std = true;
308                }
309                "-?" | "-h" | "--help" => {
310                    println!("{}", command_message());
311                    if let "--mode" = args.next().as_ref().map(|s| &s[..]).unwrap_or("") {
312                        println!("{}", mode_message());
313                    }
314                    process::exit(0);
315                }
316                "-m" | "--module" => {
317                    let module = args
318                        .next()
319                        .expect("the value of `-m` is not passed")
320                        .into_boxed_str();
321                    cfg.module = Box::leak(module);
322                }
323                "--mode" => {
324                    let mode = args.next().expect("the value of `--mode` is not passed");
325                    if let "-?" | "-h" | "--help" = &mode[..] {
326                        println!("{}", mode_message());
327                        process::exit(0);
328                    }
329                    cfg.mode = ErgMode::try_from(&mode[..]).unwrap_or_else(|_| {
330                        eprintln!("invalid mode: {mode}");
331                        process::exit(1);
332                    });
333                }
334                "--use-package" => {
335                    let name = args
336                        .next()
337                        .expect("`name` of `--use-package` is not passed")
338                        .into_boxed_str();
339                    let as_name = args
340                        .next()
341                        .expect("`as_name` of `--use-package` is not passed")
342                        .into_boxed_str();
343                    let version = args
344                        .next()
345                        .expect("`version` of `--use-package` is not passed")
346                        .into_boxed_str();
347                    packages.push(Package::new(
348                        Box::leak(name),
349                        Box::leak(as_name),
350                        Box::leak(version),
351                        None,
352                    ));
353                }
354                "--use-pylyzer" => {
355                    cfg.use_pylyzer = true;
356                }
357                "--use-local-package" => {
358                    let name = args
359                        .next()
360                        .expect("`name` of `--use-package` is not passed")
361                        .into_boxed_str();
362                    let as_name = args
363                        .next()
364                        .expect("`as_name` of `--use-package` is not passed")
365                        .into_boxed_str();
366                    let version = args
367                        .next()
368                        .expect("`version` of `--use-package` is not passed")
369                        .into_boxed_str();
370                    let path = args
371                        .next()
372                        .expect("`path` of `--use-package` is not passed")
373                        .into_boxed_str();
374                    packages.push(Package::new(
375                        Box::leak(name),
376                        Box::leak(as_name),
377                        Box::leak(version),
378                        Some(Box::leak(path)),
379                    ));
380                }
381                "--ping" => {
382                    println!("pong");
383                    process::exit(0);
384                }
385                "--ps1" => {
386                    let ps1 = args
387                        .next()
388                        .expect("the value of `--ps1` is not passed")
389                        .into_boxed_str();
390                    cfg.ps1 = Box::leak(ps1);
391                }
392                "--ps2" => {
393                    let ps2 = args
394                        .next()
395                        .expect("the value of `--ps2` is not passed")
396                        .into_boxed_str();
397                    cfg.ps2 = Box::leak(ps2);
398                }
399                "-o" | "--opt-level" | "--optimization-level" => {
400                    cfg.opt_level = args
401                        .next()
402                        .expect("the value of `-o` is not passed")
403                        .parse::<u8>()
404                        .expect("the value of `-o` is not a number");
405                }
406                "--output-dir" | "--dest" | "--dist" | "--dest-dir" | "--dist-dir" => {
407                    let output_dir = args
408                        .next()
409                        .expect("the value of `--output-dir` is not passed")
410                        .into_boxed_str();
411                    cfg.dist_dir = Some(Box::leak(output_dir));
412                }
413                "--py-command" | "--python-command" => {
414                    let py_command = args
415                        .next()
416                        .expect("the value of `--py-command` is not passed")
417                        .parse::<String>()
418                        .expect("the value of `-py-command` is not a valid Python command");
419                    cfg.py_magic_num = Some(detect_magic_number(&py_command));
420                    cfg.target_version = get_python_version(&py_command);
421                    cfg.py_command = Some(Box::leak(py_command.into_boxed_str()));
422                }
423                "--hex-py-magic-num" | "--hex-python-magic-number" => {
424                    let s_hex_magic_num = args
425                        .next()
426                        .expect("the value of `--hex-py-magic-num` is not passed");
427                    let first_byte = u8::from_str_radix(&s_hex_magic_num[0..=1], 16).unwrap();
428                    let second_byte = u8::from_str_radix(&s_hex_magic_num[2..=3], 16).unwrap();
429                    let py_magic_num = get_magic_num_from_bytes(&[first_byte, second_byte, 0, 0]);
430                    cfg.py_magic_num = Some(py_magic_num);
431                    cfg.target_version = Some(get_ver_from_magic_num(py_magic_num));
432                }
433                "--py-magic-num" | "--python-magic-number" => {
434                    let py_magic_num = args
435                        .next()
436                        .expect("the value of `--py-magic-num` is not passed")
437                        .parse::<u32>()
438                        .expect("the value of `--py-magic-num` is not a number");
439                    cfg.py_magic_num = Some(py_magic_num);
440                    cfg.target_version = Some(get_ver_from_magic_num(py_magic_num));
441                }
442                "--py-server-timeout" => {
443                    cfg.py_server_timeout = args
444                        .next()
445                        .expect("the value of `--py-server-timeout` is not passed")
446                        .parse::<u64>()
447                        .expect("the value of `--py-server-timeout` is not a number");
448                }
449                "-q" | "--quiet-startup" | "--quiet-repl" => {
450                    cfg.quiet_repl = true;
451                }
452                "-t" | "--show-type" => {
453                    cfg.show_type = true;
454                }
455                "--target-version" => {
456                    let target_version = args
457                        .next()
458                        .expect("the value of `--target-version` is not passed")
459                        .parse::<PythonVersion>()
460                        .expect("the value of `--target-version` is not a valid Python version");
461                    cfg.target_version = Some(target_version);
462                }
463                "--transpile-target" | "--target" => {
464                    let transpile_target = args
465                        .next()
466                        .expect("the value of `--transpile-target` is not passed")
467                        .into_boxed_str();
468                    cfg.transpile_target = Some(TranspileTarget::from(&transpile_target[..]));
469                }
470                "-v" | "--verbose" => {
471                    cfg.verbose = args
472                        .next()
473                        .expect("the value of `--verbose` is not passed")
474                        .parse::<u8>()
475                        .expect("the value of `--verbose` is not a number");
476                }
477                "-V" | "--version" => {
478                    println!("Erg {}", env!("CARGO_PKG_VERSION"));
479                    process::exit(0);
480                }
481                "--build-features" => {
482                    #[cfg(feature = "debug")]
483                    print!("debug ");
484                    #[cfg(feature = "backtrace")]
485                    println!("backtrace");
486                    #[cfg(feature = "els")]
487                    print!("els ");
488                    #[cfg(feature = "py_compat")]
489                    print!("py_compat ");
490                    #[cfg(feature = "japanese")]
491                    print!("japanese ");
492                    #[cfg(feature = "simplified_chinese")]
493                    print!("simplified_chinese ");
494                    #[cfg(feature = "traditional_chinese")]
495                    print!("traditional_chinese ");
496                    #[cfg(feature = "unicode")]
497                    print!("unicode ");
498                    #[cfg(feature = "pretty")]
499                    print!("pretty ");
500                    #[cfg(feature = "large_thread")]
501                    print!("large_thread");
502                    #[cfg(feature = "no_std")]
503                    println!("no_std");
504                    #[cfg(feature = "full-repl")]
505                    println!("full-repl");
506                    #[cfg(feature = "experimental")]
507                    println!("experimental");
508                    #[cfg(feature = "pylib")]
509                    println!("pylib");
510                    #[cfg(feature = "log-level-error")]
511                    println!("log-level-error");
512                    #[cfg(feature = "parallel")]
513                    println!("parallel");
514                    println!();
515                    process::exit(0);
516                }
517                other if other.starts_with('-') => {
518                    if let Some(option) = get_similar_name(OPTIONS.iter().copied(), other) {
519                        eprintln!("invalid option: {other} (did you mean `{option}`?)");
520                    } else {
521                        eprintln!("invalid option: {other}");
522                    }
523                    eprintln!(
524                        "
525USAGE:
526    erg [OPTIONS] [SUBCOMMAND] [ARGS]...
527
528    For more information try `erg --help`"
529                    );
530                    process::exit(2);
531                }
532                _ => {
533                    if let Ok(mode) = ErgMode::try_from(&arg[..]) {
534                        cfg.mode = mode;
535                        if cfg.mode == ErgMode::Pack {
536                            for arg in args {
537                                runtime_args.push(Box::leak(arg.into_boxed_str()));
538                            }
539                            break;
540                        }
541                    } else {
542                        let path = PathBuf::from_str(&arg[..])
543                            .unwrap_or_else(|_| panic!("invalid file path: {arg}"));
544                        let path = normalize_path(path);
545                        cfg.input = Input::file(path);
546                        match args.next().as_ref().map(|s| &s[..]) {
547                            Some("--") => {
548                                for arg in args {
549                                    runtime_args.push(Box::leak(arg.into_boxed_str()));
550                                }
551                            }
552                            Some(some) => {
553                                println!("invalid argument: {some}");
554                                println!("Do not pass options after the file path. If you want to pass runtime arguments, use `--` before them.");
555                                process::exit(1);
556                            }
557                            _ => {}
558                        }
559                        break;
560                    }
561                }
562            }
563        }
564        if cfg.input.is_repl() && cfg.mode != ErgMode::LanguageServer {
565            let is_stdin_piped = !stdin().is_terminal();
566            let input = if is_stdin_piped {
567                let mut buffer = String::new();
568                stdin().read_to_string(&mut buffer).unwrap();
569                Input::pipe(buffer)
570            } else {
571                Input::repl()
572            };
573            cfg.input = input;
574        }
575        cfg.runtime_args = ArcArray::from(runtime_args);
576        cfg.packages = ArcArray::from(packages);
577        cfg
578    }
579}