cl_repl/
cli.rs

1//! Implement's the command line interface
2use crate::{
3    args::{Args, Mode},
4    ctx::Context,
5    menu,
6    tools::print_token,
7};
8use cl_ast::File;
9use cl_interpret::{builtin::builtins, convalue::ConValue, env::Environment, interpret::Interpret};
10use cl_lexer::Lexer;
11use cl_parser::Parser;
12use std::{borrow::Cow, error::Error, path::Path};
13
14/// Run the command line interface
15pub fn run(args: Args) -> Result<(), Box<dyn Error>> {
16    let Args { file, include, mode, repl } = args;
17
18    let mut env = Environment::new();
19
20    env.add_builtins(&builtins! {
21        /// Lexes, parses, and evaluates an expression in the current env
22        fn eval(string) @env {
23            use cl_interpret::error::Error;
24            let string = match string {
25                ConValue::Str(string) => string.to_ref(),
26                ConValue::String(string) => string.as_str(),
27                ConValue::Ref(v) => {
28                    let string = env.get_id(*v).cloned().unwrap_or_default();
29                    return eval(env, &[string])
30                }
31                _ => Err(Error::TypeError())?
32            };
33            match Parser::new("eval", Lexer::new(string)).parse::<cl_ast::Stmt>() {
34                Err(e) => Ok(ConValue::Str(format!("{e}").into())),
35                Ok(v) => v.interpret(env),
36            }
37        }
38
39        /// Executes a file
40        fn import(path) @env {
41            use cl_interpret::error::Error;
42            match path {
43                ConValue::Str(path) => load_file(env, &**path).or(Ok(ConValue::Empty)),
44                ConValue::String(path) => load_file(env, &**path).or(Ok(ConValue::Empty)),
45                _ => Err(Error::TypeError())
46            }
47        }
48
49        fn putchar(ConValue::Char(c)) {
50            print!("{c}");
51            Ok(ConValue::Empty)
52        }
53
54        /// Gets a line of input from stdin
55        fn get_line(prompt) {
56            use cl_interpret::error::Error;
57            let prompt = match prompt {
58                ConValue::Str(prompt) => prompt.to_ref(),
59                ConValue::String(prompt) => prompt.as_str(),
60                _ => Err(Error::TypeError())?,
61            };
62            match repline::Repline::new("", prompt, "").read() {
63                Ok(line) => Ok(ConValue::String(line)),
64                Err(repline::Error::CtrlD(line)) => Ok(ConValue::String(line)),
65                Err(repline::Error::CtrlC(_)) => Err(cl_interpret::error::Error::Break(ConValue::Empty)),
66                Err(e) => Ok(ConValue::Str(e.to_string().into())),
67            }
68        }
69    });
70
71    for path in include {
72        load_file(&mut env, path)?;
73    }
74
75    if repl {
76        if let Some(file) = file
77            && let Err(e) = load_file(&mut env, file)
78        {
79            eprintln!("{e}")
80        }
81        let mut ctx = Context::with_env(env);
82        menu::main_menu(mode, &mut ctx)?;
83    } else {
84        let path = format_path_for_display(file.as_deref());
85        let code = match &file {
86            Some(file) => std::fs::read_to_string(file)?,
87            None => std::io::read_to_string(std::io::stdin())?,
88        };
89
90        match mode {
91            Mode::Lex => lex_code(&path, &code),
92            Mode::Fmt => fmt_code(&path, &code),
93            Mode::Run => run_code(&path, &code, &mut env),
94        }?;
95    }
96    Ok(())
97}
98
99fn format_path_for_display(path: Option<&Path>) -> Cow<'_, str> {
100    match path {
101        Some(file) => file
102            .to_str()
103            .map(Cow::Borrowed)
104            .unwrap_or_else(|| Cow::Owned(file.display().to_string())),
105        None => Cow::Borrowed(""),
106    }
107}
108
109fn load_file(env: &mut Environment, path: impl AsRef<Path>) -> Result<ConValue, Box<dyn Error>> {
110    let path = path.as_ref();
111    let inliner = cl_parser::inliner::ModuleInliner::new(path.with_extension(""));
112    let file = std::fs::read_to_string(path)?;
113    let code = Parser::new(path.display().to_string(), Lexer::new(&file)).parse()?;
114    let code = match inliner.inline(code) {
115        Ok(a) => a,
116        Err((code, io_errs, parse_errs)) => {
117            for (file, err) in io_errs {
118                eprintln!("{}:{err}", file.display());
119            }
120            for (file, err) in parse_errs {
121                eprintln!("{}:{err}", file.display());
122            }
123            code
124        }
125    };
126    use cl_ast::WeightOf;
127    eprintln!("File {} weighs {} units", code.name, code.weight_of());
128
129    match env.eval(&code) {
130        Ok(v) => Ok(v),
131        Err(e) => {
132            eprintln!("{e}");
133            Ok(ConValue::Empty)
134        }
135    }
136}
137
138fn lex_code(path: &str, code: &str) -> Result<(), Box<dyn Error>> {
139    for token in Lexer::new(code) {
140        if !path.is_empty() {
141            print!("{}:", path);
142        }
143        match token {
144            Ok(token) => print_token(&token),
145            Err(e) => println!("{e}"),
146        }
147    }
148    Ok(())
149}
150
151fn fmt_code(path: &str, code: &str) -> Result<(), Box<dyn Error>> {
152    let code = Parser::new(path, Lexer::new(code)).parse::<File>()?;
153    println!("{code}");
154    Ok(())
155}
156
157fn run_code(path: &str, code: &str, env: &mut Environment) -> Result<(), Box<dyn Error>> {
158    let code = Parser::new(path, Lexer::new(code)).parse::<File>()?;
159    match code.interpret(env)? {
160        ConValue::Empty => {}
161        ret => println!("{ret}"),
162    }
163    if env.get("main".into()).is_ok() {
164        match env.call("main".into(), &[]) {
165            Ok(ConValue::Empty) => {}
166            Ok(ret) => println!("{ret}"),
167            Err(e) => println!("Error: {e}"),
168        }
169    }
170    Ok(())
171}