1use amarok_syntax::{Diagnostic, FileId, SourceMap};
8use std::env;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12const USAGE: &str = "Usage: amarok_cli [--no-std] [--stdlib <dir>] <path-to-file.amarok>";
13
14fn main() {
15 if let Err(message) = run() {
16 eprintln!("{message}");
17 std::process::exit(1);
18 }
19}
20
21fn run() -> Result<(), String> {
22 let mut arguments = env::args().skip(1);
23
24 let mut no_std = false;
25 let mut stdlib_override: Option<PathBuf> = None;
26 let mut path: Option<String> = None;
27
28 while let Some(argument) = arguments.next() {
29 match argument.as_str() {
30 "--no-std" => no_std = true,
31 "--stdlib" => {
32 let value = arguments
33 .next()
34 .ok_or_else(|| format!("--stdlib requires a directory.\n{USAGE}"))?;
35 stdlib_override = Some(PathBuf::from(value));
36 }
37 "--" => {
38 path = arguments.next();
39 break;
40 }
41 other if other.starts_with("--") => {
42 return Err(format!("Unknown option: {other}\n{USAGE}"));
43 }
44 _ => {
45 path = Some(argument);
46 break;
47 }
48 }
49 }
50
51 let Some(path) = path else {
52 return Err(format!("Missing source file.\n{USAGE}"));
53 };
54
55 if arguments.next().is_some() {
56 return Err(format!("Too many arguments.\n{USAGE}"));
57 }
58
59 let entry_path = PathBuf::from(&path);
60 let source = fs::read_to_string(&entry_path)
61 .map_err(|error| format!("Amarok error: Failed to read file {path}: {error}"))?;
62
63 let mut source_map = SourceMap::new();
66 let entry_canonical = entry_path
67 .canonicalize()
68 .unwrap_or_else(|_| entry_path.clone());
69 let entry_file_id = source_map.add_file(entry_canonical.clone(), source.clone());
70
71 let program = match amarok_parser::parse_program_with_file_id(&source, entry_file_id) {
72 Ok(program) => program,
73 Err(error) => {
74 return Err(format!(
75 "Amarok error: {}",
76 render_diagnostic(&source_map, &entry_path, &source, &error)
77 ));
78 }
79 };
80
81 let mut interpreter = if no_std {
82 amarok_interpreter::Interpreter::new_no_std()
83 } else {
84 amarok_interpreter::Interpreter::new()
85 };
86
87 let project_root = entry_canonical
89 .parent()
90 .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
91 interpreter.add_module_root(project_root);
92
93 if let Some(dir) = stdlib_override
94 .or_else(|| env::var_os("AMAROK_STDLIB").map(PathBuf::from))
95 .or_else(default_stdlib_dir)
96 {
97 interpreter.add_module_root(dir);
98 }
99
100 interpreter.set_source_map(source_map);
101
102 let result = interpreter.run_program(&program);
103
104 for line in interpreter.output_lines() {
106 println!("{line}");
107 }
108
109 if let Err(error) = result {
110 let source_map = interpreter.source_map();
111 return Err(format!(
112 "Amarok error: {}",
113 render_diagnostic(source_map, &entry_path, &source, &error)
114 ));
115 }
116
117 Ok(())
118}
119
120fn default_stdlib_dir() -> Option<PathBuf> {
121 let manifest_dir = env!("CARGO_MANIFEST_DIR");
123 let candidate = Path::new(manifest_dir).join("..").join("..").join("stdlib");
124 if candidate.is_dir() {
125 Some(candidate)
126 } else {
127 None
128 }
129}
130
131fn line_col_from_offset(source: &str, offset: usize) -> (usize, usize) {
134 let mut line = 1usize;
135 let mut col = 1usize;
136
137 for (index, ch) in source.char_indices() {
138 if index >= offset {
139 break;
140 }
141 if ch == '\n' {
142 line += 1;
143 col = 1;
144 } else {
145 col += 1;
146 }
147 }
148
149 (line, col)
150}
151
152fn line_text_and_line_start(source: &str, line_number: usize) -> Option<(&str, usize)> {
153 let mut current_line = 1usize;
154 let mut line_start = 0usize;
155
156 for (index, ch) in source.char_indices() {
157 if current_line == line_number && ch == '\n' {
158 return Some((&source[line_start..index], line_start));
159 }
160 if ch == '\n' {
161 current_line += 1;
162 line_start = index + 1;
163 }
164 }
165
166 if current_line == line_number {
167 Some((&source[line_start..], line_start))
168 } else {
169 None
170 }
171}
172
173fn render_diagnostic(
174 source_map: &SourceMap,
175 entry_path: &Path,
176 entry_source: &str,
177 diagnostic: &Diagnostic,
178) -> String {
179 let Some(span) = diagnostic.span else {
180 return format!("{}: {}\n", entry_path.display(), diagnostic.message);
181 };
182
183 let (path_display, source_text): (String, &str) =
186 if span.file_id != FileId::DUMMY && source_map.get(span.file_id).is_some() {
187 let file = source_map.get(span.file_id).unwrap();
188 (file.path.display().to_string(), file.source.as_str())
189 } else {
190 (entry_path.display().to_string(), entry_source)
191 };
192
193 let (line, col) = line_col_from_offset(source_text, span.start);
194 let mut output = format!("{path_display}:{line}:{col}: {}\n", diagnostic.message);
195
196 if let Some((line_text, line_start)) = line_text_and_line_start(source_text, line) {
197 output.push_str(line_text);
198 output.push('\n');
199
200 let caret_start = span.start.saturating_sub(line_start);
201 let caret_end = span.end.saturating_sub(line_start).max(caret_start + 1);
202
203 output.push_str(&" ".repeat(caret_start));
204 output.push_str(&"^".repeat(caret_end - caret_start));
205 output.push('\n');
206 }
207
208 output
209}