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