Rust · 54916 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 pub mod dep_scan;
7
8 use std::fs;
9 use std::path::{Path, PathBuf};
10 use std::process::Command;
11 use std::time::SystemTime;
12
13 use crate::codegen::{emit, isel, linearscan, peephole};
14 use crate::codegen::mir::MachineFunction;
15 use crate::ir::{lower, printer as ir_printer, verify};
16 use crate::lexer::{detect_source_form, tokenize, SourceForm};
17 use crate::parser::Parser;
18 use crate::sema::{resolve, validate};
19
20 /// Optimization level requested at the CLI.
21 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22 pub enum OptLevel {
23 O0,
24 O1,
25 O2,
26 O3,
27 Os,
28 Ofast,
29 }
30
31 impl OptLevel {
32 pub fn parse_flag(flag: &str) -> Option<Self> {
33 match flag.to_ascii_lowercase().as_str() {
34 "o0" => Some(Self::O0),
35 "o1" => Some(Self::O1),
36 "o2" => Some(Self::O2),
37 "o3" => Some(Self::O3),
38 "os" => Some(Self::Os),
39 "ofast" => Some(Self::Ofast),
40 _ => None,
41 }
42 }
43
44 pub fn as_flag(&self) -> &'static str {
45 match self {
46 Self::O0 => "-O0",
47 Self::O1 => "-O1",
48 Self::O2 => "-O2",
49 Self::O3 => "-O3",
50 Self::Os => "-Os",
51 Self::Ofast => "-Ofast",
52 }
53 }
54
55 pub fn as_str(&self) -> &'static str {
56 match self {
57 Self::O0 => "O0",
58 Self::O1 => "O1",
59 Self::O2 => "O2",
60 Self::O3 => "O3",
61 Self::Os => "Os",
62 Self::Ofast => "Ofast",
63 }
64 }
65 }
66
67 /// Source-form override requested on the command line. None means
68 /// detect from the file extension (.f90 → free, .f / .for → fixed).
69 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
70 pub enum SourceFormOverride {
71 Free,
72 Fixed,
73 }
74
75 /// Action that should run when args parsing completes successfully
76 /// without producing a compile job (e.g. --help, --version).
77 #[derive(Debug, Clone, PartialEq, Eq)]
78 pub enum InfoAction {
79 Help,
80 Version,
81 DumpVersion,
82 }
83
84 /// Result of parsing CLI args — either a real compile job or an
85 /// informational request.
86 pub enum ParsedCli {
87 Compile(Box<Options>),
88 Info(InfoAction),
89 }
90
91 /// Compilation options.
92 pub struct Options {
93 // ---- I/O ----
94 pub input: PathBuf,
95 /// Additional input files for multi-source mode.
96 pub extra_inputs: Vec<PathBuf>,
97 pub output: Option<PathBuf>,
98
99 // ---- Mode ----
100 pub emit_asm: bool, // -S
101 pub emit_obj: bool, // -c
102 pub emit_ir: bool, // --emit-ir
103 pub emit_ast: bool, // --emit-ast
104 pub emit_tokens: bool, // --emit-tokens
105 pub preprocess_only: bool, // -E
106
107 // ---- Language ----
108 pub std: Option<crate::sema::validate::FortranStandard>,
109 pub source_form_override: Option<SourceFormOverride>,
110 pub default_integer_8: bool,
111 pub default_real_8: bool,
112 pub force_implicit_none: bool,
113 pub recursive_default: bool,
114 pub backslash_escapes: bool,
115 pub max_stack_var_size: Option<u64>,
116
117 // ---- Optimization ----
118 pub opt_level: OptLevel,
119
120 // ---- Warnings ----
121 pub warn_all: bool,
122 pub warn_extra: bool,
123 pub warn_pedantic: bool,
124 pub warn_deprecated: bool,
125 pub warn_as_error: bool,
126 pub disabled_warnings: Vec<String>,
127
128 // ---- Debug / introspection ----
129 pub debug_info: bool, // -g (accepted; DWARF deferred)
130 pub verbose: bool, // -v
131 pub time_report: bool, // --time-report
132 pub diagnostics_format: DiagnosticsFormat, // --diagnostics-format=
133 pub check_bounds: bool, // -fcheck=bounds
134 pub check_all: bool, // -fcheck=all
135
136 // ---- Search paths / linking ----
137 /// Directories to search for `.amod` module files (`-I <dir>`).
138 pub module_search_paths: Vec<PathBuf>,
139 /// Directory to write generated `.amod` files (`-J <dir>`).
140 pub module_output_dir: Option<PathBuf>,
141 /// `-L <dir>` library search paths passed to `ld`.
142 pub library_search_paths: Vec<PathBuf>,
143 /// `-l<name>` libraries passed to `ld`.
144 pub link_libs: Vec<String>,
145 /// `-shared` / `-static`.
146 pub shared: bool,
147 pub static_link: bool,
148 /// `-rpath` entries passed to `ld`.
149 pub rpath: Vec<PathBuf>,
150 }
151
152 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
153 pub enum DiagnosticsFormat {
154 Text,
155 Json,
156 }
157
158 impl Default for Options {
159 fn default() -> Self {
160 Self {
161 input: PathBuf::new(),
162 extra_inputs: Vec::new(),
163 output: None,
164 emit_asm: false,
165 emit_obj: false,
166 emit_ir: false,
167 emit_ast: false,
168 emit_tokens: false,
169 preprocess_only: false,
170 std: None,
171 source_form_override: None,
172 default_integer_8: false,
173 default_real_8: false,
174 force_implicit_none: false,
175 recursive_default: false,
176 backslash_escapes: false,
177 max_stack_var_size: None,
178 opt_level: OptLevel::O0,
179 warn_all: false,
180 warn_extra: false,
181 warn_pedantic: false,
182 warn_deprecated: false,
183 warn_as_error: false,
184 disabled_warnings: Vec::new(),
185 debug_info: false,
186 verbose: false,
187 time_report: false,
188 diagnostics_format: DiagnosticsFormat::Text,
189 check_bounds: false,
190 check_all: false,
191 module_search_paths: Vec::new(),
192 module_output_dir: None,
193 library_search_paths: Vec::new(),
194 link_libs: Vec::new(),
195 shared: false,
196 static_link: false,
197 rpath: Vec::new(),
198 }
199 }
200 }
201
202 impl Options {
203 /// Old name preserved for callers that haven't been migrated.
204 /// New code should call `parse_cli` and dispatch on `ParsedCli`.
205 pub fn from_args(args: &[String]) -> Result<Self, String> {
206 match parse_cli(args)? {
207 ParsedCli::Compile(opts) => Ok(*opts),
208 ParsedCli::Info(_) => Err("info request — call parse_cli".into()),
209 }
210 }
211
212 /// Determine the output path based on input and flags.
213 pub fn output_path(&self) -> PathBuf {
214 if let Some(ref o) = self.output {
215 return o.clone();
216 }
217 let stem = self
218 .input
219 .file_stem()
220 .unwrap_or_default()
221 .to_str()
222 .unwrap_or("a");
223 if self.emit_asm {
224 PathBuf::from(format!("{}.s", stem))
225 } else if self.emit_obj {
226 PathBuf::from(format!("{}.o", stem))
227 } else if self.emit_ir {
228 PathBuf::from(format!("{}.ir", stem))
229 } else {
230 PathBuf::from(stem)
231 }
232 }
233 }
234
235 /// Parse the command line. Returns either a compile job or a request
236 /// for informational output (so main.rs can branch and exit cleanly
237 /// without a compile attempt). Supports response files via `@file`,
238 /// joined-form short options (`-Idir`, `-O2`, `-llib`), and
239 /// `--key=value` style for the long options that take a value.
240 pub fn parse_cli(raw_args: &[String]) -> Result<ParsedCli, String> {
241 let args = expand_response_files(raw_args)?;
242 let mut opts = Options::default();
243 let mut inputs: Vec<PathBuf> = Vec::new();
244 let mut info_action: Option<InfoAction> = None;
245
246 let mut i = 0;
247 while i < args.len() {
248 let arg = args[i].clone();
249 match arg.as_str() {
250 // ---- Information ----
251 "--help" | "-h" => info_action = Some(InfoAction::Help),
252 "--version" | "-V" => info_action = Some(InfoAction::Version),
253 "-dumpversion" => info_action = Some(InfoAction::DumpVersion),
254
255 // ---- Output path ----
256 "-o" => {
257 i += 1;
258 opts.output = Some(PathBuf::from(args.get(i).ok_or("-o requires an argument")?));
259 }
260
261 // ---- Mode ----
262 "-S" => opts.emit_asm = true,
263 "-c" => opts.emit_obj = true,
264 "-E" => opts.preprocess_only = true,
265 "--emit-ir" => opts.emit_ir = true,
266 "--emit-ast" => opts.emit_ast = true,
267 "--emit-tokens" => opts.emit_tokens = true,
268
269 // ---- Optimization ----
270 "-O" => opts.opt_level = OptLevel::O0,
271 arg if arg.starts_with("-O") => {
272 opts.opt_level = OptLevel::parse_flag(&arg[1..])
273 .ok_or_else(|| format!("unknown optimization level: {}", arg))?;
274 }
275
276 // ---- Module / include search paths ----
277 "-I" => {
278 i += 1;
279 opts.module_search_paths
280 .push(PathBuf::from(args.get(i).ok_or("-I requires a directory")?));
281 }
282 arg if arg.starts_with("-I") => opts.module_search_paths.push(PathBuf::from(&arg[2..])),
283
284 "-J" => {
285 i += 1;
286 opts.module_output_dir =
287 Some(PathBuf::from(args.get(i).ok_or("-J requires a directory")?));
288 }
289 arg if arg.starts_with("-J") => {
290 opts.module_output_dir = Some(PathBuf::from(&arg[2..]));
291 }
292
293 // ---- Linker search / libs / rpath ----
294 "-L" => {
295 i += 1;
296 opts.library_search_paths
297 .push(PathBuf::from(args.get(i).ok_or("-L requires a directory")?));
298 }
299 arg if arg.starts_with("-L") => {
300 opts.library_search_paths.push(PathBuf::from(&arg[2..]))
301 }
302
303 "-l" => {
304 i += 1;
305 opts.link_libs
306 .push(args.get(i).ok_or("-l requires a library name")?.clone());
307 }
308 arg if arg.starts_with("-l") => opts.link_libs.push(arg[2..].to_string()),
309
310 "-rpath" | "--rpath" => {
311 i += 1;
312 opts.rpath
313 .push(PathBuf::from(args.get(i).ok_or("-rpath requires a path")?));
314 }
315
316 "-shared" => opts.shared = true,
317 "-static" => opts.static_link = true,
318
319 // ---- Standards / language flags ----
320 arg if arg.starts_with("--std=") => {
321 let val = &arg["--std=".len()..];
322 opts.std = Some(
323 crate::sema::validate::FortranStandard::parse_flag(val)
324 .ok_or_else(|| format!("unknown --std value: {}", val))?,
325 );
326 }
327 "--std" => {
328 i += 1;
329 let val = args.get(i).ok_or("--std requires a value")?;
330 opts.std = Some(
331 crate::sema::validate::FortranStandard::parse_flag(val)
332 .ok_or_else(|| format!("unknown --std value: {}", val))?,
333 );
334 }
335 "-ffree-form" => opts.source_form_override = Some(SourceFormOverride::Free),
336 "-ffixed-form" => opts.source_form_override = Some(SourceFormOverride::Fixed),
337 "-fdefault-integer-8" => opts.default_integer_8 = true,
338 "-fdefault-real-8" => opts.default_real_8 = true,
339 "-fimplicit-none" => opts.force_implicit_none = true,
340 "-frecursive" => opts.recursive_default = true,
341 "-fbackslash" => opts.backslash_escapes = true,
342 "-fno-backslash" => opts.backslash_escapes = false,
343 arg if arg.starts_with("-fmax-stack-var-size=") => {
344 let val = &arg["-fmax-stack-var-size=".len()..];
345 opts.max_stack_var_size = Some(
346 val.parse()
347 .map_err(|_| format!("invalid -fmax-stack-var-size value: {}", val))?,
348 );
349 }
350
351 // ---- Runtime checks ----
352 "-fcheck=bounds" => opts.check_bounds = true,
353 "-fcheck=all" => {
354 opts.check_bounds = true;
355 opts.check_all = true;
356 }
357
358 // ---- Warnings (accepted; gating is gradual sprint work) ----
359 "-Wall" => opts.warn_all = true,
360 "-Wextra" => opts.warn_extra = true,
361 "-Wpedantic" | "-pedantic" => opts.warn_pedantic = true,
362 "-Wdeprecated" => opts.warn_deprecated = true,
363 "-Werror" => opts.warn_as_error = true,
364 arg if arg.starts_with("-Wno-") => {
365 opts.disabled_warnings.push(arg[5..].to_string());
366 }
367 arg if arg.starts_with("-W") => {
368 // Unknown -Wfoo: accept silently rather than fail
369 // (gfortran has hundreds of these and projects pass
370 // them blindly). Record so it's queryable later.
371 opts.disabled_warnings.push(arg.to_string());
372 }
373
374 // ---- Debug / introspection ----
375 "-g" | "-g1" | "-g2" | "-g3" | "-g0" => opts.debug_info = true,
376 arg if arg.starts_with("-g") => opts.debug_info = true,
377 "-v" | "--verbose" => opts.verbose = true,
378 "--time-report" => opts.time_report = true,
379 arg if arg.starts_with("--diagnostics-format=") => {
380 let val = &arg["--diagnostics-format=".len()..];
381 opts.diagnostics_format = match val {
382 "text" => DiagnosticsFormat::Text,
383 "json" => DiagnosticsFormat::Json,
384 other => {
385 return Err(format!("unknown --diagnostics-format value: {}", other))
386 }
387 };
388 }
389
390 // ---- Positional input file ----
391 arg if !arg.starts_with('-') => inputs.push(PathBuf::from(arg)),
392
393 other => return Err(format!("unknown option: {}", other)),
394 }
395 i += 1;
396 }
397
398 if let Some(action) = info_action {
399 return Ok(ParsedCli::Info(action));
400 }
401
402 if inputs.is_empty() {
403 return Err("no input file".into());
404 }
405 opts.input = inputs.remove(0);
406 opts.extra_inputs = inputs;
407 Ok(ParsedCli::Compile(Box::new(opts)))
408 }
409
410 /// Expand any `@file` argument into the lines of `file`, treating
411 /// each whitespace-separated token as an additional argument.
412 fn expand_response_files(args: &[String]) -> Result<Vec<String>, String> {
413 let mut expanded: Vec<String> = Vec::with_capacity(args.len());
414 for arg in args {
415 if let Some(path) = arg.strip_prefix('@') {
416 let body = fs::read_to_string(path)
417 .map_err(|e| format!("cannot read response file '{}': {}", path, e))?;
418 for tok in body.split_whitespace() {
419 expanded.push(tok.to_string());
420 }
421 } else {
422 expanded.push(arg.clone());
423 }
424 }
425 Ok(expanded)
426 }
427
428 /// Help text printed by `--help`.
429 pub const HELP_TEXT: &str = "\
430 USAGE: armfortas [OPTIONS] <files...>
431 afs [OPTIONS] <files...>
432
433 COMPILATION:
434 -c Compile to object file only (no linking)
435 -S Emit assembly text
436 -E Preprocess only
437 -o <file> Output file name
438
439 LANGUAGE:
440 --std=<standard> Fortran standard (f77, f90, f95, f2003, f2008, f2018, f2023)
441 -ffree-form Force free-form source
442 -ffixed-form Force fixed-form source
443 -fdefault-integer-8 Make default integer kind 8 bytes
444 -fdefault-real-8 Make default real kind 8 bytes
445 -fimplicit-none Force implicit none in all scopes
446 -frecursive Make all procedures recursive by default
447 -fbackslash Interpret backslash in strings as escape
448 -fmax-stack-var-size=<n> Stack variable size threshold (bytes)
449
450 OPTIMIZATION:
451 -O0, -O1, -O2, -O3 Optimization level (default -O0)
452 -Os Optimize for size
453 -Ofast Aggressive optimization
454
455 WARNINGS:
456 -Wall All standard warnings
457 -Wextra Extra warnings
458 -Wpedantic Pedantic standard conformance warnings
459 -Wdeprecated Deprecated feature warnings
460 -Werror Treat warnings as errors
461 -Wno-<name> Disable specific warning
462
463 DEBUGGING:
464 -g Generate debug information (DWARF emission TODO)
465 --emit-ir Dump IR to the output path
466 --emit-ast Dump AST to the output path
467 --emit-tokens Dump token stream to the output path
468 -v, --verbose Verbose output (show compilation phases)
469 --time-report Show time spent in each compilation phase
470 -fcheck=bounds Enable runtime array bounds checking
471 -fcheck=all Enable all runtime checks
472 --diagnostics-format=text|json
473 Diagnostic output format
474
475 DIRECTORIES:
476 -I <dir> Module/include search path
477 -J <dir> Module output directory
478 -L <dir> Library search path
479 -l <lib> Link library
480
481 LINKING:
482 -shared Produce shared library
483 -static Static linking
484 -rpath <path> Runtime library path
485
486 INFORMATION:
487 --version, -V Print version
488 --help, -h Print help
489 -dumpversion Print version number only
490
491 OTHER:
492 @<file> Read additional arguments from <file> (one per token)
493 ";
494
495 /// Version string emitted by `--version`.
496 pub fn version_string() -> String {
497 format!(
498 "armfortas {} (aarch64-apple-darwin)",
499 env!("CARGO_PKG_VERSION")
500 )
501 }
502
503 /// Just the version number, for `-dumpversion`.
504 pub fn dump_version_string() -> String {
505 env!("CARGO_PKG_VERSION").to_string()
506 }
507
508 /// Tracks per-phase wall-clock time for `--time-report`. When
509 /// disabled, all operations are zero-overhead (no Instant calls, no
510 /// allocation).
511 struct PhaseTimer {
512 enabled: bool,
513 samples: Vec<(&'static str, std::time::Duration)>,
514 start: Option<std::time::Instant>,
515 }
516
517 struct PhaseGuard {
518 name: &'static str,
519 started: Option<std::time::Instant>,
520 }
521
522 impl PhaseTimer {
523 fn new(enabled: bool) -> Self {
524 Self {
525 enabled,
526 samples: Vec::new(),
527 start: if enabled {
528 Some(std::time::Instant::now())
529 } else {
530 None
531 },
532 }
533 }
534 fn start(&self, name: &'static str) -> PhaseGuard {
535 PhaseGuard {
536 name,
537 started: if self.enabled {
538 Some(std::time::Instant::now())
539 } else {
540 None
541 },
542 }
543 }
544 fn record(&mut self, name: &'static str, dur: std::time::Duration) {
545 if self.enabled {
546 self.samples.push((name, dur));
547 }
548 }
549 fn report(&self) {
550 if !self.enabled {
551 return;
552 }
553 let total: std::time::Duration = self
554 .samples
555 .iter()
556 .map(|(_, d)| *d)
557 .sum::<std::time::Duration>();
558 let total_ms = total.as_secs_f64() * 1000.0;
559 eprintln!("Phase Time (ms) %");
560 eprintln!("─────────────────────────────────");
561 for (name, d) in &self.samples {
562 let ms = d.as_secs_f64() * 1000.0;
563 let pct = if total_ms > 0.0 { ms / total_ms * 100.0 } else { 0.0 };
564 eprintln!("{:<16} {:>8.2} {:>4.0}%", name, ms, pct);
565 }
566 eprintln!("─────────────────────────────────");
567 let wall = self
568 .start
569 .map(|s| s.elapsed().as_secs_f64() * 1000.0)
570 .unwrap_or(0.0);
571 eprintln!("{:<16} {:>8.2} {:>4.0}%", "Total", wall, 100.0);
572 }
573 }
574
575 impl PhaseGuard {
576 fn end(self, timer: &mut PhaseTimer) {
577 if let Some(start) = self.started {
578 timer.record(self.name, start.elapsed());
579 }
580 }
581 }
582
583 fn main_wrapper_target(allocated: &[MachineFunction]) -> Option<&str> {
584 // Only emit _main if there's a __prog_* function (a Fortran PROGRAM
585 // body). The previous .or_else fallback picked any non-"main"
586 // function, which incorrectly wrapped module procedures.
587 allocated
588 .iter()
589 .find(|func| func.name.starts_with("__prog_"))
590 .map(|func| func.name.as_str())
591 }
592
593 /// Compile a Fortran source file through the full pipeline.
594 pub fn compile(opts: &Options) -> Result<(), String> {
595 let mut phases = PhaseTimer::new(opts.time_report);
596 if opts.verbose {
597 eprintln!("{}", version_string());
598 }
599
600 // 1. Read source.
601 if opts.verbose {
602 eprintln!(" reading: {}", opts.input.display());
603 }
604 let phase = phases.start("read");
605 let source = fs::read_to_string(&opts.input)
606 .map_err(|e| format!("cannot read '{}': {}", opts.input.display(), e))?;
607 phase.end(&mut phases);
608
609 // 2. Preprocess.
610 let source_form = match opts.source_form_override {
611 Some(SourceFormOverride::Free) => SourceForm::FreeForm,
612 Some(SourceFormOverride::Fixed) => SourceForm::FixedForm,
613 None => detect_source_form(&opts.input.to_string_lossy()),
614 };
615 if opts.verbose {
616 let form = match source_form {
617 SourceForm::FreeForm => "free-form",
618 SourceForm::FixedForm => "fixed-form",
619 };
620 eprintln!(" preprocessing: {} ({})", opts.input.display(), form);
621 }
622 let phase = phases.start("preprocess");
623 let pp_config = crate::preprocess::PreprocConfig {
624 filename: opts.input.to_str().unwrap_or("<input>").to_string(),
625 fixed_form: matches!(source_form, SourceForm::FixedForm),
626 ..crate::preprocess::PreprocConfig::default()
627 };
628 let pp_result =
629 crate::preprocess::preprocess(&source, &pp_config).map_err(|e| format!("{}", e))?;
630 phase.end(&mut phases);
631 let preprocessed = pp_result.text;
632
633 if opts.preprocess_only {
634 let out = opts.output_path();
635 if out.as_os_str() == "-" {
636 print!("{}", preprocessed);
637 } else {
638 fs::write(&out, &preprocessed)
639 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
640 }
641 if opts.verbose {
642 eprintln!(" preprocess-only: wrote {}", out.display());
643 }
644 phases.report();
645 return Ok(());
646 }
647
648 // 3. Lex.
649 let phase = phases.start("lex");
650 let tokens = tokenize(&preprocessed, 0, source_form).map_err(|e| {
651 format!(
652 "{}:{}:{}: lexer error: {}",
653 opts.input.display(),
654 e.span.start.line,
655 e.span.start.col,
656 e.msg
657 )
658 })?;
659 phase.end(&mut phases);
660 if opts.verbose {
661 eprintln!(" lexed: {} tokens", tokens.len());
662 }
663 if opts.emit_tokens {
664 let out = opts.output_path();
665 let mut buf = String::new();
666 for t in &tokens {
667 buf.push_str(&format!("{:?}\n", t));
668 }
669 fs::write(&out, &buf)
670 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
671 return Ok(());
672 }
673
674 // 4. Parse.
675 let phase = phases.start("parse");
676 let mut parser = Parser::new(&tokens);
677 let units = parser.parse_file().map_err(|e| {
678 format!(
679 "{}:{}:{}: parse error: {}",
680 opts.input.display(),
681 e.span.start.line,
682 e.span.start.col,
683 e.msg
684 )
685 })?;
686 phase.end(&mut phases);
687 if opts.verbose {
688 eprintln!(" parsed: {} top-level units", units.len());
689 }
690 if opts.emit_ast {
691 let out = opts.output_path();
692 let mut buf = String::new();
693 for u in &units {
694 buf.push_str(&format!("{:#?}\n", u));
695 }
696 fs::write(&out, &buf)
697 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
698 return Ok(());
699 }
700
701 // 5. Semantic analysis.
702 let phase = phases.start("sema");
703 let resolve_result = resolve::resolve_file(&units, &opts.module_search_paths).map_err(|e| {
704 format!(
705 "{}:{}:{}: {}",
706 opts.input.display(),
707 e.span.start.line,
708 e.span.start.col,
709 e.msg
710 )
711 })?;
712 let st = resolve_result.st;
713 let type_layouts = resolve_result.type_layouts;
714
715 // Build external globals from .amod-loaded modules.
716 let mut external_globals = std::collections::HashMap::new();
717 for ext_mod in &resolve_result.external_modules {
718 external_globals.extend(crate::sema::amod::extract_module_globals(ext_mod));
719 }
720
721 let diags = validate::validate_file_with_layouts(&units, &st, opts.std, &type_layouts);
722 let mut had_error = false;
723 for d in &diags {
724 match d.kind {
725 validate::DiagKind::Error => {
726 eprintln!(
727 "{}:{}:{}: error: {}",
728 opts.input.display(),
729 d.span.start.line,
730 d.span.start.col,
731 d.msg
732 );
733 had_error = true;
734 }
735 validate::DiagKind::Warning => {
736 eprintln!(
737 "{}:{}:{}: warning: {}",
738 opts.input.display(),
739 d.span.start.line,
740 d.span.start.col,
741 d.msg
742 );
743 if opts.warn_as_error {
744 had_error = true;
745 }
746 }
747 }
748 }
749 if had_error {
750 return Err(format!("aborting due to errors in {}", opts.input.display()));
751 }
752 phase.end(&mut phases);
753 if opts.verbose {
754 eprintln!(" sema: {} diagnostics", diags.len());
755 }
756
757 // 6. Lower to IR.
758 // Build external char_len_star_params from .amod-loaded modules.
759 let mut external_char_len_star = std::collections::HashMap::new();
760 for ext_mod in &resolve_result.external_modules {
761 external_char_len_star.extend(crate::sema::amod::extract_char_len_star_params(ext_mod));
762 }
763
764 let (mut ir_module, module_globals) = lower::lower_file(&units, &st, &type_layouts, external_globals, external_char_len_star);
765 let ir_errors = verify::verify_module(&ir_module);
766 if !ir_errors.is_empty() {
767 let msg = ir_errors
768 .iter()
769 .map(|e| e.to_string())
770 .collect::<Vec<_>>()
771 .join("\n");
772 return Err(format!("internal error: IR verification failed:\n{}", msg));
773 }
774 let module_has_i128 = ir_module.contains_i128();
775 if opts.verbose {
776 eprintln!(" IR: {} functions", ir_module.functions.len());
777 }
778 // 6.5. Run IR optimization pipeline.
779 //
780 // This is where const_fold, mem2reg, LICM, DSE, loop unrolling, and
781 // every other IR-level pass actually fire. At O0 the pipeline is empty
782 // so nothing changes. The pipeline runs to fixpoint; the pass manager
783 // verifies the IR after every pass.
784 let phase = phases.start("opt");
785 {
786 use crate::opt::pipeline::OptLevel as IrOpt;
787 let ir_opt = match opts.opt_level {
788 OptLevel::O0 => IrOpt::O0,
789 OptLevel::O1 => IrOpt::O1,
790 OptLevel::O2 => IrOpt::O2,
791 OptLevel::O3 => IrOpt::O3,
792 OptLevel::Os => IrOpt::Os,
793 OptLevel::Ofast => IrOpt::Ofast,
794 };
795 let pm = if ir_module.contains_i128_outside_globals() && opts.opt_level != OptLevel::O0 {
796 crate::opt::build_i128_pipeline(ir_opt).ok_or_else(|| {
797 format!(
798 "integer(16) / i128 optimization at -{} is not yet supported; use --emit-ir to inspect the raw IR for now",
799 opts.opt_level.as_flag()
800 )
801 })?
802 } else {
803 crate::opt::build_pipeline(ir_opt)
804 };
805 pm.run(&mut ir_module);
806 }
807 phase.end(&mut phases);
808 if opts.verbose {
809 eprintln!(" optimization: -{}", opts.opt_level.as_str());
810 }
811
812 if opts.emit_ir {
813 let ir_text = ir_printer::print_module(&ir_module);
814 let out = opts.output_path();
815 fs::write(&out, &ir_text)
816 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
817 return Ok(());
818 }
819
820 if module_has_i128 && !ir_module.i128_backend_o0_supported() {
821 return Err(
822 "backend does not yet support integer(16) / i128 codegen; use --emit-ir for now"
823 .into(),
824 );
825 }
826
827 // 7. Instruction selection.
828 let phase = phases.start("codegen");
829 let machine_funcs = isel::select_module(&ir_module);
830
831 // 7.5. Backend peephole (O2+): FMA fusion, etc.
832 let mut allocated: Vec<_> = machine_funcs;
833 if opts.opt_level >= OptLevel::O2 {
834 for mf in &mut allocated {
835 peephole::run_peephole(mf);
836 }
837 }
838
839 // 8. Register allocation (linear scan).
840 for mf in &mut allocated {
841 let liveness = crate::codegen::liveness::compute_liveness(mf);
842 let result = linearscan::linear_scan(mf);
843 linearscan::apply_allocation(mf, &result, &liveness);
844 linearscan::insert_callee_saves(mf, &result.callee_saved_used);
845 linearscan::coalesce_moves(mf);
846 // 8.5. Tail call optimization (O1+): BL + epilogue → epilogue + B.
847 // Runs after regalloc so we can inspect physical register assignments.
848 if opts.opt_level >= OptLevel::O1 {
849 crate::codegen::tailcall::tail_call_opt(mf);
850 }
851 }
852
853 // 9. Emit assembly.
854 let mut asm_text = String::new();
855 asm_text.push_str(".section __TEXT,__text,regular,pure_instructions\n");
856 for mf in &allocated {
857 // Re-emit __TEXT section before each function in case the previous
858 // function's constant pool switched to __DATA.
859 asm_text.push_str(".section __TEXT,__text,regular,pure_instructions\n");
860 asm_text.push_str(&emit::emit_function(mf));
861 asm_text.push('\n');
862 }
863
864 // Emit module-level globals (SAVE'd locals + module variables)
865 // into a __DATA,__data section. Must come before _main so the
866 // labels are defined when functions reference them.
867 if !ir_module.globals.is_empty() {
868 asm_text.push_str(&emit::emit_globals(&ir_module.globals));
869 asm_text.push('\n');
870 }
871
872 // Emit _main entry point (must be in __TEXT section).
873 if let Some(user_func) = main_wrapper_target(&allocated) {
874 if user_func != "main" {
875 asm_text.push_str("\n.section __TEXT,__text,regular,pure_instructions\n");
876 asm_text.push_str(&format!(
877 "\
878 .globl _main
879 .p2align 2
880 _main:
881 stp x29, x30, [sp, #-16]!
882 mov x29, sp
883 bl _afs_program_init
884 bl _{0}
885 bl _afs_program_finalize
886 mov x0, #0
887 ldp x29, x30, [sp], #16
888 ret
889 ",
890 user_func
891 ));
892 }
893 }
894
895 phase.end(&mut phases);
896 if opts.verbose {
897 eprintln!(" codegen: {} machine functions", allocated.len());
898 }
899 if opts.emit_asm {
900 let out = opts.output_path();
901 fs::write(&out, &asm_text)
902 .map_err(|e| format!("cannot write '{}': {}", out.display(), e))?;
903 if opts.verbose {
904 eprintln!(" wrote: {}", out.display());
905 }
906 phases.report();
907 return Ok(());
908 }
909
910 // 10. Assemble (using system assembler for now).
911 //
912 // The temp .s / .o paths must satisfy two competing needs:
913 // (1) `ld` embeds the .o path in the linked binary's symbol
914 // table (the OSO debug stab), so two back-to-back compiles
915 // of the same source to the same output path must use the
916 // same .o name — otherwise the embedded string varies and
917 // reproducible-build tests fail. PID is unsafe here
918 // because each compile_binary call spawns a fresh
919 // subprocess with a different PID.
920 // (2) Two parallel compiles of two DIFFERENT sources with the
921 // same basename (e.g. both writing `mod.o` to different
922 // unique-dir test outputs) must NOT race on the same temp
923 // file. Output stem alone is therefore not enough.
924 // The cheap fix that satisfies both: derive a stable hash of the
925 // full output path with FNV-1a and use it in the temp basename.
926 // Same output path → same hash → same .o (deterministic across
927 // subprocesses). Different output paths → different hashes → no
928 // collision. std::collections::hash_map::DefaultHasher uses a
929 // per-process random seed and would defeat (1).
930 let out_path = opts.output_path();
931 let stem = out_path
932 .file_stem()
933 .and_then(|s| s.to_str())
934 .map(|s| s.to_string())
935 .unwrap_or_else(|| "afs".to_string());
936 let token = {
937 let bytes = out_path.as_os_str().as_encoded_bytes();
938 let mut h: u64 = 0xcbf29ce484222325;
939 for &b in bytes {
940 h ^= b as u64;
941 h = h.wrapping_mul(0x100000001b3);
942 }
943 h
944 };
945 let asm_path = std::env::temp_dir().join(format!("armfortas_{}_{:016x}.s", stem, token));
946 let obj_path = if opts.emit_obj {
947 out_path.clone()
948 } else {
949 std::env::temp_dir().join(format!("armfortas_{}_{:016x}.o", stem, token))
950 };
951
952 fs::write(&asm_path, &asm_text).map_err(|e| format!("cannot write temp assembly: {}", e))?;
953
954 let phase = phases.start("assemble");
955 let as_result = Command::new("as")
956 .args(["-o", obj_path.to_str().unwrap(), asm_path.to_str().unwrap()])
957 .output()
958 .map_err(|e| format!("cannot run assembler: {}", e))?;
959 phase.end(&mut phases);
960
961 if !as_result.status.success() {
962 let stderr = String::from_utf8_lossy(&as_result.stderr);
963 return Err(format!("assembler failed:\n{}", stderr));
964 }
965 if opts.verbose {
966 eprintln!(" assembled: {}", obj_path.display());
967 }
968
969 if opts.emit_obj {
970 // Emit .amod files for each MODULE in the compilation unit.
971 // -J <dir> overrides where they go; default is the parent of
972 // the output .o.
973 for unit in &units {
974 if let crate::ast::unit::ProgramUnit::Module { name, .. } = &unit.node {
975 let mod_key = name.to_lowercase();
976 if let Some(mod_scope_id) = st.find_module_scope(&mod_key) {
977 let amod_text = crate::sema::amod::write_amod(
978 name,
979 opts.input.to_str().unwrap_or(""),
980 &source,
981 &st,
982 mod_scope_id,
983 &module_globals,
984 &type_layouts,
985 &ir_module,
986 &std::collections::HashMap::new(), // char_len_star computed by writer from scope
987 );
988 let amod_dir: std::path::PathBuf = opts
989 .module_output_dir
990 .clone()
991 .unwrap_or_else(|| {
992 opts.output_path()
993 .parent()
994 .unwrap_or_else(|| std::path::Path::new("."))
995 .to_path_buf()
996 });
997 let amod_path = amod_dir.join(format!("{}.amod", mod_key));
998 if let Err(e) = fs::write(&amod_path, &amod_text) {
999 eprintln!("warning: cannot write {}: {}", amod_path.display(), e);
1000 } else if opts.verbose {
1001 eprintln!(" amod: {}", amod_path.display());
1002 }
1003 }
1004 }
1005 }
1006 phases.report();
1007 return Ok(());
1008 }
1009
1010 // 11. Link.
1011 let binary_path = opts.output_path();
1012 let phase = phases.start("link");
1013 link(&obj_path, &binary_path, opts)?;
1014 phase.end(&mut phases);
1015 if opts.verbose {
1016 eprintln!(" linked: {}", binary_path.display());
1017 }
1018
1019 // Cleanup.
1020 let _ = fs::remove_file(&asm_path);
1021 let _ = fs::remove_file(&obj_path);
1022
1023 phases.report();
1024 Ok(())
1025 }
1026
1027 /// Link an object file with the runtime library to produce a binary.
1028 /// `opts` contributes the user-supplied `-L`, `-l`, `-rpath`,
1029 /// `-shared`, and `-static` flags that need to make it through to ld.
1030 fn link(obj: &Path, output: &Path, opts: &Options) -> Result<(), String> {
1031 let rt_path = find_runtime_lib()?;
1032 let sdk = Command::new("xcrun")
1033 .args(["--show-sdk-path"])
1034 .output()
1035 .map_err(|e| format!("cannot run xcrun: {}", e))?;
1036 let sysroot = String::from_utf8_lossy(&sdk.stdout).trim().to_string();
1037
1038 let mut args: Vec<String> = vec![
1039 obj.to_string_lossy().into_owned(),
1040 rt_path,
1041 "-lSystem".into(),
1042 "-no_uuid".into(),
1043 "-syslibroot".into(),
1044 sysroot,
1045 "-e".into(),
1046 "_main".into(),
1047 "-o".into(),
1048 output.to_string_lossy().into_owned(),
1049 ];
1050 push_link_flags(&mut args, opts);
1051
1052 let ld_result = Command::new("ld")
1053 .args(&args)
1054 .output()
1055 .map_err(|e| format!("cannot run linker: {}", e))?;
1056
1057 if !ld_result.status.success() {
1058 let stderr = String::from_utf8_lossy(&ld_result.stderr);
1059 return Err(format!("linker failed:\n{}", stderr));
1060 }
1061
1062 Ok(())
1063 }
1064
1065 /// Append the user-supplied linker flags from `opts` to `args`.
1066 /// `-L<dir>` and `-l<name>` map directly; `-rpath` is passed as a
1067 /// pair; `-shared` switches output type; `-static` discourages
1068 /// dynamic linking on supported platforms.
1069 fn push_link_flags(args: &mut Vec<String>, opts: &Options) {
1070 for dir in &opts.library_search_paths {
1071 args.push(format!("-L{}", dir.display()));
1072 }
1073 for lib in &opts.link_libs {
1074 args.push(format!("-l{}", lib));
1075 }
1076 for path in &opts.rpath {
1077 args.push("-rpath".into());
1078 args.push(path.to_string_lossy().into_owned());
1079 }
1080 if opts.shared {
1081 args.push("-dylib".into());
1082 }
1083 if opts.static_link {
1084 // Apple ld doesn't have a true -static; the closest is
1085 // -search_paths_first to bias toward .a archives. Keep the
1086 // intent visible without breaking link.
1087 args.push("-search_paths_first".into());
1088 }
1089 }
1090
1091 /// Link multiple object files with the runtime to produce a binary.
1092 fn link_multi(objs: &[PathBuf], output: &Path, opts: &Options) -> Result<(), String> {
1093 let rt_path = find_runtime_lib()?;
1094 let sdk = Command::new("xcrun")
1095 .args(["--show-sdk-path"])
1096 .output()
1097 .map_err(|e| format!("cannot run xcrun: {}", e))?;
1098 let sysroot = String::from_utf8_lossy(&sdk.stdout).trim().to_string();
1099
1100 let mut args: Vec<String> = vec![
1101 "-o".into(), output.to_str().unwrap().into(),
1102 ];
1103 for obj in objs {
1104 args.push(obj.to_str().unwrap().into());
1105 }
1106 args.extend([
1107 rt_path,
1108 "-lSystem".into(),
1109 "-no_uuid".into(),
1110 "-syslibroot".into(),
1111 sysroot,
1112 "-e".into(),
1113 "_main".into(),
1114 ]);
1115 push_link_flags(&mut args, opts);
1116 let ld_result = Command::new("ld")
1117 .args(&args)
1118 .output()
1119 .map_err(|e| format!("cannot run linker: {}", e))?;
1120 if !ld_result.status.success() {
1121 let stderr = String::from_utf8_lossy(&ld_result.stderr);
1122 return Err(format!("linker failed:\n{}", stderr));
1123 }
1124 Ok(())
1125 }
1126
1127 /// Compile multiple Fortran source files with automatic dependency
1128 /// resolution, producing a single linked binary.
1129 ///
1130 /// 1. Scan all files for MODULE/USE dependencies.
1131 /// 2. Topological sort (error on cycles).
1132 /// 3. Compile each in order to a temp .o + .amod.
1133 /// 4. Link all .o files into the output binary.
1134 pub fn compile_multi(opts: &Options) -> Result<(), String> {
1135 let mut all_inputs = vec![opts.input.clone()];
1136 all_inputs.extend(opts.extra_inputs.iter().cloned());
1137
1138 // Scan dependencies.
1139 let file_deps: Vec<dep_scan::FileDeps> = all_inputs.iter()
1140 .map(|p| dep_scan::scan_file(p))
1141 .collect::<Result<Vec<_>, _>>()?;
1142
1143 // Topological sort.
1144 let order = dep_scan::resolve_compilation_order(&file_deps)?;
1145
1146 // Compile each file in order.
1147 let tmp_dir = std::env::temp_dir().join(format!("afs_multi_{}", std::process::id()));
1148 fs::create_dir_all(&tmp_dir)
1149 .map_err(|e| format!("cannot create temp dir: {}", e))?;
1150
1151 let mut object_files: Vec<PathBuf> = Vec::new();
1152 for &idx in &order {
1153 let src = &file_deps[idx].path;
1154 let stem = src.file_stem().unwrap_or_default().to_str().unwrap_or("out");
1155 let obj_path = tmp_dir.join(format!("{}.o", stem));
1156
1157 // Build a single-file Options for this source by inheriting
1158 // the user-facing flags and overriding only the per-file bits.
1159 let mut sub_opts = Options {
1160 input: src.clone(),
1161 extra_inputs: vec![],
1162 output: Some(obj_path.clone()),
1163 emit_obj: true,
1164 ..Options::default()
1165 };
1166 sub_opts.opt_level = opts.opt_level;
1167 sub_opts.std = opts.std;
1168 sub_opts.source_form_override = opts.source_form_override;
1169 sub_opts.default_integer_8 = opts.default_integer_8;
1170 sub_opts.default_real_8 = opts.default_real_8;
1171 sub_opts.force_implicit_none = opts.force_implicit_none;
1172 sub_opts.recursive_default = opts.recursive_default;
1173 sub_opts.backslash_escapes = opts.backslash_escapes;
1174 sub_opts.max_stack_var_size = opts.max_stack_var_size;
1175 sub_opts.warn_all = opts.warn_all;
1176 sub_opts.warn_extra = opts.warn_extra;
1177 sub_opts.warn_pedantic = opts.warn_pedantic;
1178 sub_opts.warn_deprecated = opts.warn_deprecated;
1179 sub_opts.warn_as_error = opts.warn_as_error;
1180 sub_opts.disabled_warnings = opts.disabled_warnings.clone();
1181 sub_opts.debug_info = opts.debug_info;
1182 sub_opts.verbose = opts.verbose;
1183 sub_opts.time_report = opts.time_report;
1184 sub_opts.diagnostics_format = opts.diagnostics_format;
1185 sub_opts.check_bounds = opts.check_bounds;
1186 sub_opts.check_all = opts.check_all;
1187 sub_opts.module_output_dir = opts.module_output_dir.clone();
1188 sub_opts.module_search_paths = {
1189 let mut paths = opts.module_search_paths.clone();
1190 paths.push(tmp_dir.clone()); // find .amod from earlier compilations
1191 paths
1192 };
1193 compile(&sub_opts)?;
1194 object_files.push(obj_path);
1195 }
1196
1197 // Link all object files.
1198 let output = opts.output.clone().unwrap_or_else(|| PathBuf::from("a.out"));
1199 link_multi(&object_files, &output, opts)?;
1200
1201 // Cleanup.
1202 let _ = fs::remove_dir_all(&tmp_dir);
1203
1204 Ok(())
1205 }
1206
1207 /// Find libarmfortas_rt.a in common locations.
1208 fn find_runtime_lib() -> Result<String, String> {
1209 if let Some(workspace_root) = find_workspace_root() {
1210 maybe_refresh_runtime_lib(&workspace_root)?;
1211 for candidate in [
1212 workspace_root.join("target/debug/libarmfortas_rt.a"),
1213 workspace_root.join("target/release/libarmfortas_rt.a"),
1214 ] {
1215 if candidate.exists() {
1216 return Ok(candidate.to_string_lossy().into_owned());
1217 }
1218 }
1219 }
1220
1221 // Fall back to a runtime shipped next to the compiler binary.
1222 if let Ok(exe) = std::env::current_exe() {
1223 let dir = exe.parent().unwrap_or(Path::new("."));
1224 let candidate = dir.join("libarmfortas_rt.a");
1225 if candidate.exists() {
1226 return Ok(candidate.to_str().unwrap().to_string());
1227 }
1228 }
1229
1230 Err("cannot find libarmfortas_rt.a — build with 'cargo build -p armfortas-rt'".into())
1231 }
1232
1233 fn maybe_refresh_runtime_lib(workspace_root: &Path) -> Result<(), String> {
1234 let runtime_dir = workspace_root.join("runtime");
1235 if !runtime_dir.join("Cargo.toml").exists() {
1236 return Ok(());
1237 }
1238
1239 let Some(source_mtime) = newest_mtime(&runtime_dir) else {
1240 return Ok(());
1241 };
1242 let debug_archive = workspace_root.join("target/debug/libarmfortas_rt.a");
1243 let archive_mtime = fs::metadata(&debug_archive).ok().and_then(|meta| meta.modified().ok());
1244
1245 if archive_mtime.is_some_and(|mtime| mtime >= source_mtime) {
1246 return Ok(());
1247 }
1248
1249 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
1250 let output = Command::new(cargo)
1251 .current_dir(workspace_root)
1252 .args(["build", "-p", "armfortas-rt"])
1253 .output()
1254 .map_err(|e| format!("cannot rebuild libarmfortas_rt.a: {}", e))?;
1255 if output.status.success() {
1256 Ok(())
1257 } else {
1258 Err(format!(
1259 "cannot rebuild libarmfortas_rt.a:\n{}",
1260 String::from_utf8_lossy(&output.stderr)
1261 ))
1262 }
1263 }
1264
1265 fn newest_mtime(path: &Path) -> Option<SystemTime> {
1266 let meta = fs::metadata(path).ok()?;
1267 let mut newest = meta.modified().ok()?;
1268 if meta.is_dir() {
1269 for entry in fs::read_dir(path).ok()? {
1270 let entry = entry.ok()?;
1271 let child = newest_mtime(&entry.path())?;
1272 if child > newest {
1273 newest = child;
1274 }
1275 }
1276 }
1277 Some(newest)
1278 }
1279
1280 fn find_workspace_root() -> Option<PathBuf> {
1281 let mut bases = Vec::new();
1282 if let Ok(cwd) = std::env::current_dir() {
1283 bases.push(cwd);
1284 }
1285 if let Ok(exe) = std::env::current_exe() {
1286 if let Some(dir) = exe.parent() {
1287 bases.push(dir.to_path_buf());
1288 }
1289 }
1290
1291 for base in bases {
1292 for ancestor in base.ancestors() {
1293 if ancestor.join("Cargo.toml").exists() && ancestor.join("runtime/Cargo.toml").exists() {
1294 return Some(ancestor.to_path_buf());
1295 }
1296 }
1297 }
1298 None
1299 }
1300
1301 #[cfg(test)]
1302 mod tests {
1303 use super::*;
1304 use std::fs;
1305
1306 #[test]
1307 fn parses_os_optimization_flag() {
1308 assert_eq!(OptLevel::parse_flag("Os"), Some(OptLevel::Os));
1309 assert_eq!(OptLevel::parse_flag("os"), Some(OptLevel::Os));
1310 assert_eq!(OptLevel::Os.as_flag(), "-Os");
1311 assert_eq!(OptLevel::Os.as_str(), "Os");
1312 }
1313
1314 #[test]
1315 fn options_from_args_accepts_os() {
1316 let args = vec!["-Os".to_string(), "hello.f90".to_string()];
1317 let opts = Options::from_args(&args).expect("driver should accept -Os");
1318 assert_eq!(opts.opt_level, OptLevel::Os);
1319 assert_eq!(opts.input, PathBuf::from("hello.f90"));
1320 }
1321
1322 fn i128_fixture() -> PathBuf {
1323 let path = PathBuf::from("tests/fixtures").join("integer16_ir.f90");
1324 assert!(path.exists(), "missing test fixture {}", path.display());
1325 path
1326 }
1327
1328 fn i128_reject_fixture() -> PathBuf {
1329 let path = PathBuf::from("tests/fixtures").join("integer16_mul.f90");
1330 assert!(path.exists(), "missing test fixture {}", path.display());
1331 path
1332 }
1333
1334 fn i128_internal_call_fixture() -> PathBuf {
1335 let path = PathBuf::from("tests/fixtures").join("integer16_internal_call.f90");
1336 assert!(path.exists(), "missing test fixture {}", path.display());
1337 path
1338 }
1339
1340 fn i128_external_call_fixture() -> PathBuf {
1341 let path = PathBuf::from("tests/fixtures").join("integer16_external_call.f90");
1342 assert!(path.exists(), "missing test fixture {}", path.display());
1343 path
1344 }
1345
1346 #[test]
1347 fn emit_ir_allows_integer16_staging_at_o0() {
1348 let output = std::env::temp_dir().join(format!(
1349 "armfortas_i128_ir_{}_{}.ir",
1350 std::process::id(),
1351 "o0"
1352 ));
1353 let opts = Options {
1354 input: i128_fixture(),
1355 output: Some(output.clone()),
1356 emit_asm: false,
1357 emit_obj: false,
1358 emit_ir: true,
1359 preprocess_only: false,
1360 opt_level: OptLevel::O0,
1361 extra_inputs: vec![],
1362 module_search_paths: vec![],
1363 ..Options::default()
1364 };
1365
1366 compile(&opts).expect("O0 --emit-ir should support integer(16) staging");
1367 let ir = fs::read_to_string(&output).expect("missing emitted IR");
1368 assert!(ir.contains("i128"), "emitted IR should expose integer(16) as i128:\n{}", ir);
1369 let _ = fs::remove_file(output);
1370 }
1371
1372 #[test]
1373 fn backend_rejects_integer16_arithmetic_for_now() {
1374 let output = std::env::temp_dir().join(format!(
1375 "armfortas_i128_bin_{}_{}",
1376 std::process::id(),
1377 "o0"
1378 ));
1379 let opts = Options {
1380 input: i128_reject_fixture(),
1381 output: Some(output),
1382 emit_asm: false,
1383 emit_obj: false,
1384 emit_ir: false,
1385 preprocess_only: false,
1386 opt_level: OptLevel::O0,
1387 extra_inputs: vec![],
1388 module_search_paths: vec![],
1389 ..Options::default()
1390 };
1391
1392 let err = compile(&opts).expect_err("backend should reject integer(16) until i128 codegen lands");
1393 assert!(
1394 err.contains("backend does not yet support integer(16) / i128 codegen"),
1395 "unexpected backend rejection:\n{}",
1396 err
1397 );
1398 }
1399
1400 #[test]
1401 fn backend_allows_simple_integer16_memory_codegen_at_o0() {
1402 let output = std::env::temp_dir().join(format!(
1403 "armfortas_i128_mem_{}_{}.s",
1404 std::process::id(),
1405 "o0"
1406 ));
1407 let opts = Options {
1408 input: i128_fixture(),
1409 output: Some(output.clone()),
1410 emit_asm: true,
1411 emit_obj: false,
1412 emit_ir: false,
1413 preprocess_only: false,
1414 opt_level: OptLevel::O0,
1415 extra_inputs: vec![],
1416 module_search_paths: vec![],
1417 ..Options::default()
1418 };
1419
1420 compile(&opts).expect("simple integer(16) memory traffic should codegen at O0");
1421 let asm = fs::read_to_string(&output).expect("missing emitted assembly");
1422 assert!(asm.contains("stp x16, x17"), "expected paired i128 store in asm:\n{}", asm);
1423 let _ = fs::remove_file(output);
1424 }
1425
1426 #[test]
1427 fn backend_allows_simple_integer16_add_codegen_at_o0() {
1428 let output = std::env::temp_dir().join(format!(
1429 "armfortas_i128_add_{}_{}.s",
1430 std::process::id(),
1431 "o0"
1432 ));
1433 let opts = Options {
1434 input: PathBuf::from("tests/fixtures").join("integer16_add.f90"),
1435 output: Some(output.clone()),
1436 emit_asm: true,
1437 emit_obj: false,
1438 emit_ir: false,
1439 preprocess_only: false,
1440 opt_level: OptLevel::O0,
1441 extra_inputs: vec![],
1442 module_search_paths: vec![],
1443 ..Options::default()
1444 };
1445
1446 compile(&opts).expect("simple integer(16) add should codegen at O0");
1447 let asm = fs::read_to_string(&output).expect("missing emitted assembly");
1448 assert!(asm.contains("adds x16, x16, x8"), "expected i128 add carry chain in asm:\n{}", asm);
1449 let _ = fs::remove_file(output);
1450 }
1451
1452 #[test]
1453 fn backend_allows_internal_integer16_call_codegen_at_o0() {
1454 let output = std::env::temp_dir().join(format!(
1455 "armfortas_i128_call_{}_{}.s",
1456 std::process::id(),
1457 "o0"
1458 ));
1459 let opts = Options {
1460 input: i128_internal_call_fixture(),
1461 output: Some(output.clone()),
1462 emit_asm: true,
1463 emit_obj: false,
1464 emit_ir: false,
1465 preprocess_only: false,
1466 opt_level: OptLevel::O0,
1467 extra_inputs: vec![],
1468 module_search_paths: vec![],
1469 ..Options::default()
1470 };
1471
1472 compile(&opts).expect("internal integer(16) call should codegen at O0");
1473 let asm = fs::read_to_string(&output).expect("missing emitted assembly");
1474 assert!(asm.contains("bl _add_one"), "expected internal helper call in asm:\n{}", asm);
1475 assert!(asm.contains("stp x0, x1"), "expected pair-register i128 ABI spill in asm:\n{}", asm);
1476 let _ = fs::remove_file(output);
1477 }
1478
1479 #[test]
1480 fn backend_allows_external_integer16_call_codegen_at_o0() {
1481 let output = std::env::temp_dir().join(format!(
1482 "armfortas_i128_external_call_{}_{}.s",
1483 std::process::id(),
1484 "o0"
1485 ));
1486 let opts = Options {
1487 input: i128_external_call_fixture(),
1488 output: Some(output.clone()),
1489 emit_asm: true,
1490 emit_obj: false,
1491 emit_ir: false,
1492 preprocess_only: false,
1493 opt_level: OptLevel::O0,
1494 extra_inputs: vec![],
1495 module_search_paths: vec![],
1496 ..Options::default()
1497 };
1498
1499 compile(&opts).expect("external integer(16) call should codegen at O0");
1500 let asm = fs::read_to_string(&output).expect("missing emitted assembly");
1501 assert!(asm.contains("bl _add_ext"), "expected external helper call in asm:\n{}", asm);
1502 assert!(asm.contains("stp x0, x1"), "expected pair-register i128 ABI spill in asm:\n{}", asm);
1503 let _ = fs::remove_file(output);
1504 }
1505
1506 #[test]
1507 fn backend_allows_integer16_mul_after_o1_const_fold() {
1508 let output = std::env::temp_dir().join(format!(
1509 "armfortas_i128_mul_{}_{}.s",
1510 std::process::id(),
1511 "o1"
1512 ));
1513 let opts = Options {
1514 input: i128_reject_fixture(),
1515 output: Some(output.clone()),
1516 emit_asm: true,
1517 emit_obj: false,
1518 emit_ir: false,
1519 preprocess_only: false,
1520 opt_level: OptLevel::O1,
1521 extra_inputs: vec![],
1522 module_search_paths: vec![],
1523 ..Options::default()
1524 };
1525
1526 compile(&opts).expect("integer(16) multiply should codegen at O1 after const fold");
1527 let asm = fs::read_to_string(&output).expect("missing emitted assembly");
1528 assert!(asm.contains("movz x16, #42"), "expected folded i128 constant in asm:\n{}", asm);
1529 assert!(!asm.contains("mul "), "expected O1 i128 multiply to fold away before backend:\n{}", asm);
1530 let _ = fs::remove_file(output);
1531 }
1532
1533 #[test]
1534 fn main_wrapper_prefers_program_body_over_earlier_helpers() {
1535 let allocated = vec![
1536 MachineFunction::new("bump".into()),
1537 MachineFunction::new("__prog_audit_entry".into()),
1538 ];
1539
1540 assert_eq!(
1541 main_wrapper_target(&allocated),
1542 Some("__prog_audit_entry"),
1543 "main wrapper should call the lowered program body, not the first helper"
1544 );
1545 }
1546 }
1547