This repository has no description
3.8 kB
130 lines
1use rustyline::error::ReadlineError;
2use rustyline::DefaultEditor;
3use std::env;
4use std::path::{Path, PathBuf};
5use std::process::{Command, ExitCode};
6
7fn repo_root() -> Result<PathBuf, String> {
8 env::current_dir().map_err(|err| format!("failed to get current directory: {err}"))
9}
10
11fn mlang_binary(root: &Path) -> PathBuf {
12 root.join(".lake").join("build").join("bin").join("mlang")
13}
14
15fn eval(binary: &Path, input: &str) -> Result<(), String> {
16 let output = Command::new(binary)
17 .arg("--eval")
18 .arg(input)
19 .output()
20 .map_err(|err| format!("failed to run {}: {err}", binary.display()))?;
21
22 if !output.stdout.is_empty() {
23 print!("{}", String::from_utf8_lossy(&output.stdout));
24 }
25 if !output.stderr.is_empty() {
26 eprint!("{}", String::from_utf8_lossy(&output.stderr));
27 }
28
29 if output.status.success() {
30 Ok(())
31 } else {
32 Err(format!("mlang exited with {}", output.status))
33 }
34}
35
36fn parse_bare_let_binding(input: &str) -> Option<(&str, &str)> {
37 let rest = input.strip_prefix("let ")?;
38 let eq_idx = rest.find('=')?;
39 let name = rest[..eq_idx].trim();
40 if name.is_empty()
41 || !name
42 .chars()
43 .all(|c| c == '_' || c.is_ascii_alphanumeric())
44 || !name
45 .chars()
46 .next()
47 .is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
48 {
49 return None;
50 }
51
52 let value = rest[eq_idx + 1..].trim();
53 if value.is_empty() || input.contains(';') {
54 return None;
55 }
56
57 Some((name, value))
58}
59
60fn render_repl_input(prelude: &[String], input: &str) -> String {
61 let body = match parse_bare_let_binding(input) {
62 Some((name, value)) => format!("let {name} = {value} in {name}"),
63 None => input.to_owned(),
64 };
65
66 if prelude.is_empty() {
67 body
68 } else {
69 let mut rendered = prelude
70 .iter()
71 .rev()
72 .fold(body, |acc, binding| format!("{binding} in {acc}"));
73 rendered.shrink_to_fit();
74 rendered
75 }
76}
77
78fn main() -> ExitCode {
79 let root = match repo_root() {
80 Ok(root) => root,
81 Err(err) => {
82 eprintln!("{err}");
83 return ExitCode::FAILURE;
84 }
85 };
86 let binary = mlang_binary(&root);
87 if !binary.is_file() {
88 eprintln!("missing evaluator binary at {}", binary.display());
89 return ExitCode::FAILURE;
90 }
91
92 let mut rl = match DefaultEditor::new() {
93 Ok(rl) => rl,
94 Err(err) => {
95 eprintln!("failed to initialize line editor: {err}");
96 return ExitCode::FAILURE;
97 }
98 };
99
100 println!("mlang REPL. enter :quit to exit.");
101 let mut prelude: Vec<String> = Vec::new();
102 loop {
103 match rl.readline("mlang> ") {
104 Ok(line) => {
105 let input = line.trim();
106 if input.is_empty() {
107 continue;
108 }
109 if input == ":quit" || input == ":q" {
110 return ExitCode::SUCCESS;
111 }
112 let _ = rl.add_history_entry(input);
113 let rendered = render_repl_input(&prelude, input);
114 if eval(&binary, &rendered).is_ok() {
115 if let Some((name, value)) = parse_bare_let_binding(input) {
116 prelude.push(format!("let {name} = {value}"));
117 }
118 }
119 }
120 Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
121 println!();
122 return ExitCode::SUCCESS;
123 }
124 Err(err) => {
125 eprintln!("readline error: {err}");
126 return ExitCode::FAILURE;
127 }
128 }
129 }
130}