Rust · 16359 bytes Raw Blame History
1 //! Compilation driver.
2 //!
3 //! CLI argument parsing, phase orchestration, multi-file compilation,
4 //! dependency resolution, and linker invocation.
5
6 use std::fs;
7 use std::path::{Path, PathBuf};
8 use std::process::Command;
9
10 use crate::codegen::{emit, isel, linearscan, peephole};
11 use crate::ir::{lower, printer as ir_printer, verify};
12 use crate::lexer::{detect_source_form, tokenize, SourceForm};
13 use crate::parser::Parser;
14 use crate::sema::{resolve, validate};
15
16 /// Optimization level requested at the CLI.
17 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18 pub enum OptLevel {
19 O0,
20 O1,
21 O2,
22 O3,
23 Os,
24 Ofast,
25 }
26
27 impl OptLevel {
28 pub fn parse_flag(flag: &str) -> Option<Self> {
29 match flag.to_ascii_lowercase().as_str() {
30 "o0" => Some(Self::O0),
31 "o1" => Some(Self::O1),
32 "o2" => Some(Self::O2),
33 "o3" => Some(Self::O3),
34 "os" => Some(Self::Os),
35 "ofast" => Some(Self::Ofast),
36 _ => None,
37 }
38 }
39
40 pub fn as_flag(&self) -> &'static str {
41 match self {
42 Self::O0 => "-O0",
43 Self::O1 => "-O1",
44 Self::O2 => "-O2",
45 Self::O3 => "-O3",
46 Self::Os => "-Os",
47 Self::Ofast => "-Ofast",
48 }
49 }
50
51 pub fn as_str(&self) -> &'static str {
52 match self {
53 Self::O0 => "O0",
54 Self::O1 => "O1",
55 Self::O2 => "O2",
56 Self::O3 => "O3",
57 Self::Os => "Os",
58 Self::Ofast => "Ofast",
59 }
60 }
61 }
62
63 /// Compilation options.
64 pub struct Options {
65 pub input: PathBuf,
66 pub output: Option<PathBuf>,
67 pub emit_asm: bool, // -S
68 pub emit_obj: bool, // -c
69 pub emit_ir: bool, // --emit-ir
70 pub preprocess_only: bool, // -E
71 pub opt_level: OptLevel, // -O0 .. -Ofast
72 }
73
74 impl Options {
75 pub fn from_args(args: &[String]) -> Result<Self, String> {
76 let mut input = None;
77 let mut output = None;
78 let mut emit_asm = false;
79 let mut emit_obj = false;
80 let mut emit_ir = false;
81 let mut preprocess_only = false;
82 let mut opt_level = OptLevel::O0;
83
84 let mut i = 0;
85 while i < args.len() {
86 match args[i].as_str() {
87 "-o" => {
88 i += 1;
89 if i < args.len() {
90 output = Some(PathBuf::from(&args[i]));
91 } else {
92 return Err("-o requires an argument".into());
93 }
94 }
95 "-S" => emit_asm = true,
96 "-c" => emit_obj = true,
97 "-E" => preprocess_only = true,
98 "--emit-ir" => emit_ir = true,
99 arg if arg.starts_with("-O") => {
100 let tail = &arg[1..];
101 opt_level = OptLevel::parse_flag(tail)
102 .ok_or_else(|| format!("unknown optimization level: {}", arg))?;
103 }
104 arg if !arg.starts_with('-') => {
105 input = Some(PathBuf::from(arg));
106 }
107 other => return Err(format!("unknown option: {}", other)),
108 }
109 i += 1;
110 }
111
112 let input = input.ok_or("no input file")?;
113 Ok(Self {
114 input,
115 output,
116 emit_asm,
117 emit_obj,
118 emit_ir,
119 preprocess_only,
120 opt_level,
121 })
122 }
123
124 /// Determine the output path based on input and flags.
125 pub fn output_path(&self) -> PathBuf {
126 if let Some(ref o) = self.output {
127 return o.clone();
128 }
129 let stem = self
130 .input
131 .file_stem()
132 .unwrap_or_default()
133 .to_str()
134 .unwrap_or("a");
135 if self.emit_asm {
136 PathBuf::from(format!("{}.s", stem))
137 } else if self.emit_obj {
138 PathBuf::from(format!("{}.o", stem))
139 } else if self.emit_ir {
140 PathBuf::from(format!("{}.ir", stem))
141 } else {
142 PathBuf::from(stem)
143 }
144 }
145 }
146
147 /// Compile a Fortran source file through the full pipeline.
148 pub fn compile(opts: &Options) -> Result<(), String> {
149 // 1. Read source.
150 let source = fs::read_to_string(&opts.input)
151 .map_err(|e| format!("cannot read '{}': {}", opts.input.display(), e))?;
152
153 // 2. Preprocess.
154 let source_form = detect_source_form(&opts.input.to_string_lossy());
155 let pp_config = crate::preprocess::PreprocConfig {
156 filename: opts.input.to_str().unwrap_or("<input>").to_string(),
157 fixed_form: matches!(source_form, SourceForm::FixedForm),
158 ..crate::preprocess::PreprocConfig::default()
159 };
160 let pp_result =
161 crate::preprocess::preprocess(&source, &pp_config).map_err(|e| format!("{}", e))?;
162 let preprocessed = pp_result.text;
163
164 if opts.preprocess_only {
165 let out = opts.output_path();
166 if out.as_os_str() == "-" {
167 print!("{}", preprocessed);
168 } else {
169 fs::write(&out, &preprocessed)
170 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
171 }
172 return Ok(());
173 }
174
175 // 3. Lex.
176 let tokens = tokenize(&preprocessed, 0, source_form).map_err(|e| {
177 format!(
178 "{}:{}:{}: lexer error: {}",
179 opts.input.display(),
180 e.span.start.line,
181 e.span.start.col,
182 e.msg
183 )
184 })?;
185
186 // 4. Parse.
187 let mut parser = Parser::new(&tokens);
188 let units = parser.parse_file().map_err(|e| {
189 format!(
190 "{}:{}:{}: parse error: {}",
191 opts.input.display(),
192 e.span.start.line,
193 e.span.start.col,
194 e.msg
195 )
196 })?;
197
198 // 5. Semantic analysis.
199 let (st, type_layouts) = resolve::resolve_file(&units).map_err(|e| {
200 format!(
201 "{}:{}:{}: {}",
202 opts.input.display(),
203 e.span.start.line,
204 e.span.start.col,
205 e.msg
206 )
207 })?;
208 let diags = validate::validate_file(&units, &st);
209 for d in &diags {
210 if d.kind == validate::DiagKind::Error {
211 return Err(format!(
212 "{}:{}:{}: error: {}",
213 opts.input.display(),
214 d.span.start.line,
215 d.span.start.col,
216 d.msg
217 ));
218 }
219 }
220
221 // 6. Lower to IR.
222 let mut ir_module = lower::lower_file(&units, &st, &type_layouts);
223 let ir_errors = verify::verify_module(&ir_module);
224 if !ir_errors.is_empty() {
225 let msg = ir_errors
226 .iter()
227 .map(|e| e.to_string())
228 .collect::<Vec<_>>()
229 .join("\n");
230 return Err(format!("internal error: IR verification failed:\n{}", msg));
231 }
232 let module_has_i128 = ir_module.contains_i128();
233 if ir_module.contains_i128_outside_globals() && opts.opt_level != OptLevel::O0 {
234 return Err(
235 "integer(16) / i128 optimization is not yet supported; use -O0 --emit-ir for now"
236 .into(),
237 );
238 }
239
240 // 6.5. Run IR optimization pipeline.
241 //
242 // This is where const_fold, mem2reg, LICM, DSE, loop unrolling, and
243 // every other IR-level pass actually fire. At O0 the pipeline is empty
244 // so nothing changes. The pipeline runs to fixpoint; the pass manager
245 // verifies the IR after every pass.
246 {
247 use crate::opt::pipeline::OptLevel as IrOpt;
248 let ir_opt = match opts.opt_level {
249 OptLevel::O0 => IrOpt::O0,
250 OptLevel::O1 => IrOpt::O1,
251 OptLevel::O2 => IrOpt::O2,
252 OptLevel::O3 => IrOpt::O3,
253 OptLevel::Os => IrOpt::Os,
254 OptLevel::Ofast => IrOpt::Ofast,
255 };
256 let pm = crate::opt::build_pipeline(ir_opt);
257 pm.run(&mut ir_module);
258 }
259
260 if opts.emit_ir {
261 let ir_text = ir_printer::print_module(&ir_module);
262 let out = opts.output_path();
263 fs::write(&out, &ir_text)
264 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
265 return Ok(());
266 }
267
268 if module_has_i128 && !ir_module.i128_backend_data_only_supported() {
269 return Err(
270 "backend does not yet support integer(16) / i128 codegen; use --emit-ir for now"
271 .into(),
272 );
273 }
274
275 // 7. Instruction selection.
276 let machine_funcs = isel::select_module(&ir_module);
277
278 // 7.5. Backend peephole (O2+): FMA fusion, etc.
279 let mut allocated: Vec<_> = machine_funcs;
280 if opts.opt_level >= OptLevel::O2 {
281 for mf in &mut allocated {
282 peephole::run_peephole(mf);
283 }
284 }
285
286 // 8. Register allocation (linear scan).
287 for mf in &mut allocated {
288 let liveness = crate::codegen::liveness::compute_liveness(mf);
289 let result = linearscan::linear_scan(mf);
290 linearscan::apply_allocation(mf, &result, &liveness);
291 linearscan::insert_callee_saves(mf, &result.callee_saved_used);
292 linearscan::coalesce_moves(mf);
293 // 8.5. Tail call optimization (O1+): BL + epilogue → epilogue + B.
294 // Runs after regalloc so we can inspect physical register assignments.
295 if opts.opt_level >= OptLevel::O1 {
296 crate::codegen::tailcall::tail_call_opt(mf);
297 }
298 }
299
300 // 9. Emit assembly.
301 let mut asm_text = String::new();
302 asm_text.push_str(".section __TEXT,__text,regular,pure_instructions\n");
303 for mf in &allocated {
304 // Re-emit __TEXT section before each function in case the previous
305 // function's constant pool switched to __DATA.
306 asm_text.push_str(".section __TEXT,__text,regular,pure_instructions\n");
307 asm_text.push_str(&emit::emit_function(mf));
308 asm_text.push('\n');
309 }
310
311 // Emit module-level globals (SAVE'd locals + module variables)
312 // into a __DATA,__data section. Must come before _main so the
313 // labels are defined when functions reference them.
314 if !ir_module.globals.is_empty() {
315 asm_text.push_str(&emit::emit_globals(&ir_module.globals));
316 asm_text.push('\n');
317 }
318
319 // Emit _main entry point (must be in __TEXT section).
320 if let Some(user_func) = allocated.first() {
321 if user_func.name != "main" {
322 asm_text.push_str("\n.section __TEXT,__text,regular,pure_instructions\n");
323 asm_text.push_str(&format!(
324 "\
325 .globl _main
326 .p2align 2
327 _main:
328 stp x29, x30, [sp, #-16]!
329 mov x29, sp
330 bl _afs_program_init
331 bl _{0}
332 bl _afs_program_finalize
333 mov x0, #0
334 ldp x29, x30, [sp], #16
335 ret
336 ",
337 user_func.name
338 ));
339 }
340 }
341
342 if opts.emit_asm {
343 let out = opts.output_path();
344 fs::write(&out, &asm_text)
345 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
346 return Ok(());
347 }
348
349 // 10. Assemble (using system assembler for now).
350 let pid = std::process::id();
351 let asm_path = std::env::temp_dir().join(format!("armfortas_{}.s", pid));
352 let obj_path = if opts.emit_obj {
353 opts.output_path()
354 } else {
355 std::env::temp_dir().join(format!("armfortas_{}.o", pid))
356 };
357
358 fs::write(&asm_path, &asm_text).map_err(|e| format!("cannot write temp assembly: {}", e))?;
359
360 let as_result = Command::new("as")
361 .args(["-o", obj_path.to_str().unwrap(), asm_path.to_str().unwrap()])
362 .output()
363 .map_err(|e| format!("cannot run assembler: {}", e))?;
364
365 if !as_result.status.success() {
366 let stderr = String::from_utf8_lossy(&as_result.stderr);
367 return Err(format!("assembler failed:\n{}", stderr));
368 }
369
370 if opts.emit_obj {
371 return Ok(());
372 }
373
374 // 11. Link.
375 let binary_path = opts.output_path();
376 link(&obj_path, &binary_path)?;
377
378 // Cleanup.
379 let _ = fs::remove_file(&asm_path);
380 let _ = fs::remove_file(&obj_path);
381
382 Ok(())
383 }
384
385 /// Link an object file with the runtime library to produce a binary.
386 fn link(obj: &Path, output: &Path) -> Result<(), String> {
387 // Find the runtime library.
388 let rt_path = find_runtime_lib()?;
389
390 // Find the SDK sysroot.
391 let sdk = Command::new("xcrun")
392 .args(["--show-sdk-path"])
393 .output()
394 .map_err(|e| format!("cannot run xcrun: {}", e))?;
395 let sysroot = String::from_utf8_lossy(&sdk.stdout).trim().to_string();
396
397 let ld_result = Command::new("ld")
398 .args([
399 obj.to_str().unwrap(),
400 &rt_path,
401 "-lSystem",
402 "-no_uuid",
403 "-syslibroot",
404 &sysroot,
405 "-e",
406 "_main",
407 "-o",
408 output.to_str().unwrap(),
409 ])
410 .output()
411 .map_err(|e| format!("cannot run linker: {}", e))?;
412
413 if !ld_result.status.success() {
414 let stderr = String::from_utf8_lossy(&ld_result.stderr);
415 return Err(format!("linker failed:\n{}", stderr));
416 }
417
418 Ok(())
419 }
420
421 /// Find libarmfortas_rt.a in common locations.
422 fn find_runtime_lib() -> Result<String, String> {
423 // Check next to the compiler binary.
424 if let Ok(exe) = std::env::current_exe() {
425 let dir = exe.parent().unwrap_or(Path::new("."));
426 let candidate = dir.join("libarmfortas_rt.a");
427 if candidate.exists() {
428 return Ok(candidate.to_str().unwrap().to_string());
429 }
430 }
431
432 // Check cargo target directory (for development).
433 let candidates = [
434 "target/debug/libarmfortas_rt.a",
435 "target/release/libarmfortas_rt.a",
436 "../target/debug/libarmfortas_rt.a",
437 ];
438 for c in &candidates {
439 if Path::new(c).exists() {
440 return Ok(c.to_string());
441 }
442 }
443
444 Err("cannot find libarmfortas_rt.a — build with 'cargo build -p armfortas-rt'".into())
445 }
446
447 #[cfg(test)]
448 mod tests {
449 use super::*;
450 use std::fs;
451
452 #[test]
453 fn parses_os_optimization_flag() {
454 assert_eq!(OptLevel::parse_flag("Os"), Some(OptLevel::Os));
455 assert_eq!(OptLevel::parse_flag("os"), Some(OptLevel::Os));
456 assert_eq!(OptLevel::Os.as_flag(), "-Os");
457 assert_eq!(OptLevel::Os.as_str(), "Os");
458 }
459
460 #[test]
461 fn options_from_args_accepts_os() {
462 let args = vec!["-Os".to_string(), "hello.f90".to_string()];
463 let opts = Options::from_args(&args).expect("driver should accept -Os");
464 assert_eq!(opts.opt_level, OptLevel::Os);
465 assert_eq!(opts.input, PathBuf::from("hello.f90"));
466 }
467
468 fn i128_fixture() -> PathBuf {
469 let path = PathBuf::from("tests/fixtures").join("integer16_ir.f90");
470 assert!(path.exists(), "missing test fixture {}", path.display());
471 path
472 }
473
474 #[test]
475 fn emit_ir_allows_integer16_staging_at_o0() {
476 let output = std::env::temp_dir().join(format!(
477 "armfortas_i128_ir_{}_{}.ir",
478 std::process::id(),
479 "o0"
480 ));
481 let opts = Options {
482 input: i128_fixture(),
483 output: Some(output.clone()),
484 emit_asm: false,
485 emit_obj: false,
486 emit_ir: true,
487 preprocess_only: false,
488 opt_level: OptLevel::O0,
489 };
490
491 compile(&opts).expect("O0 --emit-ir should support integer(16) staging");
492 let ir = fs::read_to_string(&output).expect("missing emitted IR");
493 assert!(ir.contains("i128"), "emitted IR should expose integer(16) as i128:\n{}", ir);
494 let _ = fs::remove_file(output);
495 }
496
497 #[test]
498 fn backend_rejects_integer16_codegen_for_now() {
499 let output = std::env::temp_dir().join(format!(
500 "armfortas_i128_bin_{}_{}",
501 std::process::id(),
502 "o0"
503 ));
504 let opts = Options {
505 input: i128_fixture(),
506 output: Some(output),
507 emit_asm: false,
508 emit_obj: false,
509 emit_ir: false,
510 preprocess_only: false,
511 opt_level: OptLevel::O0,
512 };
513
514 let err = compile(&opts).expect_err("backend should reject integer(16) until i128 codegen lands");
515 assert!(
516 err.contains("backend does not yet support integer(16) / i128 codegen"),
517 "unexpected backend rejection:\n{}",
518 err
519 );
520 }
521 }
522