Rust · 124935 bytes Raw Blame History
1 //! Sprint 32 CLI driver tests.
2 //!
3 //! Each test exercises one user-visible behaviour of the `armfortas`
4 //! / `afs` driver via subprocess invocation. Subprocess use is
5 //! deliberate — we want to catch wrong-exit-code, wrong-stdout-vs-
6 //! stderr-routing, and missing-symbol-from-bin issues that an
7 //! in-process API call wouldn't see.
8
9 use std::path::PathBuf;
10 use std::process::Command;
11
12 fn compiler(name: &str) -> PathBuf {
13 if let Some(path) = std::env::var_os(format!("CARGO_BIN_EXE_{}", name)) {
14 return PathBuf::from(path);
15 }
16 let candidate = PathBuf::from("target/debug").join(name);
17 if candidate.exists() {
18 return std::fs::canonicalize(candidate).expect("cannot canonicalize debug compiler path");
19 }
20 let candidate = PathBuf::from("target/release").join(name);
21 if candidate.exists() {
22 return std::fs::canonicalize(candidate)
23 .expect("cannot canonicalize release compiler path");
24 }
25 panic!(
26 "compiler binary '{}' not built — run `cargo build --bins` first",
27 name
28 );
29 }
30
31 fn unique_path(stem: &str, ext: &str) -> PathBuf {
32 let pid = std::process::id();
33 let nanos = std::time::SystemTime::now()
34 .duration_since(std::time::UNIX_EPOCH)
35 .unwrap()
36 .as_nanos();
37 std::env::temp_dir().join(format!("afs_cli_{}_{}_{}.{}", stem, pid, nanos, ext))
38 }
39
40 fn unique_dir(stem: &str) -> PathBuf {
41 let dir = unique_path(stem, "dir");
42 std::fs::create_dir_all(&dir).expect("cannot create CLI test directory");
43 dir
44 }
45
46 fn write_program(text: &str, suffix: &str) -> PathBuf {
47 let path = unique_path("src", suffix);
48 std::fs::write(&path, text).expect("cannot write CLI test source");
49 path
50 }
51
52 fn write_program_in(dir: &std::path::Path, name: &str, text: &str) -> PathBuf {
53 let path = dir.join(name);
54 std::fs::write(&path, text).expect("cannot write CLI test source");
55 path
56 }
57
58 fn undefined_symbols(path: &std::path::Path) -> Vec<String> {
59 let out = Command::new("nm")
60 .args(["-u", "-j", path.to_str().unwrap()])
61 .output()
62 .expect("failed to spawn nm");
63 assert!(
64 out.status.success(),
65 "nm failed for {}: {}",
66 path.display(),
67 String::from_utf8_lossy(&out.stderr)
68 );
69 String::from_utf8_lossy(&out.stdout)
70 .lines()
71 .map(str::trim)
72 .filter(|line| !line.is_empty())
73 .map(ToOwned::to_owned)
74 .collect()
75 }
76
77 #[test]
78 fn version_flag_prints_version_string_to_stdout() {
79 let out = Command::new(compiler("armfortas"))
80 .arg("--version")
81 .output()
82 .expect("failed to spawn armfortas");
83 assert!(out.status.success(), "exit code: {:?}", out.status);
84 let stdout = String::from_utf8_lossy(&out.stdout);
85 assert!(
86 stdout.contains("armfortas") && stdout.contains("0.1.0"),
87 "unexpected --version output: {}",
88 stdout
89 );
90 // The version string belongs on stdout (not stderr) per
91 // gfortran/clang convention; users shell-pipe it.
92 assert!(
93 out.stderr.is_empty(),
94 "stderr should be empty: {:?}",
95 String::from_utf8_lossy(&out.stderr)
96 );
97 }
98
99 #[test]
100 fn help_flag_shows_usage_and_exits_zero() {
101 let out = Command::new(compiler("armfortas"))
102 .arg("--help")
103 .output()
104 .expect("failed to spawn armfortas");
105 assert!(out.status.success(), "--help should succeed");
106 let stdout = String::from_utf8_lossy(&out.stdout);
107 assert!(stdout.contains("USAGE"), "help missing USAGE line");
108 assert!(stdout.contains("--std="), "help missing --std= entry");
109 }
110
111 #[test]
112 fn dumpversion_prints_just_the_version_number() {
113 let out = Command::new(compiler("armfortas"))
114 .arg("-dumpversion")
115 .output()
116 .expect("failed to spawn armfortas");
117 assert!(out.status.success());
118 let stdout = String::from_utf8_lossy(&out.stdout);
119 assert_eq!(stdout.trim(), "0.1.0");
120 }
121
122 #[test]
123 fn afs_alias_runs_the_same_compiler() {
124 let out = Command::new(compiler("afs"))
125 .arg("--version")
126 .output()
127 .expect("failed to spawn afs alias");
128 assert!(out.status.success());
129 let stdout = String::from_utf8_lossy(&out.stdout);
130 assert!(
131 stdout.starts_with("afs "),
132 "afs --version should identify itself as afs: {}",
133 stdout
134 );
135 }
136
137 #[test]
138 fn no_args_prints_help_to_stdout_and_exits_zero() {
139 let out = Command::new(compiler("armfortas"))
140 .output()
141 .expect("failed to spawn armfortas");
142 assert!(
143 out.status.success(),
144 "no-arg invocation should show usage help"
145 );
146 let stdout = String::from_utf8_lossy(&out.stdout);
147 assert!(
148 stdout.contains("USAGE"),
149 "no-arg invocation should print help to stdout: {}",
150 stdout
151 );
152 assert!(
153 out.stderr.is_empty(),
154 "no-arg invocation should not print usage to stderr: {}",
155 String::from_utf8_lossy(&out.stderr)
156 );
157 }
158
159 #[test]
160 fn no_input_after_flags_prints_help_and_mentions_missing_input() {
161 let out = Command::new(compiler("armfortas"))
162 .arg("-Wall")
163 .output()
164 .expect("failed to spawn armfortas");
165 assert!(
166 out.status.success(),
167 "flag-only no-input invocation should exit zero"
168 );
169 let stdout = String::from_utf8_lossy(&out.stdout);
170 let stderr = String::from_utf8_lossy(&out.stderr);
171 assert!(stdout.contains("USAGE"), "missing help text: {}", stdout);
172 assert!(
173 stderr.contains("no input file"),
174 "expected missing-input note on stderr: {}",
175 stderr
176 );
177 }
178
179 #[test]
180 fn dash_c_produces_object_file_only() {
181 let src = write_program("module foo\n integer :: x = 1\nend module\n", "f90");
182 let out = unique_path("obj", "o");
183 let result = Command::new(compiler("armfortas"))
184 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
185 .output()
186 .expect("compile failed to spawn");
187 assert!(
188 result.status.success(),
189 "-c compile failed: {}",
190 String::from_utf8_lossy(&result.stderr)
191 );
192 assert!(out.exists(), "-c should produce an object file");
193 let _ = std::fs::remove_file(&out);
194 let _ = std::fs::remove_file(&src);
195 }
196
197 #[test]
198 fn fixed_form_program_compiles_and_runs() {
199 let src = write_program(
200 " PROGRAM P\n INTEGER I, S\n S = 0\n DO 10 I = 1, 3\n S = S + I\n 10 CONTINUE\n PRINT *, S\n END\n",
201 "f",
202 );
203 let out = unique_path("fixed_form", "bin");
204 let compile = Command::new(compiler("armfortas"))
205 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
206 .output()
207 .expect("fixed-form compile failed to spawn");
208 assert!(
209 compile.status.success(),
210 "fixed-form compile failed: {}",
211 String::from_utf8_lossy(&compile.stderr)
212 );
213
214 let run = Command::new(&out).output().expect("fixed-form run failed");
215 assert!(
216 run.status.success(),
217 "fixed-form run failed: {:?}",
218 run.status
219 );
220 let stdout = String::from_utf8_lossy(&run.stdout);
221 assert!(
222 stdout.trim().ends_with('6'),
223 "unexpected fixed-form output: {}",
224 stdout
225 );
226
227 let _ = std::fs::remove_file(&out);
228 let _ = std::fs::remove_file(&src);
229 }
230
231 #[test]
232 fn select_lowering_coerces_mixed_width_branch_values() {
233 let src = write_program(
234 "program p\n implicit none\n integer :: x\n integer(8) :: y\n y = 7_8\n if (y > 0_8) then\n x = 1\n else\n x = y\n end if\n print *, x\nend program\n",
235 "f90",
236 );
237 let out = unique_path("select_mixed_width", "o");
238 let compile = Command::new(compiler("armfortas"))
239 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
240 .output()
241 .expect("mixed-width select compile failed to spawn");
242 assert!(
243 compile.status.success(),
244 "mixed-width select compile failed: {}",
245 String::from_utf8_lossy(&compile.stderr)
246 );
247 assert!(
248 out.exists(),
249 "mixed-width select should produce an object file"
250 );
251
252 let _ = std::fs::remove_file(&out);
253 let _ = std::fs::remove_file(&src);
254 }
255
256 #[test]
257 fn max_intrinsic_coerces_mixed_width_integer_args() {
258 let src = write_program(
259 "program p\n implicit none\n integer :: x\n integer(8) :: y\n y = 7_8\n x = max(1, y)\n print *, x\nend program\n",
260 "f90",
261 );
262 let out = unique_path("max_mixed_width", "o");
263 let compile = Command::new(compiler("armfortas"))
264 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
265 .output()
266 .expect("mixed-width max compile failed to spawn");
267 assert!(
268 compile.status.success(),
269 "mixed-width max compile failed: {}",
270 String::from_utf8_lossy(&compile.stderr)
271 );
272 assert!(
273 out.exists(),
274 "mixed-width max should produce an object file"
275 );
276
277 let _ = std::fs::remove_file(&out);
278 let _ = std::fs::remove_file(&src);
279 }
280
281 #[test]
282 fn counted_do_coerces_mixed_width_bounds() {
283 let src = write_program(
284 "program p\n implicit none\n character(len=5) :: s\n integer :: i, total\n s = 'abc '\n total = 0\n do i = len_trim(s), 1, -1\n total = total + i\n end do\n print *, total\nend program\n",
285 "f90",
286 );
287 let out = unique_path("do_mixed_width", "o");
288 let compile = Command::new(compiler("armfortas"))
289 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
290 .output()
291 .expect("mixed-width DO compile failed to spawn");
292 assert!(
293 compile.status.success(),
294 "mixed-width DO compile failed: {}",
295 String::from_utf8_lossy(&compile.stderr)
296 );
297 assert!(out.exists(), "mixed-width DO should produce an object file");
298
299 let _ = std::fs::remove_file(&out);
300 let _ = std::fs::remove_file(&src);
301 }
302
303 #[test]
304 fn runtime_sized_local_character_uses_runtime_string_support() {
305 let src = write_program(
306 "subroutine f(input, trimmed)\n implicit none\n character(len=*), intent(in) :: input\n integer, intent(out) :: trimmed\n character(len=len(input)) :: working_input\n working_input = input\n trimmed = len_trim(working_input)\nend subroutine\n",
307 "f90",
308 );
309 let out = unique_path("runtime_char_local", "o");
310 let compile = Command::new(compiler("armfortas"))
311 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
312 .output()
313 .expect("runtime-sized local character compile failed to spawn");
314 assert!(
315 compile.status.success(),
316 "runtime-sized local character compile failed: {}",
317 String::from_utf8_lossy(&compile.stderr)
318 );
319
320 let undefined = undefined_symbols(&out);
321 assert!(
322 undefined.iter().any(|sym| sym == "_afs_len_trim"),
323 "runtime-sized local character should call afs_len_trim, undefineds were: {:?}",
324 undefined
325 );
326 assert!(
327 !undefined.iter().any(|sym| sym == "_working_input"),
328 "runtime-sized local character should not lower to an external working_input call: {:?}",
329 undefined
330 );
331 assert!(
332 !undefined.iter().any(|sym| sym == "_len_trim"),
333 "runtime-sized local character should not lower to a raw len_trim symbol: {:?}",
334 undefined
335 );
336
337 let _ = std::fs::remove_file(&out);
338 let _ = std::fs::remove_file(&src);
339 }
340
341 #[test]
342 fn assumed_length_character_dummy_keeps_hidden_length_abi() {
343 let src = write_program(
344 "subroutine f(prompt_str, first)\n implicit none\n character(len=*), intent(in) :: prompt_str\n character(len=1), intent(out) :: first\n first = prompt_str(1:1)\nend subroutine\n",
345 "f90",
346 );
347 let out = unique_path("assumed_len_dummy", "o");
348 let compile = Command::new(compiler("armfortas"))
349 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
350 .output()
351 .expect("assumed-length dummy compile failed to spawn");
352 assert!(
353 compile.status.success(),
354 "assumed-length dummy compile failed: {}",
355 String::from_utf8_lossy(&compile.stderr)
356 );
357
358 let undefined = undefined_symbols(&out);
359 assert!(
360 !undefined.iter().any(|sym| sym == "_prompt_str"),
361 "assumed-length dummy should not become an external prompt_str call: {:?}",
362 undefined
363 );
364
365 let _ = std::fs::remove_file(&out);
366 let _ = std::fs::remove_file(&src);
367 }
368
369 #[test]
370 fn bind_c_name_call_uses_declared_c_symbol() {
371 let src = write_program(
372 "program p\n use iso_c_binding, only: c_int\n implicit none\n interface\n function getpid_c() bind(c, name='getpid') result(pid)\n import :: c_int\n integer(c_int) :: pid\n end function getpid_c\n end interface\n integer(c_int) :: pid\n pid = getpid_c()\nend program\n",
373 "f90",
374 );
375 let out = unique_path("bind_c_name_call", "o");
376 let compile = Command::new(compiler("armfortas"))
377 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
378 .output()
379 .expect("bind(c) name compile failed to spawn");
380 assert!(
381 compile.status.success(),
382 "bind(c) name compile failed: {}",
383 String::from_utf8_lossy(&compile.stderr)
384 );
385
386 let undefined = undefined_symbols(&out);
387 assert!(
388 undefined.iter().any(|sym| sym == "_getpid"),
389 "bind(c, name=...) should call the declared C symbol: {:?}",
390 undefined
391 );
392 assert!(
393 !undefined.iter().any(|sym| sym == "_getpid_c"),
394 "bind(c, name=...) should not call the local Fortran alias: {:?}",
395 undefined
396 );
397
398 let _ = std::fs::remove_file(&out);
399 let _ = std::fs::remove_file(&src);
400 }
401
402 #[test]
403 fn module_procedure_case_and_bind_label_survive_amod_import() {
404 let dir = unique_dir("amod_case_bind");
405 let mod_src = write_program_in(
406 &dir,
407 "m.f90",
408 "module m\n use iso_c_binding, only: c_int\n implicit none\n interface\n function C_CLOSE(fd) bind(c, name='close') result(ret)\n import :: c_int\n integer(c_int), value :: fd\n integer(c_int) :: ret\n end function C_CLOSE\n end interface\ncontains\n function WEXITSTATUS(status) result(exit_status)\n integer(c_int), intent(in) :: status\n integer :: exit_status\n exit_status = status + 1\n end function WEXITSTATUS\nend module\n",
409 );
410 let use_src = write_program_in(
411 &dir,
412 "use_m.f90",
413 "program p\n use iso_c_binding, only: c_int\n use m\n implicit none\n integer(c_int) :: status, closed\n status = WEXITSTATUS(1_c_int)\n closed = C_CLOSE(0_c_int)\nend program\n",
414 );
415
416 let mod_obj = dir.join("m.o");
417 let compile_mod = Command::new(compiler("armfortas"))
418 .current_dir(&dir)
419 .args([
420 "-c",
421 "-J",
422 dir.to_str().unwrap(),
423 mod_src.to_str().unwrap(),
424 "-o",
425 mod_obj.to_str().unwrap(),
426 ])
427 .env("NO_COLOR", "1")
428 .output()
429 .expect("module compile failed to spawn");
430 assert!(
431 compile_mod.status.success(),
432 "module compile failed: {}",
433 String::from_utf8_lossy(&compile_mod.stderr)
434 );
435
436 let use_obj = dir.join("use_m.o");
437 let compile_use = Command::new(compiler("armfortas"))
438 .current_dir(&dir)
439 .args([
440 "-c",
441 "-I",
442 dir.to_str().unwrap(),
443 "-J",
444 dir.to_str().unwrap(),
445 use_src.to_str().unwrap(),
446 "-o",
447 use_obj.to_str().unwrap(),
448 ])
449 .env("NO_COLOR", "1")
450 .output()
451 .expect("consumer compile failed to spawn");
452 assert!(
453 compile_use.status.success(),
454 "consumer compile failed: {}",
455 String::from_utf8_lossy(&compile_use.stderr)
456 );
457
458 let undefined = undefined_symbols(&use_obj);
459 assert!(
460 undefined
461 .iter()
462 .any(|sym| sym == "_afs_modproc_m_WEXITSTATUS"),
463 "mixed-case module procedures should retain case across .amod import: {:?}",
464 undefined
465 );
466 assert!(
467 !undefined
468 .iter()
469 .any(|sym| sym == "_afs_modproc_m_wexitstatus"),
470 "imported mixed-case module procedures should not be downcased: {:?}",
471 undefined
472 );
473 assert!(
474 undefined.iter().any(|sym| sym == "_close"),
475 "bind(c, name=...) procedures should keep binding labels across .amod import: {:?}",
476 undefined
477 );
478 assert!(
479 !undefined.iter().any(|sym| sym == "_c_close"),
480 "bind(c, name=...) procedures should not fall back to Fortran aliases: {:?}",
481 undefined
482 );
483
484 let _ = std::fs::remove_dir_all(&dir);
485 }
486
487 #[test]
488 fn repeat_intrinsic_lowers_to_runtime_symbol() {
489 let src = write_program(
490 "program p\n implicit none\n character(len=:), allocatable :: s\n s = repeat('ab', 3)\n print *, len_trim(s)\nend program\n",
491 "f90",
492 );
493 let out = unique_path("repeat_runtime", "o");
494 let compile = Command::new(compiler("armfortas"))
495 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
496 .output()
497 .expect("repeat intrinsic compile failed to spawn");
498 assert!(
499 compile.status.success(),
500 "repeat intrinsic compile failed: {}",
501 String::from_utf8_lossy(&compile.stderr)
502 );
503
504 let undefined = undefined_symbols(&out);
505 assert!(
506 undefined.iter().any(|sym| sym == "_afs_repeat"),
507 "repeat intrinsic should lower to afs_repeat, undefineds were: {:?}",
508 undefined
509 );
510 assert!(
511 !undefined.iter().any(|sym| sym == "_repeat"),
512 "repeat intrinsic should not lower to a raw repeat symbol: {:?}",
513 undefined
514 );
515
516 let _ = std::fs::remove_file(&out);
517 let _ = std::fs::remove_file(&src);
518 }
519
520 #[test]
521 fn pointer_dummy_associated_lowers_without_raw_symbol() {
522 let src = write_program(
523 "module m\n implicit none\n type :: node_t\n integer :: value = 0\n end type node_t\ncontains\n logical function present(node) result(ok)\n type(node_t), pointer, intent(in) :: node\n ok = associated(node)\n end function present\nend module m\n",
524 "f90",
525 );
526 let out = unique_path("associated_pointer_dummy", "o");
527 let compile = Command::new(compiler("armfortas"))
528 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
529 .env("NO_COLOR", "1")
530 .output()
531 .expect("pointer associated compile failed to spawn");
532 assert!(
533 compile.status.success(),
534 "pointer associated compile failed: {}",
535 String::from_utf8_lossy(&compile.stderr)
536 );
537
538 let undefined = undefined_symbols(&out);
539 assert!(
540 !undefined.iter().any(|sym| sym == "_associated"),
541 "pointer dummy associated() should not escape as a raw symbol: {:?}",
542 undefined
543 );
544
545 let _ = std::fs::remove_file(&out);
546 let _ = std::fs::remove_file(&src);
547 }
548
549 #[test]
550 fn pointer_function_result_associated_lowers_without_raw_symbol() {
551 let src = write_program(
552 "module m\n implicit none\n type :: node_t\n integer :: value = 0\n end type node_t\ncontains\n recursive function parse() result(node)\n type(node_t), pointer :: node, right_node\n nullify(node)\n if (.not. associated(node)) return\n if (.not. associated(right_node)) return\n end function parse\nend module m\n",
553 "f90",
554 );
555 let out = unique_path("associated_pointer_result", "o");
556 let compile = Command::new(compiler("armfortas"))
557 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
558 .env("NO_COLOR", "1")
559 .output()
560 .expect("pointer result associated compile failed to spawn");
561 assert!(
562 compile.status.success(),
563 "pointer result associated compile failed: {}",
564 String::from_utf8_lossy(&compile.stderr)
565 );
566
567 let undefined = undefined_symbols(&out);
568 assert!(
569 !undefined.iter().any(|sym| sym == "_associated"),
570 "pointer function-result associated() should not escape as a raw symbol: {:?}",
571 undefined
572 );
573
574 let _ = std::fs::remove_file(&out);
575 let _ = std::fs::remove_file(&src);
576 }
577
578 #[test]
579 fn component_array_intrinsics_survive_logical_condition_lowering() {
580 let src = write_program(
581 "module m\n implicit none\n type :: cmd_t\n character(:), allocatable :: tokens(:)\n integer, allocatable :: token_lengths(:)\n end type cmd_t\ncontains\n integer function f(cmd, i) result(strip_len)\n type(cmd_t), intent(in) :: cmd\n integer, intent(in) :: i\n if (allocated(cmd%token_lengths) .and. i <= size(cmd%token_lengths) .and. cmd%token_lengths(i) > 0) then\n strip_len = cmd%token_lengths(i)\n else\n strip_len = len_trim(cmd%tokens(i))\n end if\n end function f\nend module m\n",
582 "f90",
583 );
584 let out = unique_path("component_array_condition", "o");
585 let compile = Command::new(compiler("armfortas"))
586 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
587 .output()
588 .expect("component array condition compile failed to spawn");
589 assert!(
590 compile.status.success(),
591 "component array condition compile failed: {}",
592 String::from_utf8_lossy(&compile.stderr)
593 );
594
595 let undefined = undefined_symbols(&out);
596 assert!(
597 undefined.iter().any(|sym| sym == "_afs_array_allocated"),
598 "component array condition should lower allocated() to afs_array_allocated: {:?}",
599 undefined
600 );
601 assert!(
602 undefined.iter().any(|sym| sym == "_afs_array_size"),
603 "component array condition should lower size() to afs_array_size: {:?}",
604 undefined
605 );
606 assert!(
607 undefined.iter().any(|sym| sym == "_afs_len_trim"),
608 "component array condition should lower len_trim() to afs_len_trim: {:?}",
609 undefined
610 );
611 assert!(
612 !undefined
613 .iter()
614 .any(|sym| sym == "_allocated" || sym == "_size"),
615 "component array condition should not call raw allocated/size symbols: {:?}",
616 undefined
617 );
618
619 let _ = std::fs::remove_file(&out);
620 let _ = std::fs::remove_file(&src);
621 }
622
623 #[test]
624 fn allocatable_array_element_component_intrinsics_do_not_escape() {
625 let src = write_program(
626 "module m\n implicit none\n integer, parameter :: max_token_len = 32\n type :: command_t\n character(len=:), allocatable :: tokens(:)\n character(len=max_token_len), allocatable :: prefix_assignments(:)\n character(len=:), allocatable :: heredoc_delimiter\n end type command_t\ncontains\n subroutine f()\n type(command_t), allocatable :: temp_commands(:)\n integer :: i\n allocate(temp_commands(2))\n i = 1\n if (allocated(temp_commands(i)%prefix_assignments)) print *, 1\n if (allocated(temp_commands(i)%tokens)) print *, 2\n if (allocated(temp_commands(i)%heredoc_delimiter)) print *, 3\n end subroutine f\nend module m\n",
627 "f90",
628 );
629 let out = unique_path("allocatable_base_component_intrinsics", "o");
630 let compile = Command::new(compiler("armfortas"))
631 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
632 .env("NO_COLOR", "1")
633 .output()
634 .expect("allocatable base component intrinsic compile failed to spawn");
635 assert!(
636 compile.status.success(),
637 "allocatable base component intrinsic compile failed: {}",
638 String::from_utf8_lossy(&compile.stderr)
639 );
640
641 let undefined = undefined_symbols(&out);
642 assert!(
643 undefined.iter().any(|sym| sym == "_afs_array_allocated"),
644 "allocatable component arrays should lower allocated() to afs_array_allocated: {:?}",
645 undefined
646 );
647 assert!(
648 undefined.iter().any(|sym| sym == "_afs_string_allocated"),
649 "allocatable character components should lower allocated() to afs_string_allocated: {:?}",
650 undefined
651 );
652 assert!(
653 !undefined.iter().any(|sym| sym == "_allocated"),
654 "allocatable array-element component allocated() should not escape as a raw symbol: {:?}",
655 undefined
656 );
657
658 let _ = std::fs::remove_file(&out);
659 let _ = std::fs::remove_file(&src);
660 }
661
662 #[test]
663 fn fixed_component_array_size_lowers_without_raw_symbol() {
664 let src = write_program(
665 "module m\n implicit none\n type :: shell_t\n integer :: vars(4)\n end type shell_t\ncontains\n integer function f(shell) result(n)\n type(shell_t), intent(in) :: shell\n n = size(shell%vars)\n end function f\nend module m\n",
666 "f90",
667 );
668 let out = unique_path("fixed_component_size", "o");
669 let compile = Command::new(compiler("armfortas"))
670 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
671 .env("NO_COLOR", "1")
672 .output()
673 .expect("fixed component size compile failed to spawn");
674 assert!(
675 compile.status.success(),
676 "fixed component size compile failed: {}",
677 String::from_utf8_lossy(&compile.stderr)
678 );
679
680 let undefined = undefined_symbols(&out);
681 assert!(
682 !undefined.iter().any(|sym| sym == "_size"),
683 "fixed-size component array SIZE() should not escape as a raw symbol: {:?}",
684 undefined
685 );
686
687 let _ = std::fs::remove_file(&out);
688 let _ = std::fs::remove_file(&src);
689 }
690
691 #[test]
692 fn allocate_bounds_size_intrinsic_lowers_without_raw_symbol() {
693 let src = write_program(
694 "module m\n implicit none\n type :: string_t\n character(:), allocatable :: str\n end type string_t\n type :: shell_t\n type(string_t), allocatable :: positional_params(:)\n end type shell_t\ncontains\n subroutine f(shell)\n type(shell_t), intent(inout) :: shell\n type(string_t), allocatable :: saved(:)\n integer :: i\n if (allocated(shell%positional_params)) then\n allocate(saved(size(shell%positional_params)))\n do i = 1, size(shell%positional_params)\n saved(i)%str = shell%positional_params(i)%str\n end do\n end if\n end subroutine f\nend module m\n",
695 "f90",
696 );
697 let out = unique_path("allocate_bounds_size", "o");
698 let compile = Command::new(compiler("armfortas"))
699 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
700 .env("NO_COLOR", "1")
701 .output()
702 .expect("allocate-bounds size compile failed to spawn");
703 assert!(
704 compile.status.success(),
705 "allocate-bounds size compile failed: {}",
706 String::from_utf8_lossy(&compile.stderr)
707 );
708
709 let undefined = undefined_symbols(&out);
710 assert!(
711 undefined.iter().any(|sym| sym == "_afs_array_size"),
712 "allocate bounds should still lower size() to afs_array_size: {:?}",
713 undefined
714 );
715 assert!(
716 !undefined.iter().any(|sym| sym == "_size"),
717 "allocate bounds size() should not escape as a raw symbol: {:?}",
718 undefined
719 );
720
721 let _ = std::fs::remove_file(&out);
722 let _ = std::fs::remove_file(&src);
723 }
724
725 #[test]
726 fn fixed_component_array_element_assignment_compiles() {
727 let src = write_program(
728 "module m\n implicit none\n type :: command_t\n integer :: code = 0\n end type command_t\n type :: trap_table_t\n type(command_t) :: commands(3)\n end type trap_table_t\ncontains\n subroutine set_code(tab, i, v)\n type(trap_table_t), intent(inout) :: tab\n integer, intent(in) :: i, v\n tab%commands(i)%code = v\n end subroutine set_code\nend module m\n",
729 "f90",
730 );
731 let out = unique_path("fixed_component_array", "o");
732 let compile = Command::new(compiler("armfortas"))
733 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
734 .output()
735 .expect("fixed component array compile failed to spawn");
736 assert!(
737 compile.status.success(),
738 "fixed component array compile failed: {}",
739 String::from_utf8_lossy(&compile.stderr)
740 );
741
742 let _ = std::fs::remove_file(&out);
743 let _ = std::fs::remove_file(&src);
744 }
745
746 #[test]
747 fn scalar_char_component_ops_and_achar_compile() {
748 let src = write_program(
749 "module m\n implicit none\n type :: shell_t\n character(len=8) :: ifs = ''\n end type shell_t\ncontains\n subroutine f(shell, sep)\n type(shell_t), intent(in) :: shell\n character(len=1), intent(out) :: sep\n if (len_trim(shell%ifs) > 0) then\n sep = shell%ifs(1:1)\n else\n sep = achar(0)\n end if\n end subroutine f\nend module m\n",
750 "f90",
751 );
752 let out = unique_path("scalar_char_component", "o");
753 let compile = Command::new(compiler("armfortas"))
754 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
755 .output()
756 .expect("scalar char component compile failed to spawn");
757 assert!(
758 compile.status.success(),
759 "scalar char component compile failed: {}",
760 String::from_utf8_lossy(&compile.stderr)
761 );
762
763 let undefined = undefined_symbols(&out);
764 assert!(
765 undefined.iter().any(|sym| sym == "_afs_len_trim"),
766 "scalar char component should lower len_trim() to afs_len_trim: {:?}",
767 undefined
768 );
769 assert!(
770 undefined.iter().any(|sym| sym == "_afs_char"),
771 "ACHAR should lower to afs_char: {:?}",
772 undefined
773 );
774 assert!(
775 !undefined.iter().any(|sym| sym == "_achar" || sym == "_ifs"),
776 "scalar char component lowering should not introduce raw achar/ifs symbols: {:?}",
777 undefined
778 );
779
780 let _ = std::fs::remove_file(&out);
781 let _ = std::fs::remove_file(&src);
782 }
783
784 #[test]
785 fn scalar_char_substring_argument_avoids_raw_local_symbol() {
786 let src = write_program(
787 "module m\n implicit none\ncontains\n integer function visual_length(s)\n character(len=*), intent(in) :: s\n visual_length = len_trim(s)\n end function visual_length\n\n integer function run(input) result(n)\n character(len=*), intent(in) :: input\n character(len=len(input)) :: working_input\n working_input = input\n n = visual_length(working_input(2:3))\n end function run\nend module m\n",
788 "f90",
789 );
790 let out = unique_path("char_substring_arg", "o");
791 let compile = Command::new(compiler("armfortas"))
792 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
793 .output()
794 .expect("char substring argument compile failed to spawn");
795 assert!(
796 compile.status.success(),
797 "char substring argument compile failed: {}",
798 String::from_utf8_lossy(&compile.stderr)
799 );
800
801 let undefined = undefined_symbols(&out);
802 assert!(
803 undefined.iter().any(|sym| sym == "_afs_len_trim"),
804 "character dummy call should still route len_trim through the runtime: {:?}",
805 undefined
806 );
807 assert!(
808 !undefined.iter().any(|sym| sym == "_working_input"),
809 "character substring argument should not lower as an external local symbol: {:?}",
810 undefined
811 );
812
813 let _ = std::fs::remove_file(&out);
814 let _ = std::fs::remove_file(&src);
815 }
816
817 #[test]
818 fn allocated_on_derived_array_element_component_uses_descriptor_runtime() {
819 let src = write_program(
820 "program p\n implicit none\n type :: cmd_t\n character(:), allocatable :: tokens(:)\n end type cmd_t\n type(cmd_t) :: cmds(2)\n logical :: ok\n ok = allocated(cmds(1)%tokens)\n if (ok) print *, size(cmds(1)%tokens)\nend program\n",
821 "f90",
822 );
823 let out = unique_path("derived_array_component_allocated", "o");
824 let compile = Command::new(compiler("armfortas"))
825 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
826 .output()
827 .expect("derived array component allocated compile failed to spawn");
828 assert!(
829 compile.status.success(),
830 "derived array component allocated compile failed: {}",
831 String::from_utf8_lossy(&compile.stderr)
832 );
833
834 let undefined = undefined_symbols(&out);
835 assert!(
836 undefined.iter().any(|sym| sym == "_afs_array_allocated"),
837 "allocated(cmds(i)%tokens) should lower to afs_array_allocated: {:?}",
838 undefined
839 );
840 assert!(
841 undefined.iter().any(|sym| sym == "_afs_array_size"),
842 "size(cmds(i)%tokens) should lower to afs_array_size: {:?}",
843 undefined
844 );
845 assert!(
846 !undefined.iter().any(|sym| sym == "_allocated" || sym == "_size"),
847 "derived array element component intrinsics should not call raw allocated/size symbols: {:?}",
848 undefined
849 );
850
851 let _ = std::fs::remove_file(&out);
852 let _ = std::fs::remove_file(&src);
853 }
854
855 #[test]
856 fn named_len_char_component_substring_and_trim_compile() {
857 let src = write_program(
858 "module m\n implicit none\n integer, parameter :: max_token_len = 8\n type :: token_t\n character(len=max_token_len) :: value\n end type token_t\ncontains\n subroutine f(tok, i, is_bang, trimmed)\n type(token_t), intent(in) :: tok\n integer, intent(in) :: i\n logical, intent(out) :: is_bang\n character(len=max_token_len), intent(out) :: trimmed\n is_bang = (tok%value(i:i) == '!')\n trimmed = trim(tok%value)\n end subroutine f\nend module m\n",
859 "f90",
860 );
861 let out = unique_path("named_len_char_component", "o");
862 let compile = Command::new(compiler("armfortas"))
863 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
864 .output()
865 .expect("named-len char component compile failed to spawn");
866 assert!(
867 compile.status.success(),
868 "named-len char component compile failed: {}",
869 String::from_utf8_lossy(&compile.stderr)
870 );
871 assert!(
872 out.exists(),
873 "named-len char component should produce an object file"
874 );
875
876 let _ = std::fs::remove_file(&out);
877 let _ = std::fs::remove_file(&src);
878 }
879
880 #[test]
881 fn imported_derived_array_global_component_access_compiles() {
882 let dir = unique_dir("derived_array_global");
883 let dep = write_program_in(
884 &dir,
885 "dep.f90",
886 "module dep\n implicit none\n type :: item_t\n logical :: active = .false.\n end type item_t\n type(item_t), save :: items(2)\ncontains\n subroutine init_items()\n items(1)%active = .true.\n end subroutine init_items\nend module dep\n",
887 );
888 let user = write_program_in(
889 &dir,
890 "user.f90",
891 "module user_mod\n use dep, only: items\n implicit none\ncontains\n logical function item_active(i)\n integer, intent(in) :: i\n item_active = items(i)%active\n end function item_active\nend module user_mod\n",
892 );
893 let dep_obj = dir.join("dep.o");
894 let user_obj = dir.join("user.o");
895
896 let dep_compile = Command::new(compiler("armfortas"))
897 .args([
898 "-c",
899 dep.to_str().unwrap(),
900 "-J",
901 dir.to_str().unwrap(),
902 "-o",
903 dep_obj.to_str().unwrap(),
904 ])
905 .output()
906 .expect("dep module compile failed to spawn");
907 assert!(
908 dep_compile.status.success(),
909 "dep module compile failed: {}",
910 String::from_utf8_lossy(&dep_compile.stderr)
911 );
912
913 let user_compile = Command::new(compiler("armfortas"))
914 .args([
915 "-c",
916 user.to_str().unwrap(),
917 "-I",
918 dir.to_str().unwrap(),
919 "-J",
920 dir.to_str().unwrap(),
921 "-o",
922 user_obj.to_str().unwrap(),
923 ])
924 .output()
925 .expect("user module compile failed to spawn");
926 assert!(
927 user_compile.status.success(),
928 "user module compile failed: {}",
929 String::from_utf8_lossy(&user_compile.stderr)
930 );
931
932 let _ = std::fs::remove_file(&dep_obj);
933 let _ = std::fs::remove_file(&user_obj);
934 let _ = std::fs::remove_file(dir.join("dep.amod"));
935 let _ = std::fs::remove_file(&dep);
936 let _ = std::fs::remove_file(&user);
937 let _ = std::fs::remove_dir_all(&dir);
938 }
939
940 #[test]
941 fn derived_array_element_assignment_with_pointer_component_compiles() {
942 let src = write_program(
943 "module m\n implicit none\n type :: node_t\n integer :: x = 0\n end type node_t\n type :: entry_t\n character(len=256) :: name\n type(node_t), pointer :: body => null()\n end type entry_t\n type(entry_t), save :: entries(4)\ncontains\n subroutine shift(i)\n integer, intent(in) :: i\n entries(i) = entries(i + 1)\n end subroutine shift\nend module m\n",
944 "f90",
945 );
946 let out = unique_path("derived_array_shift_ptr", "o");
947 let compile = Command::new(compiler("armfortas"))
948 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
949 .output()
950 .expect("derived array shift compile failed to spawn");
951 assert!(
952 compile.status.success(),
953 "derived array shift compile failed: {}",
954 String::from_utf8_lossy(&compile.stderr)
955 );
956
957 let _ = std::fs::remove_file(&out);
958 let _ = std::fs::remove_file(&src);
959 }
960
961 #[test]
962 fn dash_o_equals_form_sets_output_path() {
963 let src = write_program("program p\n print *, 1\nend program\n", "f90");
964 let out = unique_path("oeq", "o");
965 let arg = format!("-o={}", out.display());
966 let result = Command::new(compiler("armfortas"))
967 .args(["-c", src.to_str().unwrap(), &arg])
968 .output()
969 .expect("compile failed to spawn");
970 assert!(
971 result.status.success(),
972 "-o=path compile failed: {}",
973 String::from_utf8_lossy(&result.stderr)
974 );
975 assert!(out.exists(), "-o=path should produce the requested output");
976 let _ = std::fs::remove_file(&out);
977 let _ = std::fs::remove_file(&src);
978 }
979
980 #[test]
981 fn duplicate_o_is_rejected() {
982 let src = write_program("program p\n print *, 1\nend program\n", "f90");
983 let out_a = unique_path("dup_a", "bin");
984 let out_b = unique_path("dup_b", "bin");
985 let result = Command::new(compiler("armfortas"))
986 .args([
987 src.to_str().unwrap(),
988 "-o",
989 out_a.to_str().unwrap(),
990 "-o",
991 out_b.to_str().unwrap(),
992 ])
993 .output()
994 .expect("compile failed to spawn");
995 assert!(!result.status.success(), "duplicate -o should fail");
996 let stderr = String::from_utf8_lossy(&result.stderr);
997 assert!(
998 stderr.contains("duplicate -o"),
999 "expected duplicate -o diagnostic: {}",
1000 stderr
1001 );
1002 assert!(!out_a.exists(), "first output should not be produced");
1003 assert!(!out_b.exists(), "second output should not be produced");
1004 let _ = std::fs::remove_file(&src);
1005 }
1006
1007 #[test]
1008 fn multi_input_dash_c_produces_one_object_per_source() {
1009 let dir = unique_dir("multi_c_ok");
1010 write_program_in(&dir, "m.f90", "module m\n integer :: x = 7\nend module\n");
1011 write_program_in(
1012 &dir,
1013 "user.f90",
1014 "program p\n use m\n print *, x\nend program\n",
1015 );
1016 let result = Command::new(compiler("armfortas"))
1017 .current_dir(&dir)
1018 .args(["-c", "m.f90", "user.f90"])
1019 .output()
1020 .expect("compile failed to spawn");
1021 assert!(
1022 result.status.success(),
1023 "multi-input -c failed: {}",
1024 String::from_utf8_lossy(&result.stderr)
1025 );
1026 assert!(dir.join("m.o").exists(), "module object was not written");
1027 assert!(dir.join("user.o").exists(), "user object was not written");
1028 assert!(
1029 dir.join("m.amod").exists(),
1030 "module interface was not written"
1031 );
1032 let _ = std::fs::remove_dir_all(&dir);
1033 }
1034
1035 #[test]
1036 fn multi_input_dash_c_with_o_is_rejected() {
1037 let dir = unique_dir("multi_c_err");
1038 write_program_in(&dir, "a.f90", "program a\n print *, 1\nend program\n");
1039 write_program_in(&dir, "b.f90", "program b\n print *, 2\nend program\n");
1040 let result = Command::new(compiler("armfortas"))
1041 .current_dir(&dir)
1042 .args(["-c", "a.f90", "b.f90", "-o", "multi.o"])
1043 .output()
1044 .expect("compile failed to spawn");
1045 assert!(
1046 !result.status.success(),
1047 "multi-input -c with -o should fail"
1048 );
1049 let stderr = String::from_utf8_lossy(&result.stderr);
1050 assert!(
1051 stderr.contains("-o") && stderr.contains("multiple input files"),
1052 "expected -c/-o multi-input diagnostic: {}",
1053 stderr
1054 );
1055 assert!(
1056 !dir.join("multi.o").exists(),
1057 "no linked or object output should be produced"
1058 );
1059 let _ = std::fs::remove_dir_all(&dir);
1060 }
1061
1062 #[test]
1063 fn prebuilt_object_input_links_cleanly() {
1064 let src = write_program("program p\n print *, 9\nend program\n", "f90");
1065 let obj = unique_path("link_only_obj", "o");
1066 let compile = Command::new(compiler("armfortas"))
1067 .args(["-c", src.to_str().unwrap(), "-o", obj.to_str().unwrap()])
1068 .output()
1069 .expect("object compile failed to spawn");
1070 assert!(
1071 compile.status.success(),
1072 "object compile failed: {}",
1073 String::from_utf8_lossy(&compile.stderr)
1074 );
1075
1076 let exe = unique_path("link_only_obj", "bin");
1077 let link = Command::new(compiler("armfortas"))
1078 .args([obj.to_str().unwrap(), "-o", exe.to_str().unwrap()])
1079 .output()
1080 .expect("link-only spawn failed");
1081 assert!(
1082 link.status.success(),
1083 "prebuilt object link failed: {}",
1084 String::from_utf8_lossy(&link.stderr)
1085 );
1086 assert!(exe.exists(), "prebuilt object link should write the binary");
1087
1088 let _ = std::fs::remove_file(&exe);
1089 let _ = std::fs::remove_file(&obj);
1090 let _ = std::fs::remove_file(&src);
1091 }
1092
1093 #[test]
1094 fn prebuilt_archive_input_links_after_objects() {
1095 let dir = unique_dir("link_only_archive");
1096 let helper_src = write_program_in(
1097 &dir,
1098 "helper.f90",
1099 "subroutine helper()\n print *, 7\nend subroutine helper\n",
1100 );
1101 let main_src = write_program_in(
1102 &dir,
1103 "main.f90",
1104 "program p\n call helper()\nend program p\n",
1105 );
1106
1107 let helper_obj = dir.join("helper.o");
1108 let compile_helper = Command::new(compiler("armfortas"))
1109 .current_dir(&dir)
1110 .args([
1111 "-c",
1112 helper_src.to_str().unwrap(),
1113 "-o",
1114 helper_obj.to_str().unwrap(),
1115 ])
1116 .output()
1117 .expect("helper compile spawn failed");
1118 assert!(
1119 compile_helper.status.success(),
1120 "helper compile failed: {}",
1121 String::from_utf8_lossy(&compile_helper.stderr)
1122 );
1123
1124 let archive = dir.join("libhelper.a");
1125 let ar = Command::new("ar")
1126 .current_dir(&dir)
1127 .args([
1128 "rcs",
1129 archive.to_str().unwrap(),
1130 helper_obj.to_str().unwrap(),
1131 ])
1132 .output()
1133 .expect("archive spawn failed");
1134 assert!(
1135 ar.status.success(),
1136 "archive creation failed: {}",
1137 String::from_utf8_lossy(&ar.stderr)
1138 );
1139
1140 let main_obj = dir.join("main.o");
1141 let compile_main = Command::new(compiler("armfortas"))
1142 .current_dir(&dir)
1143 .args([
1144 "-c",
1145 main_src.to_str().unwrap(),
1146 "-o",
1147 main_obj.to_str().unwrap(),
1148 ])
1149 .output()
1150 .expect("main compile spawn failed");
1151 assert!(
1152 compile_main.status.success(),
1153 "main compile failed: {}",
1154 String::from_utf8_lossy(&compile_main.stderr)
1155 );
1156
1157 let exe = dir.join("linked_archive");
1158 let link = Command::new(compiler("armfortas"))
1159 .current_dir(&dir)
1160 .args([
1161 main_obj.to_str().unwrap(),
1162 archive.to_str().unwrap(),
1163 "-o",
1164 exe.to_str().unwrap(),
1165 ])
1166 .output()
1167 .expect("archive link spawn failed");
1168 assert!(
1169 link.status.success(),
1170 "prebuilt archive link failed: {}",
1171 String::from_utf8_lossy(&link.stderr)
1172 );
1173 assert!(
1174 exe.exists(),
1175 "prebuilt archive link should write the binary"
1176 );
1177
1178 let _ = std::fs::remove_dir_all(&dir);
1179 }
1180
1181 #[test]
1182 fn dash_capital_s_produces_assembly_text() {
1183 let src = write_program("program p\n print *, 1\nend program\n", "f90");
1184 let out = unique_path("asm", "s");
1185 let result = Command::new(compiler("armfortas"))
1186 .args(["-S", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
1187 .output()
1188 .expect("spawn failed");
1189 assert!(
1190 result.status.success(),
1191 "-S compile failed: {}",
1192 String::from_utf8_lossy(&result.stderr)
1193 );
1194 let asm = std::fs::read_to_string(&out).expect("missing asm output");
1195 assert!(
1196 asm.contains("__TEXT"),
1197 ".s output should contain section directive"
1198 );
1199 let _ = std::fs::remove_file(&out);
1200 let _ = std::fs::remove_file(&src);
1201 }
1202
1203 #[test]
1204 fn dash_capital_e_preprocesses_only() {
1205 let src = write_program(
1206 "#define X 99\nprogram p\n print *, X\nend program\n",
1207 "F90",
1208 );
1209 let out = unique_path("pp", "f90");
1210 let result = Command::new(compiler("armfortas"))
1211 .args(["-E", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
1212 .output()
1213 .expect("spawn failed");
1214 assert!(
1215 result.status.success(),
1216 "-E preprocess failed: {}",
1217 String::from_utf8_lossy(&result.stderr)
1218 );
1219 let pp = std::fs::read_to_string(&out).expect("missing preprocessed output");
1220 assert!(
1221 pp.contains(", 99"),
1222 "preprocessed text should expand the macro: {}",
1223 pp
1224 );
1225 let _ = std::fs::remove_file(&out);
1226 let _ = std::fs::remove_file(&src);
1227 }
1228
1229 #[test]
1230 fn dash_capital_e_without_o_writes_to_stdout() {
1231 let dir = unique_dir("pp_stdout");
1232 write_program_in(
1233 &dir,
1234 "hello.F90",
1235 "#define X 99\nprogram p\n print *, X\nend program\n",
1236 );
1237 let result = Command::new(compiler("armfortas"))
1238 .current_dir(&dir)
1239 .args(["-E", "hello.F90"])
1240 .output()
1241 .expect("spawn failed");
1242 assert!(
1243 result.status.success(),
1244 "-E preprocess failed: {}",
1245 String::from_utf8_lossy(&result.stderr)
1246 );
1247 let stdout = String::from_utf8_lossy(&result.stdout);
1248 assert!(
1249 stdout.contains(", 99"),
1250 "preprocessed output should be written to stdout: {}",
1251 stdout
1252 );
1253 assert!(
1254 !dir.join("hello").exists(),
1255 "default -E output should not create a bare-stem file"
1256 );
1257 let _ = std::fs::remove_dir_all(&dir);
1258 }
1259
1260 #[test]
1261 fn dash_capital_d_defines_preprocessor_macro() {
1262 let src = write_program(
1263 "#ifdef USE_C_STRINGS\n#define X 1\n#else\n#define X 0\n#endif\nprogram p\n print *, X\nend program\n",
1264 "F90",
1265 );
1266 let out = unique_path("pp_define", "f90");
1267 let result = Command::new(compiler("armfortas"))
1268 .args([
1269 "-DUSE_C_STRINGS",
1270 "-E",
1271 src.to_str().unwrap(),
1272 "-o",
1273 out.to_str().unwrap(),
1274 ])
1275 .output()
1276 .expect("spawn failed");
1277 assert!(
1278 result.status.success(),
1279 "-D preprocess failed: {}",
1280 String::from_utf8_lossy(&result.stderr)
1281 );
1282 let pp = std::fs::read_to_string(&out).expect("missing preprocessed output");
1283 assert!(
1284 pp.contains(", 1"),
1285 "preprocessed text should take the defined branch: {}",
1286 pp
1287 );
1288 let _ = std::fs::remove_file(&out);
1289 let _ = std::fs::remove_file(&src);
1290 }
1291
1292 #[test]
1293 fn dash_capital_d_rejects_invalid_macro_name() {
1294 let src = write_program("program p\n print *, 1\nend program\n", "f90");
1295 let result = Command::new(compiler("armfortas"))
1296 .args(["-D1BAD", src.to_str().unwrap(), "-c"])
1297 .output()
1298 .expect("spawn failed");
1299 assert!(
1300 !result.status.success(),
1301 "-D with an invalid macro name should fail"
1302 );
1303 let stderr = String::from_utf8_lossy(&result.stderr);
1304 assert!(
1305 stderr.contains("invalid macro definition"),
1306 "expected invalid macro diagnostic, got: {}",
1307 stderr
1308 );
1309 let _ = std::fs::remove_file(&src);
1310 }
1311
1312 #[test]
1313 fn std_f95_rejects_f2008_error_stop() {
1314 let src = write_program("program p\n error stop 'oops'\nend program\n", "f90");
1315 let out = unique_path("f95", "bin");
1316 let result = Command::new(compiler("armfortas"))
1317 .args([
1318 "--std=f95",
1319 src.to_str().unwrap(),
1320 "-o",
1321 out.to_str().unwrap(),
1322 ])
1323 .output()
1324 .expect("spawn failed");
1325 assert!(
1326 !result.status.success(),
1327 "--std=f95 should reject ERROR STOP"
1328 );
1329 let stderr = String::from_utf8_lossy(&result.stderr);
1330 assert!(
1331 stderr.contains("ERROR STOP") && stderr.contains("F2008"),
1332 "expected ERROR STOP / F2008 error: {}",
1333 stderr
1334 );
1335 let _ = std::fs::remove_file(&src);
1336 }
1337
1338 #[test]
1339 fn std_space_form_is_accepted() {
1340 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1341 let out = unique_path("std_space", "bin");
1342 let result = Command::new(compiler("armfortas"))
1343 .args([
1344 "--std",
1345 "f2018",
1346 src.to_str().unwrap(),
1347 "-o",
1348 out.to_str().unwrap(),
1349 ])
1350 .output()
1351 .expect("spawn failed");
1352 assert!(
1353 result.status.success(),
1354 "--std f2018 should compile: {}",
1355 String::from_utf8_lossy(&result.stderr)
1356 );
1357 assert!(
1358 out.exists(),
1359 "space-form --std should preserve the input path"
1360 );
1361 let _ = std::fs::remove_file(&out);
1362 let _ = std::fs::remove_file(&src);
1363 }
1364
1365 #[test]
1366 fn std_f77_rejects_free_form_source() {
1367 let src = write_program("program p\n print *, 1\nend program\n", "f90");
1368 let out = unique_path("std_f77_free", "o");
1369 let result = Command::new(compiler("armfortas"))
1370 .args([
1371 "--std=f77",
1372 "-c",
1373 src.to_str().unwrap(),
1374 "-o",
1375 out.to_str().unwrap(),
1376 ])
1377 .output()
1378 .expect("spawn failed");
1379 assert!(
1380 !result.status.success(),
1381 "--std=f77 should reject free-form source"
1382 );
1383 let stderr = String::from_utf8_lossy(&result.stderr);
1384 assert!(
1385 stderr.contains("--std=f77 requires fixed-form source"),
1386 "expected fixed-form requirement: {}",
1387 stderr
1388 );
1389 let _ = std::fs::remove_file(&out);
1390 let _ = std::fs::remove_file(&src);
1391 }
1392
1393 #[test]
1394 fn std_f95_rejects_impure_prefix() {
1395 let src = write_program("impure subroutine s()\nend subroutine\n", "f90");
1396 let out = unique_path("std_impure", "o");
1397 let result = Command::new(compiler("armfortas"))
1398 .args([
1399 "--std=f95",
1400 "-c",
1401 src.to_str().unwrap(),
1402 "-o",
1403 out.to_str().unwrap(),
1404 ])
1405 .env("NO_COLOR", "1")
1406 .output()
1407 .expect("spawn failed");
1408 assert!(!result.status.success(), "--std=f95 should reject IMPURE");
1409 let stderr = String::from_utf8_lossy(&result.stderr);
1410 assert!(
1411 stderr.contains("IMPURE") && stderr.contains("F2008"),
1412 "expected IMPURE / F2008 error: {}",
1413 stderr
1414 );
1415 let _ = std::fs::remove_file(&out);
1416 let _ = std::fs::remove_file(&src);
1417 }
1418
1419 #[test]
1420 fn std_f95_rejects_abstract_type() {
1421 let src = write_program(
1422 "module m\n type, abstract :: t\n end type t\nend module m\n",
1423 "f90",
1424 );
1425 let out = unique_path("std_abstract", "o");
1426 let result = Command::new(compiler("armfortas"))
1427 .args([
1428 "--std=f95",
1429 "-c",
1430 src.to_str().unwrap(),
1431 "-o",
1432 out.to_str().unwrap(),
1433 ])
1434 .env("NO_COLOR", "1")
1435 .output()
1436 .expect("spawn failed");
1437 assert!(
1438 !result.status.success(),
1439 "--std=f95 should reject ABSTRACT type"
1440 );
1441 let stderr = String::from_utf8_lossy(&result.stderr);
1442 assert!(
1443 stderr.contains("ABSTRACT type") && stderr.contains("F2003"),
1444 "expected ABSTRACT type / F2003 error: {}",
1445 stderr
1446 );
1447 let _ = std::fs::remove_file(&out);
1448 let _ = std::fs::remove_file(&src);
1449 }
1450
1451 #[test]
1452 fn std_f77_rejects_module_in_fixed_form() {
1453 let src = write_program(
1454 " module m\n implicit none\n end module m\n",
1455 "f",
1456 );
1457 let out = unique_path("std_f77_module", "o");
1458 let result = Command::new(compiler("armfortas"))
1459 .args([
1460 "--std=f77",
1461 "-c",
1462 src.to_str().unwrap(),
1463 "-o",
1464 out.to_str().unwrap(),
1465 ])
1466 .env("NO_COLOR", "1")
1467 .output()
1468 .expect("spawn failed");
1469 assert!(!result.status.success(), "--std=f77 should reject MODULE");
1470 let stderr = String::from_utf8_lossy(&result.stderr);
1471 assert!(
1472 stderr.contains("MODULE") && stderr.contains("F90"),
1473 "expected MODULE / F90 error: {}",
1474 stderr
1475 );
1476 let _ = std::fs::remove_file(&out);
1477 let _ = std::fs::remove_file(&src);
1478 }
1479
1480 #[test]
1481 fn help_and_version_use_last_flag_wins_precedence() {
1482 let help_then_version = Command::new(compiler("armfortas"))
1483 .args(["--help", "--version"])
1484 .output()
1485 .expect("spawn failed");
1486 assert!(help_then_version.status.success());
1487 let hv_stdout = String::from_utf8_lossy(&help_then_version.stdout);
1488 assert!(
1489 hv_stdout.trim_start().starts_with("armfortas "),
1490 "expected trailing --version to win: {}",
1491 hv_stdout
1492 );
1493
1494 let version_then_help = Command::new(compiler("armfortas"))
1495 .args(["--version", "--help"])
1496 .output()
1497 .expect("spawn failed");
1498 assert!(version_then_help.status.success());
1499 let vh_stdout = String::from_utf8_lossy(&version_then_help.stdout);
1500 assert!(
1501 vh_stdout.contains("USAGE"),
1502 "expected trailing --help to win: {}",
1503 vh_stdout
1504 );
1505 }
1506
1507 #[test]
1508 fn response_file_supplies_arguments() {
1509 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1510 let out = unique_path("resp", "bin");
1511 let resp = unique_path("flags", "txt");
1512 std::fs::write(
1513 &resp,
1514 format!("-O1\n-o\n{}\n{}\n", out.display(), src.display()),
1515 )
1516 .unwrap();
1517 let result = Command::new(compiler("armfortas"))
1518 .arg(format!("@{}", resp.display()))
1519 .output()
1520 .expect("spawn failed");
1521 assert!(
1522 result.status.success(),
1523 "@response-file compile failed: {}",
1524 String::from_utf8_lossy(&result.stderr)
1525 );
1526 assert!(out.exists(), "binary should exist after @file compile");
1527 let _ = std::fs::remove_file(&out);
1528 let _ = std::fs::remove_file(&src);
1529 let _ = std::fs::remove_file(&resp);
1530 }
1531
1532 #[test]
1533 fn diagnostics_format_json_is_rejected_until_implemented() {
1534 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1535 let out = unique_path("diag_json", "bin");
1536 let result = Command::new(compiler("armfortas"))
1537 .args([
1538 "--diagnostics-format=json",
1539 src.to_str().unwrap(),
1540 "-o",
1541 out.to_str().unwrap(),
1542 ])
1543 .output()
1544 .expect("spawn failed");
1545 assert!(
1546 !result.status.success(),
1547 "--diagnostics-format=json should be rejected until implemented"
1548 );
1549 let stderr = String::from_utf8_lossy(&result.stderr);
1550 assert!(
1551 stderr.contains("JSON diagnostics are not yet implemented"),
1552 "expected explicit json-format diagnostic: {}",
1553 stderr
1554 );
1555 let _ = std::fs::remove_file(&src);
1556 let _ = std::fs::remove_file(&out);
1557 }
1558
1559 #[test]
1560 fn nested_response_files_support_quotes_and_relative_paths() {
1561 let dir = unique_dir("rsp nested");
1562 let src = write_program_in(
1563 &dir,
1564 "file with spaces.f90",
1565 "program p\n print *, 7\nend program\n",
1566 );
1567 let out = dir.join("binary with spaces");
1568 let inner = dir.join("inner.rsp");
1569 let outer = dir.join("outer.rsp");
1570 std::fs::write(
1571 &inner,
1572 format!("\"{}\"\n-o\n\"{}\"\n", src.display(), out.display()),
1573 )
1574 .unwrap();
1575 std::fs::write(&outer, "@inner.rsp\n").unwrap();
1576
1577 let result = Command::new(compiler("armfortas"))
1578 .current_dir(&dir)
1579 .arg("@outer.rsp")
1580 .output()
1581 .expect("spawn failed");
1582 assert!(
1583 result.status.success(),
1584 "nested quoted response files should compile: {}",
1585 String::from_utf8_lossy(&result.stderr)
1586 );
1587 assert!(out.exists(), "nested response file should produce output");
1588 let _ = std::fs::remove_dir_all(&dir);
1589 }
1590
1591 #[test]
1592 fn accepted_but_unimplemented_flags_emit_warnings() {
1593 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1594 let out = unique_path("warn_flags", "o");
1595 let result = Command::new(compiler("armfortas"))
1596 .args([
1597 "-c",
1598 "-g",
1599 "-fcheck=bounds",
1600 "-fmax-stack-var-size=64",
1601 "-frecursive",
1602 "-fbackslash",
1603 "-Wall",
1604 "-Wextra",
1605 "-Wpedantic",
1606 "-Wdeprecated",
1607 src.to_str().unwrap(),
1608 "-o",
1609 out.to_str().unwrap(),
1610 ])
1611 .output()
1612 .expect("spawn failed");
1613 assert!(
1614 result.status.success(),
1615 "compile with accepted flags should still succeed: {}",
1616 String::from_utf8_lossy(&result.stderr)
1617 );
1618 let stderr = String::from_utf8_lossy(&result.stderr);
1619 for needle in [
1620 "-g is accepted, but debug info emission is not yet implemented",
1621 "-fcheck=bounds currently has no effect",
1622 "-fmax-stack-var-size is recognized but not yet implemented",
1623 "-frecursive is recognized but not yet implemented",
1624 "-fbackslash is recognized but string escape processing is not yet implemented",
1625 "-Wall is recognized but warning-group emission is not yet implemented",
1626 "-Wextra is recognized but warning-group emission is not yet implemented",
1627 ] {
1628 assert!(
1629 stderr.contains(needle),
1630 "missing warning `{}` in {}",
1631 needle,
1632 stderr
1633 );
1634 }
1635 assert!(
1636 !stderr
1637 .contains("-Wpedantic is recognized but warning-group emission is not yet implemented"),
1638 "pedantic should now be a real semantic warning group: {}",
1639 stderr
1640 );
1641 assert!(
1642 !stderr.contains(
1643 "-Wdeprecated is recognized but warning-group emission is not yet implemented"
1644 ),
1645 "deprecated should now be a real semantic warning group: {}",
1646 stderr
1647 );
1648 let _ = std::fs::remove_file(&src);
1649 let _ = std::fs::remove_file(&out);
1650 }
1651
1652 #[test]
1653 fn fcheck_all_warns_about_partial_support() {
1654 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1655 let out = unique_path("warn_fcheck_all", "o");
1656 let result = Command::new(compiler("armfortas"))
1657 .args([
1658 "-c",
1659 "-fcheck=all",
1660 src.to_str().unwrap(),
1661 "-o",
1662 out.to_str().unwrap(),
1663 ])
1664 .output()
1665 .expect("spawn failed");
1666 assert!(result.status.success());
1667 let stderr = String::from_utf8_lossy(&result.stderr);
1668 assert!(
1669 stderr.contains("-fcheck=all is accepted, but only array bounds checks exist today"),
1670 "expected -fcheck=all warning: {}",
1671 stderr
1672 );
1673 let _ = std::fs::remove_file(&src);
1674 let _ = std::fs::remove_file(&out);
1675 }
1676
1677 #[test]
1678 fn werror_promotes_cli_warnings_to_errors() {
1679 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1680 let out = unique_path("werror_warn", "o");
1681 let result = Command::new(compiler("armfortas"))
1682 .args([
1683 "-c",
1684 "-Wall",
1685 "-Werror",
1686 src.to_str().unwrap(),
1687 "-o",
1688 out.to_str().unwrap(),
1689 ])
1690 .output()
1691 .expect("spawn failed");
1692 assert!(
1693 !result.status.success(),
1694 "-Werror should promote CLI warnings"
1695 );
1696 let stderr = String::from_utf8_lossy(&result.stderr);
1697 assert!(
1698 stderr.contains(
1699 "error: -Wall is recognized but warning-group emission is not yet implemented"
1700 ),
1701 "expected promoted CLI warning: {}",
1702 stderr
1703 );
1704 let _ = std::fs::remove_file(&src);
1705 let _ = std::fs::remove_file(&out);
1706 }
1707
1708 #[test]
1709 fn unknown_warning_flag_emits_warning() {
1710 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1711 let out = unique_path("wunknown", "o");
1712 let result = Command::new(compiler("armfortas"))
1713 .args([
1714 "-c",
1715 "-Weverything",
1716 src.to_str().unwrap(),
1717 "-o",
1718 out.to_str().unwrap(),
1719 ])
1720 .output()
1721 .expect("spawn failed");
1722 assert!(
1723 result.status.success(),
1724 "unknown -W should warn but compile"
1725 );
1726 let stderr = String::from_utf8_lossy(&result.stderr);
1727 assert!(
1728 stderr.contains("unrecognized warning option '-Weverything'"),
1729 "expected unknown-warning diagnostic: {}",
1730 stderr
1731 );
1732 let _ = std::fs::remove_file(&src);
1733 let _ = std::fs::remove_file(&out);
1734 }
1735
1736 #[test]
1737 fn wpedantic_warns_on_arithmetic_if() {
1738 let src = write_program(
1739 "program p\n integer :: i\n i = 0\n if (i) 10, 20, 30\n10 continue\n20 continue\n30 continue\nend program\n",
1740 "f90",
1741 );
1742 let out = unique_path("wpedantic", "o");
1743 let result = Command::new(compiler("armfortas"))
1744 .args([
1745 "-c",
1746 "-Wpedantic",
1747 src.to_str().unwrap(),
1748 "-o",
1749 out.to_str().unwrap(),
1750 ])
1751 .env("NO_COLOR", "1")
1752 .output()
1753 .expect("spawn failed");
1754 assert!(result.status.success(), "pedantic compile failed");
1755 let stderr = String::from_utf8_lossy(&result.stderr);
1756 assert!(
1757 stderr.contains("warning: arithmetic IF is an obsolescent feature"),
1758 "expected arithmetic IF warning: {}",
1759 stderr
1760 );
1761 let _ = std::fs::remove_file(&src);
1762 let _ = std::fs::remove_file(&out);
1763 }
1764
1765 #[test]
1766 fn wdeprecated_warns_on_common_block() {
1767 let src = write_program(
1768 "program p\n integer :: x\n common /blk/ x\nend program\n",
1769 "f90",
1770 );
1771 let out = unique_path("wdeprecated", "o");
1772 let result = Command::new(compiler("armfortas"))
1773 .args([
1774 "-c",
1775 "-Wdeprecated",
1776 src.to_str().unwrap(),
1777 "-o",
1778 out.to_str().unwrap(),
1779 ])
1780 .env("NO_COLOR", "1")
1781 .output()
1782 .expect("spawn failed");
1783 assert!(result.status.success(), "deprecated compile failed");
1784 let stderr = String::from_utf8_lossy(&result.stderr);
1785 assert!(
1786 stderr.contains("warning: COMMON block is an obsolescent feature"),
1787 "expected COMMON warning: {}",
1788 stderr
1789 );
1790 let _ = std::fs::remove_file(&src);
1791 let _ = std::fs::remove_file(&out);
1792 }
1793
1794 #[test]
1795 fn unknown_warning_flag_can_be_suppressed() {
1796 let src = write_program("program p\n print *, 7\nend program\n", "f90");
1797 let out = unique_path("wunknown_suppressed", "o");
1798 let result = Command::new(compiler("armfortas"))
1799 .args([
1800 "-c",
1801 "-Weverything",
1802 "-Wno-unknown-warning-option",
1803 src.to_str().unwrap(),
1804 "-o",
1805 out.to_str().unwrap(),
1806 ])
1807 .output()
1808 .expect("spawn failed");
1809 assert!(
1810 result.status.success(),
1811 "suppressed unknown -W should compile"
1812 );
1813 let stderr = String::from_utf8_lossy(&result.stderr);
1814 assert!(
1815 !stderr.contains("unrecognized warning option"),
1816 "unknown-warning suppression should silence the warning: {}",
1817 stderr
1818 );
1819 let _ = std::fs::remove_file(&src);
1820 let _ = std::fs::remove_file(&out);
1821 }
1822
1823 #[test]
1824 fn missing_response_file_uses_io_exit_code() {
1825 let result = Command::new(compiler("armfortas"))
1826 .arg("@/definitely/missing/armfortas_cli.rsp")
1827 .output()
1828 .expect("spawn failed");
1829 assert!(
1830 !result.status.success(),
1831 "missing response file should fail"
1832 );
1833 assert_eq!(
1834 result.status.code(),
1835 Some(3),
1836 "response-file read failures should map to I/O exit code"
1837 );
1838 }
1839
1840 #[test]
1841 fn escaped_at_prefixed_input_is_treated_as_literal_filename() {
1842 let dir = unique_dir("at_input");
1843 write_program_in(&dir, "@file.f90", "program p\n print *, 7\nend program\n");
1844 let out = dir.join("at_file.o");
1845 let result = Command::new(compiler("armfortas"))
1846 .current_dir(&dir)
1847 .args(["-c", "@@file.f90", "-o", "at_file.o"])
1848 .output()
1849 .expect("spawn failed");
1850 assert!(
1851 result.status.success(),
1852 "escaped @ input should compile: {}",
1853 String::from_utf8_lossy(&result.stderr)
1854 );
1855 assert!(
1856 out.exists(),
1857 "escaped @ input should produce the object file"
1858 );
1859 let _ = std::fs::remove_dir_all(&dir);
1860 }
1861
1862 #[test]
1863 fn dash_j_writes_amod_to_chosen_directory() {
1864 let src = write_program("module dashj_mod\n integer :: y = 5\nend module\n", "f90");
1865 let out = unique_path("dashjobj", "o");
1866 let amod_dir = std::env::temp_dir().join(format!(
1867 "afs_cli_amod_{}_{}",
1868 std::process::id(),
1869 std::time::SystemTime::now()
1870 .duration_since(std::time::UNIX_EPOCH)
1871 .unwrap()
1872 .as_nanos(),
1873 ));
1874 std::fs::create_dir_all(&amod_dir).unwrap();
1875 let result = Command::new(compiler("armfortas"))
1876 .args([
1877 "-c",
1878 "-J",
1879 amod_dir.to_str().unwrap(),
1880 src.to_str().unwrap(),
1881 "-o",
1882 out.to_str().unwrap(),
1883 ])
1884 .output()
1885 .expect("spawn failed");
1886 assert!(
1887 result.status.success(),
1888 "-J compile failed: {}",
1889 String::from_utf8_lossy(&result.stderr)
1890 );
1891 let amod = amod_dir.join("dashj_mod.amod");
1892 assert!(amod.exists(), "-J should place .amod in the requested dir");
1893 let _ = std::fs::remove_file(&out);
1894 let _ = std::fs::remove_file(&src);
1895 let _ = std::fs::remove_dir_all(&amod_dir);
1896 }
1897
1898 #[test]
1899 fn dash_j_nonexistent_dir_is_hard_error() {
1900 let dir = unique_dir("dashj_bad");
1901 let src = write_program_in(
1902 &dir,
1903 "m.f90",
1904 "module dashj_mod\n integer :: y = 5\nend module\n",
1905 );
1906 let out = dir.join("m.o");
1907 let missing = dir.join("missing_modules");
1908 let result = Command::new(compiler("armfortas"))
1909 .args([
1910 "-c",
1911 "-J",
1912 missing.to_str().unwrap(),
1913 src.to_str().unwrap(),
1914 "-o",
1915 out.to_str().unwrap(),
1916 ])
1917 .output()
1918 .expect("spawn failed");
1919 assert!(!result.status.success(), "-J to missing dir should fail");
1920 assert_eq!(result.status.code(), Some(3), "expected I/O exit code");
1921 let stderr = String::from_utf8_lossy(&result.stderr);
1922 assert!(
1923 stderr.contains("cannot write"),
1924 "expected cannot-write diagnostic: {}",
1925 stderr
1926 );
1927 let _ = std::fs::remove_dir_all(&dir);
1928 }
1929
1930 #[test]
1931 fn dash_i_equals_form_finds_modules() {
1932 let dir = unique_dir("ieq_mod");
1933 let mod_src = write_program_in(
1934 &dir,
1935 "mymod.f90",
1936 "module mymod\n integer :: x = 7\nend module\n",
1937 );
1938 let user_src = write_program_in(
1939 &dir,
1940 "use_mod.f90",
1941 "program p\n use mymod\n print *, x\nend program\n",
1942 );
1943 let mod_obj = dir.join("mymod.o");
1944 let compile_mod = Command::new(compiler("armfortas"))
1945 .args([
1946 "-c",
1947 mod_src.to_str().unwrap(),
1948 "-o",
1949 mod_obj.to_str().unwrap(),
1950 ])
1951 .output()
1952 .expect("module compile spawn failed");
1953 assert!(
1954 compile_mod.status.success(),
1955 "module compile failed: {}",
1956 String::from_utf8_lossy(&compile_mod.stderr)
1957 );
1958
1959 let user_obj = dir.join("use_mod.o");
1960 let include_arg = format!("-I={}", dir.display());
1961 let compile_user = Command::new(compiler("armfortas"))
1962 .args([
1963 &include_arg,
1964 "-c",
1965 user_src.to_str().unwrap(),
1966 "-o",
1967 user_obj.to_str().unwrap(),
1968 ])
1969 .output()
1970 .expect("user compile spawn failed");
1971 assert!(
1972 compile_user.status.success(),
1973 "-I=dir should find module interfaces: {}",
1974 String::from_utf8_lossy(&compile_user.stderr)
1975 );
1976 let _ = std::fs::remove_dir_all(&dir);
1977 }
1978
1979 #[test]
1980 fn block_use_imports_module_values_and_procedures() {
1981 let dir = unique_dir("block_use_mod");
1982 let mod_src = write_program_in(
1983 &dir,
1984 "expansion.f90",
1985 "module expansion\n implicit none\n integer, save :: base_value = 7\ncontains\n function arithmetic_expansion_shell(expr, shell) result(r)\n character(len=*), intent(in) :: expr\n integer, intent(inout) :: shell\n character(len=:), allocatable :: r\n r = trim(expr)\n shell = shell + 1\n end function\nend module\n",
1986 );
1987 let user_src = write_program_in(
1988 &dir,
1989 "user.f90",
1990 "program p\n implicit none\n integer :: shell, total\n character(len=32) :: var_value\n integer :: actual_value_len\n shell = 0\n total = 0\n var_value = '123'\n actual_value_len = 3\n block\n use expansion, only: arithmetic_expansion_shell, base_value\n character(len=:), allocatable :: arith_expr, arith_result\n arith_expr = '$((' // var_value(:actual_value_len) // '))'\n arith_result = arithmetic_expansion_shell(trim(arith_expr), shell)\n total = base_value + len_trim(arith_result)\n end block\n if (shell /= 1) error stop 1\n if (total /= 14) error stop 2\nend program\n",
1991 );
1992 let mod_obj = dir.join("expansion.o");
1993 let compile_mod = Command::new(compiler("armfortas"))
1994 .current_dir(&dir)
1995 .args([
1996 "-c",
1997 "-J",
1998 dir.to_str().unwrap(),
1999 mod_src.to_str().unwrap(),
2000 "-o",
2001 mod_obj.to_str().unwrap(),
2002 ])
2003 .output()
2004 .expect("module compile spawn failed");
2005 assert!(
2006 compile_mod.status.success(),
2007 "module compile failed: {}",
2008 String::from_utf8_lossy(&compile_mod.stderr)
2009 );
2010
2011 let user_obj = dir.join("user.o");
2012 let compile_user = Command::new(compiler("armfortas"))
2013 .current_dir(&dir)
2014 .args([
2015 "-c",
2016 user_src.to_str().unwrap(),
2017 "-o",
2018 user_obj.to_str().unwrap(),
2019 ])
2020 .output()
2021 .expect("user compile spawn failed");
2022 assert!(
2023 compile_user.status.success(),
2024 "BLOCK-local USE imports should compile: {}",
2025 String::from_utf8_lossy(&compile_user.stderr)
2026 );
2027
2028 let _ = std::fs::remove_dir_all(&dir);
2029 }
2030
2031 #[test]
2032 fn block_interface_declares_callable_under_implicit_none() {
2033 let src = write_program(
2034 "subroutine s(acc_status)\n use iso_c_binding, only: c_char, c_int\n implicit none\n integer, intent(out) :: acc_status\n character(kind=c_char), target :: c_path(2)\n block\n interface\n function cache_access(pathname, mode) bind(C, name=\"access\")\n import :: c_char, c_int\n character(kind=c_char), intent(in) :: pathname(*)\n integer(c_int), value :: mode\n integer(c_int) :: cache_access\n end function\n end interface\n acc_status = cache_access(c_path, int(1, c_int))\n end block\nend subroutine\n",
2035 "f90",
2036 );
2037 let out = unique_path("block_interface_decl", "o");
2038 let result = Command::new(compiler("armfortas"))
2039 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2040 .env("NO_COLOR", "1")
2041 .output()
2042 .expect("spawn failed");
2043 assert!(
2044 result.status.success(),
2045 "BLOCK-local interface procedures should count as declared: {}",
2046 String::from_utf8_lossy(&result.stderr)
2047 );
2048 let _ = std::fs::remove_file(&out);
2049 let _ = std::fs::remove_file(&src);
2050 }
2051
2052 #[test]
2053 fn public_derived_type_in_private_module_is_emitted_and_importable() {
2054 let dir = unique_dir("public_type_mod");
2055 let mod_src = write_program_in(
2056 &dir,
2057 "m.f90",
2058 "module m\n implicit none\n private\n public :: make_t\n type, public :: result_t\n integer :: source = 0\n integer :: length = 0\n end type\ncontains\n function make_t() result(res)\n type(result_t) :: res\n res%source = 1\n res%length = 2\n end function\nend module\n",
2059 );
2060 let user_src = write_program_in(
2061 &dir,
2062 "user.f90",
2063 "program p\n use m, only: make_t, result_t\n implicit none\n type(result_t) :: x\n x = make_t()\n if (x%source /= 1) error stop 1\n if (x%length /= 2) error stop 2\nend program\n",
2064 );
2065 let mod_obj = dir.join("m.o");
2066 let compile_mod = Command::new(compiler("armfortas"))
2067 .current_dir(&dir)
2068 .args([
2069 "-c",
2070 "-J",
2071 dir.to_str().unwrap(),
2072 mod_src.to_str().unwrap(),
2073 "-o",
2074 mod_obj.to_str().unwrap(),
2075 ])
2076 .output()
2077 .expect("module compile spawn failed");
2078 assert!(
2079 compile_mod.status.success(),
2080 "module compile failed: {}",
2081 String::from_utf8_lossy(&compile_mod.stderr)
2082 );
2083
2084 let amod = dir.join("m.amod");
2085 let amod_text = std::fs::read_to_string(&amod).expect("module .amod should exist");
2086 assert!(
2087 amod_text.contains("@type result_t"),
2088 "public derived type should be exported to .amod: {}",
2089 amod_text
2090 );
2091 assert!(
2092 amod_text.contains("@field source") && amod_text.contains("@field length"),
2093 "derived type layout should be exported to .amod: {}",
2094 amod_text
2095 );
2096
2097 let user_obj = dir.join("user.o");
2098 let compile_user = Command::new(compiler("armfortas"))
2099 .current_dir(&dir)
2100 .args([
2101 "-c",
2102 user_src.to_str().unwrap(),
2103 "-o",
2104 user_obj.to_str().unwrap(),
2105 ])
2106 .output()
2107 .expect("user compile spawn failed");
2108 assert!(
2109 compile_user.status.success(),
2110 "consumer compile should import the public derived type layout: {}",
2111 String::from_utf8_lossy(&compile_user.stderr)
2112 );
2113
2114 let _ = std::fs::remove_dir_all(&dir);
2115 }
2116
2117 #[test]
2118 fn shared_compile_emits_amod_and_links_cleanly() {
2119 let dir = unique_dir("shared_mod");
2120 let lib_src = write_program_in(
2121 &dir,
2122 "mylib.f90",
2123 "module m\ncontains\n integer function answer()\n answer = 42\n end function\nend module\n",
2124 );
2125 let user_src = write_program_in(
2126 &dir,
2127 "user.f90",
2128 "program p\n use m\n print *, answer()\nend program\n",
2129 );
2130 let dylib = dir.join("libmylib.dylib");
2131 let shared = Command::new(compiler("armfortas"))
2132 .args([
2133 "-shared",
2134 lib_src.to_str().unwrap(),
2135 "-o",
2136 dylib.to_str().unwrap(),
2137 ])
2138 .output()
2139 .expect("shared compile spawn failed");
2140 assert!(
2141 shared.status.success(),
2142 "shared compile failed: {}",
2143 String::from_utf8_lossy(&shared.stderr)
2144 );
2145 assert!(
2146 dir.join("m.amod").exists(),
2147 "shared compile should emit m.amod"
2148 );
2149
2150 let exe = dir.join("use_m");
2151 let dir_str = dir.to_str().unwrap();
2152 let user = Command::new(compiler("armfortas"))
2153 .args([
2154 "-I",
2155 dir_str,
2156 "-L",
2157 dir_str,
2158 "-rpath",
2159 dir_str,
2160 "-lmylib",
2161 user_src.to_str().unwrap(),
2162 "-o",
2163 exe.to_str().unwrap(),
2164 ])
2165 .output()
2166 .expect("user compile spawn failed");
2167 assert!(
2168 user.status.success(),
2169 "consumer link failed: {}",
2170 String::from_utf8_lossy(&user.stderr)
2171 );
2172
2173 let run = Command::new(&exe).output().expect("consumer run failed");
2174 assert!(
2175 run.status.success(),
2176 "consumer run failed: {:?}",
2177 run.status
2178 );
2179 let stdout = String::from_utf8_lossy(&run.stdout);
2180 assert!(
2181 stdout.trim().ends_with("42"),
2182 "unexpected output: {}",
2183 stdout
2184 );
2185 let _ = std::fs::remove_dir_all(&dir);
2186 }
2187
2188 #[test]
2189 fn verbose_flag_streams_phase_lines_to_stderr() {
2190 let src = write_program("program p\n print *, 1\nend program\n", "f90");
2191 let out = unique_path("verbose", "bin");
2192 let result = Command::new(compiler("armfortas"))
2193 .args(["-v", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2194 .output()
2195 .expect("spawn failed");
2196 assert!(result.status.success());
2197 let stderr = String::from_utf8_lossy(&result.stderr);
2198 assert!(
2199 stderr.contains("preprocessing:"),
2200 "verbose missing preprocessing line: {}",
2201 stderr
2202 );
2203 assert!(
2204 stderr.contains("codegen:"),
2205 "verbose missing codegen line: {}",
2206 stderr
2207 );
2208 let _ = std::fs::remove_file(&out);
2209 let _ = std::fs::remove_file(&src);
2210 }
2211
2212 #[test]
2213 fn time_report_prints_phase_table() {
2214 let src = write_program("program p\n print *, 1\nend program\n", "f90");
2215 let out = unique_path("timer", "bin");
2216 let result = Command::new(compiler("armfortas"))
2217 .args([
2218 "--time-report",
2219 src.to_str().unwrap(),
2220 "-o",
2221 out.to_str().unwrap(),
2222 ])
2223 .output()
2224 .expect("spawn failed");
2225 assert!(result.status.success());
2226 let stderr = String::from_utf8_lossy(&result.stderr);
2227 assert!(
2228 stderr.contains("Phase"),
2229 "missing time-report header: {}",
2230 stderr
2231 );
2232 assert!(
2233 stderr.contains("Total"),
2234 "missing time-report total: {}",
2235 stderr
2236 );
2237 let _ = std::fs::remove_file(&out);
2238 let _ = std::fs::remove_file(&src);
2239 }
2240
2241 #[test]
2242 fn time_report_prints_phase_table_on_error() {
2243 let src = write_program("program p\n error stop 'oops'\nend program\n", "f90");
2244 let out = unique_path("timer_err", "bin");
2245 let result = Command::new(compiler("armfortas"))
2246 .args([
2247 "--time-report",
2248 "--std=f95",
2249 src.to_str().unwrap(),
2250 "-o",
2251 out.to_str().unwrap(),
2252 ])
2253 .env("NO_COLOR", "1")
2254 .output()
2255 .expect("spawn failed");
2256 assert!(
2257 !result.status.success(),
2258 "compile should fail under --std=f95"
2259 );
2260 let stderr = String::from_utf8_lossy(&result.stderr);
2261 assert!(
2262 stderr.contains("Phase"),
2263 "missing time-report header: {}",
2264 stderr
2265 );
2266 assert!(
2267 stderr.contains("Total"),
2268 "missing time-report total: {}",
2269 stderr
2270 );
2271 assert!(
2272 stderr.contains("sema"),
2273 "expected failing phase in report: {}",
2274 stderr
2275 );
2276 let _ = std::fs::remove_file(&out);
2277 let _ = std::fs::remove_file(&src);
2278 }
2279
2280 #[test]
2281 fn diagnostic_renders_source_line_and_caret() {
2282 let src = write_program("program p\n error stop 'oops'\nend program\n", "f90");
2283 let out = unique_path("diag", "bin");
2284 let result = Command::new(compiler("armfortas"))
2285 .args([
2286 "--std=f95",
2287 src.to_str().unwrap(),
2288 "-o",
2289 out.to_str().unwrap(),
2290 ])
2291 .env("NO_COLOR", "1")
2292 .output()
2293 .expect("spawn failed");
2294 assert!(!result.status.success());
2295 let stderr = String::from_utf8_lossy(&result.stderr);
2296 // Header line uses the gfortran/clang gutter format.
2297 assert!(
2298 stderr.contains(":2:3: error:"),
2299 "missing standard error header: {}",
2300 stderr
2301 );
2302 // Source line is shown with a numbered gutter (` 2 |`).
2303 assert!(
2304 stderr.contains("| error stop"),
2305 "missing source-line snippet: {}",
2306 stderr
2307 );
2308 // Caret underline lives on a ` |` line.
2309 assert!(
2310 stderr.contains(" |"),
2311 "missing caret gutter: {}",
2312 stderr
2313 );
2314 assert!(stderr.contains("^"), "missing caret marker: {}", stderr);
2315 let _ = std::fs::remove_file(&src);
2316 let _ = std::fs::remove_file(&out);
2317 }
2318
2319 #[test]
2320 fn garbage_text_is_rejected_as_parse_error() {
2321 let src = write_program("this is garbage\n", "f90");
2322 let out = unique_path("garbage", "bin");
2323 let result = Command::new(compiler("armfortas"))
2324 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2325 .env("NO_COLOR", "1")
2326 .output()
2327 .expect("spawn failed");
2328 assert!(
2329 !result.status.success(),
2330 "garbage text should fail to parse"
2331 );
2332 assert_eq!(
2333 result.status.code(),
2334 Some(1),
2335 "garbage text should be a compile-time parse error"
2336 );
2337 let stderr = String::from_utf8_lossy(&result.stderr);
2338 assert!(
2339 stderr.contains("parse error:"),
2340 "expected parse-error header: {}",
2341 stderr
2342 );
2343 assert!(
2344 stderr.contains("| this is garbage"),
2345 "expected source snippet for parse error: {}",
2346 stderr
2347 );
2348 assert!(
2349 stderr.contains("^"),
2350 "expected parse-error caret: {}",
2351 stderr
2352 );
2353 assert!(
2354 !stderr.contains("linker failed"),
2355 "garbage text should not reach the linker: {}",
2356 stderr
2357 );
2358 let _ = std::fs::remove_file(&src);
2359 let _ = std::fs::remove_file(&out);
2360 }
2361
2362 #[test]
2363 fn utf8_lexer_error_reports_character_and_caret() {
2364 let src = write_program("program p\n café = 1\nend program\n", "f90");
2365 let out = unique_path("utf8", "bin");
2366 let result = Command::new(compiler("armfortas"))
2367 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2368 .env("NO_COLOR", "1")
2369 .output()
2370 .expect("spawn failed");
2371 assert!(!result.status.success(), "UTF-8 lexer error should fail");
2372 let stderr = String::from_utf8_lossy(&result.stderr);
2373 assert!(
2374 stderr.contains("lexer error: unexpected character: 'é'"),
2375 "expected full UTF-8 character in lexer diagnostic: {}",
2376 stderr
2377 );
2378 assert!(
2379 stderr.contains("| café = 1"),
2380 "expected lexer source snippet: {}",
2381 stderr
2382 );
2383 assert!(stderr.contains("^"), "expected lexer caret: {}", stderr);
2384 let _ = std::fs::remove_file(&src);
2385 let _ = std::fs::remove_file(&out);
2386 }
2387
2388 #[test]
2389 fn bom_prefixed_source_compiles_cleanly() {
2390 let src = write_program("\u{feff}program p\n print *, 1\nend program\n", "f90");
2391 let out = unique_path("bom", "o");
2392 let result = Command::new(compiler("armfortas"))
2393 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2394 .output()
2395 .expect("spawn failed");
2396 assert!(
2397 result.status.success(),
2398 "BOM-prefixed source should compile: {}",
2399 String::from_utf8_lossy(&result.stderr)
2400 );
2401 assert!(
2402 out.exists(),
2403 "BOM-prefixed compile should produce an object"
2404 );
2405 let _ = std::fs::remove_file(&src);
2406 let _ = std::fs::remove_file(&out);
2407 }
2408
2409 #[test]
2410 fn deeply_nested_expression_fails_gracefully() {
2411 let expr = format!("{}1{}", "(".repeat(1500), ")".repeat(1500));
2412 let src = write_program(
2413 &format!("program p\n integer :: x\n x = {expr}\nend program\n"),
2414 "f90",
2415 );
2416 let out = unique_path("deep_expr", "o");
2417 let result = Command::new(compiler("armfortas"))
2418 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2419 .env("NO_COLOR", "1")
2420 .output()
2421 .expect("spawn failed");
2422 assert!(
2423 !result.status.success(),
2424 "deeply nested expression should be rejected"
2425 );
2426 assert_eq!(
2427 result.status.code(),
2428 Some(1),
2429 "deep-expression overflow should stay a compile error"
2430 );
2431 let stderr = String::from_utf8_lossy(&result.stderr);
2432 assert!(
2433 stderr.contains("expression nesting exceeds parser limit"),
2434 "expected parser depth diagnostic: {}",
2435 stderr
2436 );
2437 assert!(
2438 !stderr.contains("INTERNAL COMPILER ERROR"),
2439 "depth guard should avoid ICE path: {}",
2440 stderr
2441 );
2442 let _ = std::fs::remove_file(&src);
2443 let _ = std::fs::remove_file(&out);
2444 }
2445
2446 #[test]
2447 fn diagnostic_gutter_stays_aligned_for_six_digit_line_numbers() {
2448 let mut src_text = "! filler\n".repeat(100_000);
2449 src_text.push_str("program p\n error stop 'oops'\nend program\n");
2450 let src = write_program(&src_text, "f90");
2451 let out = unique_path("bigline", "bin");
2452 let result = Command::new(compiler("armfortas"))
2453 .args([
2454 "--std=f95",
2455 src.to_str().unwrap(),
2456 "-o",
2457 out.to_str().unwrap(),
2458 ])
2459 .env("NO_COLOR", "1")
2460 .output()
2461 .expect("spawn failed");
2462 assert!(
2463 !result.status.success(),
2464 "compile should fail under --std=f95"
2465 );
2466 let stderr = String::from_utf8_lossy(&result.stderr);
2467 let lines: Vec<_> = stderr.lines().collect();
2468 let idx = lines
2469 .iter()
2470 .position(|line| line.contains("100002 |"))
2471 .expect("missing six-digit source gutter");
2472 let source_line = lines[idx];
2473 let caret_line = *lines.get(idx + 1).expect("missing caret line");
2474 assert_eq!(
2475 source_line.find('|'),
2476 caret_line.find('|'),
2477 "source and caret gutters should stay aligned:\n{}",
2478 stderr
2479 );
2480 let _ = std::fs::remove_file(&src);
2481 let _ = std::fs::remove_file(&out);
2482 }
2483
2484 #[test]
2485 fn integer_pow_overflow_is_diagnosed() {
2486 let src = write_program("program p\n print *, 2**200\nend program\n", "f90");
2487 let out = unique_path("pow_overflow", "bin");
2488 let result = Command::new(compiler("armfortas"))
2489 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2490 .env("NO_COLOR", "1")
2491 .output()
2492 .expect("spawn failed");
2493 assert!(
2494 !result.status.success(),
2495 "constant integer overflow should be rejected"
2496 );
2497 let stderr = String::from_utf8_lossy(&result.stderr);
2498 assert!(
2499 stderr.contains("compile-time integer overflow"),
2500 "expected integer overflow diagnostic: {}",
2501 stderr
2502 );
2503 let _ = std::fs::remove_file(&src);
2504 let _ = std::fs::remove_file(&out);
2505 }
2506
2507 #[test]
2508 fn parameter_integer_literal_overflow_is_diagnosed() {
2509 let src = write_program(
2510 "program p\n integer, parameter :: x = -2147483649\n print *, x\nend program\n",
2511 "f90",
2512 );
2513 let out = unique_path("param_overflow", "bin");
2514 let result = Command::new(compiler("armfortas"))
2515 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2516 .env("NO_COLOR", "1")
2517 .output()
2518 .expect("spawn failed");
2519 assert!(
2520 !result.status.success(),
2521 "parameter overflow should be rejected"
2522 );
2523 let stderr = String::from_utf8_lossy(&result.stderr);
2524 assert!(
2525 stderr.contains("compile-time integer overflow"),
2526 "expected parameter overflow diagnostic: {}",
2527 stderr
2528 );
2529 let _ = std::fs::remove_file(&src);
2530 let _ = std::fs::remove_file(&out);
2531 }
2532
2533 #[test]
2534 fn integer_division_by_zero_is_diagnosed() {
2535 let src = write_program(
2536 "program p\n integer, parameter :: x = 1 / 0\n print *, x\nend program\n",
2537 "f90",
2538 );
2539 let out = unique_path("div_zero", "bin");
2540 let result = Command::new(compiler("armfortas"))
2541 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2542 .env("NO_COLOR", "1")
2543 .output()
2544 .expect("spawn failed");
2545 assert!(
2546 !result.status.success(),
2547 "compile-time integer division by zero should be rejected"
2548 );
2549 let stderr = String::from_utf8_lossy(&result.stderr);
2550 assert!(
2551 stderr.contains("compile-time integer division by zero"),
2552 "expected division-by-zero diagnostic: {}",
2553 stderr
2554 );
2555 let _ = std::fs::remove_file(&src);
2556 let _ = std::fs::remove_file(&out);
2557 }
2558
2559 #[test]
2560 fn no_color_env_suppresses_ansi_escapes() {
2561 let src = write_program("program p\n error stop 'x'\nend program\n", "f90");
2562 let out = unique_path("nocolor", "bin");
2563 let result = Command::new(compiler("armfortas"))
2564 .args([
2565 "--std=f95",
2566 src.to_str().unwrap(),
2567 "-o",
2568 out.to_str().unwrap(),
2569 ])
2570 .env("NO_COLOR", "1")
2571 .env_remove("CLICOLOR_FORCE")
2572 .output()
2573 .expect("spawn failed");
2574 let stderr = String::from_utf8_lossy(&result.stderr);
2575 assert!(
2576 !stderr.contains('\x1b'),
2577 "NO_COLOR must suppress ANSI escapes: {:?}",
2578 stderr
2579 );
2580 let _ = std::fs::remove_file(&src);
2581 let _ = std::fs::remove_file(&out);
2582 }
2583
2584 #[test]
2585 fn clicolor_force_enables_ansi_even_off_a_tty() {
2586 let src = write_program("program p\n error stop 'x'\nend program\n", "f90");
2587 let out = unique_path("forcecolor", "bin");
2588 let result = Command::new(compiler("armfortas"))
2589 .args([
2590 "--std=f95",
2591 src.to_str().unwrap(),
2592 "-o",
2593 out.to_str().unwrap(),
2594 ])
2595 .env("CLICOLOR_FORCE", "1")
2596 .env_remove("NO_COLOR")
2597 .output()
2598 .expect("spawn failed");
2599 let stderr = String::from_utf8_lossy(&result.stderr);
2600 assert!(
2601 stderr.contains('\x1b'),
2602 "CLICOLOR_FORCE must produce ANSI escapes: {:?}",
2603 stderr
2604 );
2605 let _ = std::fs::remove_file(&src);
2606 let _ = std::fs::remove_file(&out);
2607 }
2608
2609 #[test]
2610 fn fimplicit_none_rejects_implicitly_typed_use() {
2611 let src = write_program("program p\n i = 5\n print *, i\nend program\n", "f90");
2612 let out = unique_path("fimplicit", "bin");
2613 let result = Command::new(compiler("armfortas"))
2614 .args([
2615 "-fimplicit-none",
2616 src.to_str().unwrap(),
2617 "-o",
2618 out.to_str().unwrap(),
2619 ])
2620 .env("NO_COLOR", "1")
2621 .output()
2622 .expect("spawn failed");
2623 assert!(
2624 !result.status.success(),
2625 "-fimplicit-none should reject undeclared 'i'"
2626 );
2627 let stderr = String::from_utf8_lossy(&result.stderr);
2628 assert!(
2629 stderr.contains("'i'") && stderr.contains("IMPLICIT NONE is active"),
2630 "expected implicit-none diagnostic: {}",
2631 stderr
2632 );
2633 let _ = std::fs::remove_file(&src);
2634 }
2635
2636 #[test]
2637 fn fimplicit_none_respects_explicit_implicit_rules() {
2638 let src = write_program(
2639 "program p\n implicit integer (i-n)\n i = 5\n print *, i\nend program\n",
2640 "f90",
2641 );
2642 let out = unique_path("fimplicit_explicit", "bin");
2643 let result = Command::new(compiler("armfortas"))
2644 .args([
2645 "-fimplicit-none",
2646 src.to_str().unwrap(),
2647 "-o",
2648 out.to_str().unwrap(),
2649 ])
2650 .output()
2651 .expect("spawn failed");
2652 assert!(
2653 result.status.success(),
2654 "explicit IMPLICIT should win over -fimplicit-none: {}",
2655 String::from_utf8_lossy(&result.stderr)
2656 );
2657 let run = Command::new(&out).output().expect("run failed");
2658 let stdout = String::from_utf8_lossy(&run.stdout);
2659 assert!(
2660 stdout.trim().ends_with('5'),
2661 "expected explicit implicit typing to remain active: {}",
2662 stdout
2663 );
2664 let _ = std::fs::remove_file(&out);
2665 let _ = std::fs::remove_file(&src);
2666 }
2667
2668 #[test]
2669 fn fdefault_integer_8_changes_default_kind() {
2670 let src = write_program(
2671 "program p\n integer :: x\n print *, kind(x)\nend program\n",
2672 "f90",
2673 );
2674 let out = unique_path("defint", "bin");
2675 let result = Command::new(compiler("armfortas"))
2676 .args([
2677 "-fdefault-integer-8",
2678 src.to_str().unwrap(),
2679 "-o",
2680 out.to_str().unwrap(),
2681 ])
2682 .output()
2683 .expect("spawn failed");
2684 assert!(
2685 result.status.success(),
2686 "-fdefault-integer-8 compile failed: {}",
2687 String::from_utf8_lossy(&result.stderr)
2688 );
2689 let run = Command::new(&out).output().expect("run failed");
2690 let stdout = String::from_utf8_lossy(&run.stdout);
2691 assert!(
2692 stdout.trim().ends_with('8'),
2693 "expected kind 8: {:?}",
2694 stdout
2695 );
2696 let _ = std::fs::remove_file(&out);
2697 let _ = std::fs::remove_file(&src);
2698 }
2699
2700 #[test]
2701 fn fdefault_real_8_changes_default_kind() {
2702 let src = write_program(
2703 "program p\n real :: y\n print *, kind(y)\nend program\n",
2704 "f90",
2705 );
2706 let out = unique_path("defreal", "bin");
2707 let result = Command::new(compiler("armfortas"))
2708 .args([
2709 "-fdefault-real-8",
2710 src.to_str().unwrap(),
2711 "-o",
2712 out.to_str().unwrap(),
2713 ])
2714 .output()
2715 .expect("spawn failed");
2716 assert!(result.status.success());
2717 let run = Command::new(&out).output().expect("run failed");
2718 let stdout = String::from_utf8_lossy(&run.stdout);
2719 assert!(
2720 stdout.trim().ends_with('8'),
2721 "expected kind 8: {:?}",
2722 stdout
2723 );
2724 let _ = std::fs::remove_file(&out);
2725 let _ = std::fs::remove_file(&src);
2726 }
2727
2728 #[test]
2729 fn afs_runtime_path_env_overrides_runtime_discovery() {
2730 // Point $AFS_RUNTIME_PATH at a directory that DOES contain the
2731 // real runtime and verify compilation still succeeds — exercises
2732 // the override branch end-to-end without hiding the real runtime.
2733 let rt = PathBuf::from("target/release/libarmfortas_rt.a");
2734 if !rt.exists() {
2735 // Skip silently when running off a tree that only has a
2736 // debug runtime — CI has both; a contributor's fresh clone
2737 // with only `cargo build` will hit release.
2738 return;
2739 }
2740 let rt_dir = rt.parent().unwrap().to_path_buf();
2741 let src = write_program("program p\n print *, 11\nend program\n", "f90");
2742 let out = unique_path("rtpath", "bin");
2743 let result = Command::new(compiler("armfortas"))
2744 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2745 .env("AFS_RUNTIME_PATH", &rt_dir)
2746 .output()
2747 .expect("spawn failed");
2748 assert!(
2749 result.status.success(),
2750 "AFS_RUNTIME_PATH-directed compile failed: {}",
2751 String::from_utf8_lossy(&result.stderr)
2752 );
2753 let _ = std::fs::remove_file(&out);
2754 let _ = std::fs::remove_file(&src);
2755 }
2756
2757 #[test]
2758 fn missing_input_file_reports_io_error() {
2759 let result = Command::new(compiler("armfortas"))
2760 .args(["/nonexistent/path/source.f90"])
2761 .output()
2762 .expect("spawn failed");
2763 assert!(!result.status.success(), "missing input should fail");
2764 // Per sprint 32 #6 exit-code spec: I/O errors (cannot read input)
2765 // map to exit code 3. The driver categorises by error message
2766 // text today; a structured error type is sprint 32 #507.
2767 assert_eq!(
2768 result.status.code(),
2769 Some(3),
2770 "missing input should map to exit code 3 (I/O error), got: {:?}",
2771 result.status
2772 );
2773 }
2774
2775 #[test]
2776 fn entry_statement_reports_not_implemented() {
2777 let src = write_program(
2778 "subroutine f(x)\n integer :: x\n entry g(y)\nend subroutine\n",
2779 "f90",
2780 );
2781 let out = unique_path("entry_stmt", "o");
2782 let result = Command::new(compiler("armfortas"))
2783 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2784 .env("NO_COLOR", "1")
2785 .output()
2786 .expect("spawn failed");
2787 assert!(!result.status.success(), "ENTRY should not compile yet");
2788 let stderr = String::from_utf8_lossy(&result.stderr);
2789 assert!(
2790 stderr.contains("ENTRY statements are recognized but not yet implemented"),
2791 "expected explicit ENTRY diagnostic: {}",
2792 stderr
2793 );
2794 let _ = std::fs::remove_file(&out);
2795 let _ = std::fs::remove_file(&src);
2796 }
2797
2798 #[test]
2799 fn coarray_declaration_reports_not_implemented() {
2800 let src = write_program("program p\n integer :: x[*]\nend program\n", "f90");
2801 let out = unique_path("coarray_decl", "o");
2802 let result = Command::new(compiler("armfortas"))
2803 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2804 .env("NO_COLOR", "1")
2805 .output()
2806 .expect("spawn failed");
2807 assert!(
2808 !result.status.success(),
2809 "coarray declarations should fail honestly"
2810 );
2811 let stderr = String::from_utf8_lossy(&result.stderr);
2812 assert!(
2813 stderr.contains("coarray declarations are recognized but not yet implemented"),
2814 "expected explicit coarray declaration diagnostic: {}",
2815 stderr
2816 );
2817 let _ = std::fs::remove_file(&out);
2818 let _ = std::fs::remove_file(&src);
2819 }
2820
2821 #[test]
2822 fn coarray_sync_reports_not_implemented() {
2823 let src = write_program("program p\n sync all\nend program\n", "f90");
2824 let out = unique_path("coarray_sync", "o");
2825 let result = Command::new(compiler("armfortas"))
2826 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2827 .env("NO_COLOR", "1")
2828 .output()
2829 .expect("spawn failed");
2830 assert!(
2831 !result.status.success(),
2832 "coarray SYNC should fail honestly"
2833 );
2834 let stderr = String::from_utf8_lossy(&result.stderr);
2835 assert!(
2836 stderr.contains("coarray SYNC statements are recognized but not yet implemented"),
2837 "expected explicit coarray SYNC diagnostic: {}",
2838 stderr
2839 );
2840 let _ = std::fs::remove_file(&out);
2841 let _ = std::fs::remove_file(&src);
2842 }
2843
2844 #[test]
2845 fn procedure_pointer_decl_compiles_through_wrapper_calls() {
2846 let src = write_program(
2847 "module m\n implicit none\n abstract interface\n logical function pred(x)\n integer, intent(in) :: x\n end function pred\n subroutine act(x)\n integer, intent(in) :: x\n end subroutine act\n end interface\n procedure(pred), pointer :: p => null()\n procedure(act), pointer :: q => null()\ncontains\n logical function ok(x)\n integer, intent(in) :: x\n ok = .false.\n if (associated(p)) ok = p(x)\n end function ok\n\n subroutine run(x)\n integer, intent(in) :: x\n if (associated(q)) call q(x)\n end subroutine run\nend module\n",
2848 "f90",
2849 );
2850 let out = unique_path("procedure_ptr_decl", "o");
2851 let result = Command::new(compiler("armfortas"))
2852 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2853 .env("NO_COLOR", "1")
2854 .output()
2855 .expect("spawn failed");
2856 assert!(
2857 result.status.success(),
2858 "procedure-pointer declarations should compile: {}",
2859 String::from_utf8_lossy(&result.stderr)
2860 );
2861 let _ = std::fs::remove_file(&out);
2862 let _ = std::fs::remove_file(&src);
2863 }
2864
2865 #[test]
2866 fn procedure_pointer_calls_and_assignment_run_indirectly() {
2867 let src = write_program(
2868 "module m\n implicit none\n abstract interface\n integer function pred(x)\n integer, intent(in) :: x\n end function pred\n subroutine act(x)\n integer, intent(inout) :: x\n end subroutine act\n end interface\n procedure(pred), pointer :: p => null()\n procedure(act), pointer :: q => null()\ncontains\n integer function twice(x)\n integer, intent(in) :: x\n twice = x * 2\n end function twice\n\n subroutine bump(x)\n integer, intent(inout) :: x\n x = x + 1\n end subroutine bump\n\n subroutine init()\n p => twice\n q => bump\n end subroutine init\nend module\n\nprogram main\n use m\n implicit none\n integer :: x\n call init()\n x = p(3)\n call q(x)\n print *, x\nend program main\n",
2869 "f90",
2870 );
2871 let out = unique_path("procedure_ptr_run", "s");
2872 let compile = Command::new(compiler("armfortas"))
2873 .args(["-S", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2874 .env("NO_COLOR", "1")
2875 .output()
2876 .expect("spawn failed");
2877 assert!(
2878 compile.status.success(),
2879 "procedure-pointer indirect call program should lower to assembly: {}",
2880 String::from_utf8_lossy(&compile.stderr)
2881 );
2882 let asm = std::fs::read_to_string(&out).expect("cannot read indirect-call assembly");
2883 assert!(
2884 asm.contains("blr "),
2885 "procedure-pointer calls should lower to BLR: {}",
2886 asm
2887 );
2888 assert!(
2889 asm.contains("_twice@PAGE") && asm.contains("_bump@PAGE"),
2890 "procedure-pointer assignment should materialize callee addresses: {}",
2891 asm
2892 );
2893 assert!(
2894 !asm.contains("bl _p") && !asm.contains("bl _q"),
2895 "procedure-pointer calls should not lower as direct symbol calls: {}",
2896 asm
2897 );
2898
2899 let _ = std::fs::remove_file(&out);
2900 let _ = std::fs::remove_file(&src);
2901 }
2902
2903 #[test]
2904 fn procedure_pointer_module_export_survives_amod_import() {
2905 let dir = unique_dir("procptr_amod");
2906 let mod_src = write_program_in(
2907 &dir,
2908 "control_flow.f90",
2909 "module control_flow\n implicit none\n abstract interface\n subroutine evaluate_condition_interface(n)\n integer, intent(inout) :: n\n end subroutine\n end interface\n procedure(evaluate_condition_interface), pointer, public :: evaluate_condition => null()\nend module\n",
2910 );
2911 let user_src = write_program_in(
2912 &dir,
2913 "executor.f90",
2914 "module executor\n implicit none\ncontains\n subroutine init_control_flow_callbacks()\n use control_flow\n evaluate_condition => evaluate_condition_impl\n end subroutine\n\n subroutine evaluate_condition_impl(n)\n integer, intent(inout) :: n\n n = n + 1\n end subroutine\nend module\n",
2915 );
2916
2917 let mod_obj = dir.join("control_flow.o");
2918 let compile_mod = Command::new(compiler("armfortas"))
2919 .current_dir(&dir)
2920 .args([
2921 "-c",
2922 "-J",
2923 dir.to_str().unwrap(),
2924 mod_src.to_str().unwrap(),
2925 "-o",
2926 mod_obj.to_str().unwrap(),
2927 ])
2928 .output()
2929 .expect("module compile spawn failed");
2930 assert!(
2931 compile_mod.status.success(),
2932 "procedure-pointer module should compile: {}",
2933 String::from_utf8_lossy(&compile_mod.stderr)
2934 );
2935
2936 let user_obj = dir.join("executor.o");
2937 let compile_user = Command::new(compiler("armfortas"))
2938 .current_dir(&dir)
2939 .args([
2940 "-c",
2941 "-I",
2942 dir.to_str().unwrap(),
2943 "-J",
2944 dir.to_str().unwrap(),
2945 user_src.to_str().unwrap(),
2946 "-o",
2947 user_obj.to_str().unwrap(),
2948 ])
2949 .output()
2950 .expect("user compile spawn failed");
2951 assert!(
2952 compile_user.status.success(),
2953 "imported module procedure pointers should survive .amod export/import: {}",
2954 String::from_utf8_lossy(&compile_user.stderr)
2955 );
2956
2957 let _ = std::fs::remove_dir_all(&dir);
2958 }
2959
2960 #[test]
2961 fn char_intrinsics_and_transfer_lower_without_raw_symbols() {
2962 let src = write_program(
2963 "module m\n use iso_c_binding, only: c_funptr, c_intptr_t\ncontains\n subroutine s(buf, mask, ok)\n character(len=:), allocatable, intent(inout) :: buf\n logical, intent(in) :: mask\n logical, intent(out) :: ok\n type(c_funptr) :: sig_ign\n if (allocated(buf)) then\n ok = lgt(trim(buf), 'a')\n else\n ok = .false.\n end if\n ok = ok .or. any(buf(1:1) == ['!', '?'])\n buf = merge(buf // new_line('a'), '?', mask)\n sig_ign = transfer(1_c_intptr_t, sig_ign)\n end subroutine s\nend module m\n",
2964 "f90",
2965 );
2966 let out = unique_path("char_intrinsics_link", "o");
2967 let compile = Command::new(compiler("armfortas"))
2968 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
2969 .env("NO_COLOR", "1")
2970 .output()
2971 .expect("char intrinsic compile failed to spawn");
2972 assert!(
2973 compile.status.success(),
2974 "char intrinsic compile failed: {}",
2975 String::from_utf8_lossy(&compile.stderr)
2976 );
2977
2978 let undefined = undefined_symbols(&out);
2979 assert!(
2980 undefined.iter().any(|sym| sym == "_afs_string_allocated"),
2981 "deferred-char ALLOCATED() should lower to the string runtime: {:?}",
2982 undefined
2983 );
2984 assert!(
2985 undefined.iter().any(|sym| sym == "_afs_lgt"),
2986 "LGT should lower to the string runtime: {:?}",
2987 undefined
2988 );
2989 assert!(
2990 !undefined.iter().any(|sym| {
2991 matches!(
2992 sym.as_str(),
2993 "_allocated" | "_any" | "_merge" | "_new_line" | "_transfer" | "_lgt"
2994 )
2995 }),
2996 "char/link intrinsics should not escape as raw symbols: {:?}",
2997 undefined
2998 );
2999
3000 let _ = std::fs::remove_file(&out);
3001 let _ = std::fs::remove_file(&src);
3002 }
3003
3004 #[test]
3005 fn deferred_char_allocatable_dummy_uses_descriptor_abi() {
3006 let src = write_program(
3007 "module m\ncontains\n subroutine grow(buf, cap, content_len)\n character(len=:), allocatable, intent(inout) :: buf\n integer, intent(inout) :: cap\n integer, intent(in) :: content_len\n character(len=:), allocatable :: tmp\n integer :: new_cap\n new_cap = cap * 2\n allocate(character(len=new_cap) :: tmp)\n if (content_len > 0) tmp(1:content_len) = buf(1:content_len)\n call move_alloc(tmp, buf)\n cap = new_cap\n end subroutine\nend module\n",
3008 "f90",
3009 );
3010 let out = unique_path("deferred_char_dummy", "s");
3011 let compile = Command::new(compiler("armfortas"))
3012 .args(["-S", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
3013 .env("NO_COLOR", "1")
3014 .output()
3015 .expect("spawn failed");
3016 assert!(
3017 compile.status.success(),
3018 "deferred-length allocatable character dummy should lower cleanly: {}",
3019 String::from_utf8_lossy(&compile.stderr)
3020 );
3021 let asm = std::fs::read_to_string(&out).expect("cannot read deferred-char dummy assembly");
3022 assert!(
3023 asm.contains("bl _afs_move_alloc_string"),
3024 "MOVE_ALLOC on deferred-length character dummies should call the string runtime: {}",
3025 asm
3026 );
3027 assert!(
3028 !asm.contains("bl _move_alloc"),
3029 "deferred-length character MOVE_ALLOC should not escape as a raw external call: {}",
3030 asm
3031 );
3032 assert!(
3033 !asm.contains("bl _buf"),
3034 "substringing a deferred-length dummy should not lower as a fake function call: {}",
3035 asm
3036 );
3037
3038 let _ = std::fs::remove_file(&out);
3039 let _ = std::fs::remove_file(&src);
3040 }
3041
3042 #[test]
3043 fn use_renamed_procedure_call_uses_remote_symbol() {
3044 let dir = unique_dir("use_rename_proc");
3045 let mod_src = write_program_in(
3046 &dir,
3047 "m.f90",
3048 "module m\ncontains\n subroutine set_shell_variable()\n end subroutine set_shell_variable\nend module m\n",
3049 );
3050 let user_src = write_program_in(
3051 &dir,
3052 "user.f90",
3053 "module user\ncontains\n subroutine run()\n use m, only: var_set_shell_variable => set_shell_variable\n call var_set_shell_variable()\n end subroutine run\nend module user\n",
3054 );
3055
3056 let mod_obj = dir.join("m.o");
3057 let compile_mod = Command::new(compiler("armfortas"))
3058 .current_dir(&dir)
3059 .args([
3060 "-c",
3061 "-J",
3062 dir.to_str().unwrap(),
3063 mod_src.to_str().unwrap(),
3064 "-o",
3065 mod_obj.to_str().unwrap(),
3066 ])
3067 .output()
3068 .expect("rename module compile spawn failed");
3069 assert!(
3070 compile_mod.status.success(),
3071 "rename source module should compile: {}",
3072 String::from_utf8_lossy(&compile_mod.stderr)
3073 );
3074
3075 let user_obj = dir.join("user.o");
3076 let compile_user = Command::new(compiler("armfortas"))
3077 .current_dir(&dir)
3078 .args([
3079 "-c",
3080 "-I",
3081 dir.to_str().unwrap(),
3082 "-J",
3083 dir.to_str().unwrap(),
3084 user_src.to_str().unwrap(),
3085 "-o",
3086 user_obj.to_str().unwrap(),
3087 ])
3088 .output()
3089 .expect("rename user compile spawn failed");
3090 assert!(
3091 compile_user.status.success(),
3092 "USE-renamed procedure call should compile: {}",
3093 String::from_utf8_lossy(&compile_user.stderr)
3094 );
3095
3096 let undefined = undefined_symbols(&user_obj);
3097 assert!(
3098 undefined
3099 .iter()
3100 .any(|sym| sym == "_afs_modproc_m_set_shell_variable"),
3101 "USE rename should call the imported procedure symbol: {:?}",
3102 undefined
3103 );
3104 assert!(
3105 !undefined.iter().any(|sym| sym == "_var_set_shell_variable"),
3106 "USE rename should not lower to the local alias as a link symbol: {:?}",
3107 undefined
3108 );
3109
3110 let _ = std::fs::remove_dir_all(&dir);
3111 }
3112
3113 #[test]
3114 fn linked_binary_carries_uuid_and_launches() {
3115 let dir = unique_dir("linked_binary_uuid");
3116 let src = write_program_in(
3117 &dir,
3118 "hello.f90",
3119 "program hello\n print *, 42\nend program hello\n",
3120 );
3121
3122 let exe = dir.join("hello.bin");
3123 let compile = Command::new(compiler("armfortas"))
3124 .current_dir(&dir)
3125 .args([src.to_str().unwrap(), "-o", exe.to_str().unwrap()])
3126 .output()
3127 .expect("hello compile spawn failed");
3128 assert!(
3129 compile.status.success(),
3130 "linked hello should compile: {}",
3131 String::from_utf8_lossy(&compile.stderr)
3132 );
3133
3134 let otool = Command::new("otool")
3135 .args(["-l", exe.to_str().unwrap()])
3136 .output()
3137 .expect("otool spawn failed");
3138 assert!(
3139 otool.status.success(),
3140 "otool should inspect linked hello: {}",
3141 String::from_utf8_lossy(&otool.stderr)
3142 );
3143 let load_commands = String::from_utf8_lossy(&otool.stdout);
3144 assert!(
3145 load_commands.contains("LC_UUID"),
3146 "linked hello should carry LC_UUID so dyld accepts it:\n{}",
3147 load_commands
3148 );
3149
3150 let run = Command::new(&exe)
3151 .current_dir(&dir)
3152 .output()
3153 .expect("hello run spawn failed");
3154 assert!(
3155 run.status.success(),
3156 "linked hello should launch successfully:\nstdout:\n{}\nstderr:\n{}",
3157 String::from_utf8_lossy(&run.stdout),
3158 String::from_utf8_lossy(&run.stderr)
3159 );
3160
3161 let _ = std::fs::remove_dir_all(&dir);
3162 }
3163
3164 #[test]
3165 fn same_named_module_helpers_link_without_colliding() {
3166 let dir = unique_dir("module_helper_link_names");
3167 let mod_a = write_program_in(
3168 &dir,
3169 "mod_a.f90",
3170 "module mod_a\ncontains\n subroutine helper()\n print *, 11\n end subroutine helper\n\n subroutine run_a()\n call helper()\n end subroutine run_a\nend module mod_a\n",
3171 );
3172 let mod_b = write_program_in(
3173 &dir,
3174 "mod_b.f90",
3175 "module mod_b\ncontains\n subroutine helper()\n print *, 22\n end subroutine helper\n\n subroutine run_b()\n call helper()\n end subroutine run_b\nend module mod_b\n",
3176 );
3177 let main_src = write_program_in(
3178 &dir,
3179 "main.f90",
3180 "program p\n use mod_a, only: run_a\n use mod_b, only: run_b\n call run_a()\n call run_b()\nend program p\n",
3181 );
3182
3183 let mod_a_obj = dir.join("mod_a.o");
3184 let compile_a = Command::new(compiler("armfortas"))
3185 .current_dir(&dir)
3186 .args([
3187 "-c",
3188 "-J",
3189 dir.to_str().unwrap(),
3190 mod_a.to_str().unwrap(),
3191 "-o",
3192 mod_a_obj.to_str().unwrap(),
3193 ])
3194 .output()
3195 .expect("mod_a compile spawn failed");
3196 assert!(
3197 compile_a.status.success(),
3198 "mod_a should compile: {}",
3199 String::from_utf8_lossy(&compile_a.stderr)
3200 );
3201
3202 let mod_b_obj = dir.join("mod_b.o");
3203 let compile_b = Command::new(compiler("armfortas"))
3204 .current_dir(&dir)
3205 .args([
3206 "-c",
3207 "-I",
3208 dir.to_str().unwrap(),
3209 "-J",
3210 dir.to_str().unwrap(),
3211 mod_b.to_str().unwrap(),
3212 "-o",
3213 mod_b_obj.to_str().unwrap(),
3214 ])
3215 .output()
3216 .expect("mod_b compile spawn failed");
3217 assert!(
3218 compile_b.status.success(),
3219 "mod_b should compile: {}",
3220 String::from_utf8_lossy(&compile_b.stderr)
3221 );
3222
3223 let main_obj = dir.join("main.o");
3224 let compile_main = Command::new(compiler("armfortas"))
3225 .current_dir(&dir)
3226 .args([
3227 "-c",
3228 "-I",
3229 dir.to_str().unwrap(),
3230 "-J",
3231 dir.to_str().unwrap(),
3232 main_src.to_str().unwrap(),
3233 "-o",
3234 main_obj.to_str().unwrap(),
3235 ])
3236 .output()
3237 .expect("main compile spawn failed");
3238 assert!(
3239 compile_main.status.success(),
3240 "main should compile: {}",
3241 String::from_utf8_lossy(&compile_main.stderr)
3242 );
3243
3244 let exe = dir.join("helpers.bin");
3245 let link = Command::new(compiler("armfortas"))
3246 .current_dir(&dir)
3247 .args([
3248 mod_a_obj.to_str().unwrap(),
3249 mod_b_obj.to_str().unwrap(),
3250 main_obj.to_str().unwrap(),
3251 "-o",
3252 exe.to_str().unwrap(),
3253 ])
3254 .output()
3255 .expect("helper link spawn failed");
3256 assert!(
3257 link.status.success(),
3258 "same-named module helpers should link cleanly: {}",
3259 String::from_utf8_lossy(&link.stderr)
3260 );
3261
3262 let _ = std::fs::remove_dir_all(&dir);
3263 }
3264
3265 #[test]
3266 fn contained_helpers_link_without_cross_object_internal_symbol_collisions() {
3267 let dir = unique_dir("contained_helper_link_names");
3268 let mod_a = write_program_in(
3269 &dir,
3270 "mod_a.f90",
3271 "module mod_a\ncontains\n subroutine run_a()\n implicit none\n call helper()\n contains\n subroutine helper()\n print *, 11\n end subroutine helper\n end subroutine run_a\nend module mod_a\n",
3272 );
3273 let mod_b = write_program_in(
3274 &dir,
3275 "mod_b.f90",
3276 "module mod_b\ncontains\n subroutine run_b()\n implicit none\n call helper()\n contains\n subroutine helper()\n print *, 22\n end subroutine helper\n end subroutine run_b\nend module mod_b\n",
3277 );
3278 let main_src = write_program_in(
3279 &dir,
3280 "main.f90",
3281 "program p\n use mod_a, only: run_a\n use mod_b, only: run_b\n call run_a()\n call run_b()\nend program p\n",
3282 );
3283
3284 let mod_a_obj = dir.join("mod_a.o");
3285 let compile_a = Command::new(compiler("armfortas"))
3286 .current_dir(&dir)
3287 .args([
3288 "-c",
3289 "-J",
3290 dir.to_str().unwrap(),
3291 mod_a.to_str().unwrap(),
3292 "-o",
3293 mod_a_obj.to_str().unwrap(),
3294 ])
3295 .output()
3296 .expect("mod_a compile spawn failed");
3297 assert!(
3298 compile_a.status.success(),
3299 "mod_a should compile: {}",
3300 String::from_utf8_lossy(&compile_a.stderr)
3301 );
3302
3303 let mod_b_obj = dir.join("mod_b.o");
3304 let compile_b = Command::new(compiler("armfortas"))
3305 .current_dir(&dir)
3306 .args([
3307 "-c",
3308 "-I",
3309 dir.to_str().unwrap(),
3310 "-J",
3311 dir.to_str().unwrap(),
3312 mod_b.to_str().unwrap(),
3313 "-o",
3314 mod_b_obj.to_str().unwrap(),
3315 ])
3316 .output()
3317 .expect("mod_b compile spawn failed");
3318 assert!(
3319 compile_b.status.success(),
3320 "mod_b should compile: {}",
3321 String::from_utf8_lossy(&compile_b.stderr)
3322 );
3323
3324 let main_obj = dir.join("main.o");
3325 let compile_main = Command::new(compiler("armfortas"))
3326 .current_dir(&dir)
3327 .args([
3328 "-c",
3329 "-I",
3330 dir.to_str().unwrap(),
3331 "-J",
3332 dir.to_str().unwrap(),
3333 main_src.to_str().unwrap(),
3334 "-o",
3335 main_obj.to_str().unwrap(),
3336 ])
3337 .output()
3338 .expect("main compile spawn failed");
3339 assert!(
3340 compile_main.status.success(),
3341 "main should compile: {}",
3342 String::from_utf8_lossy(&compile_main.stderr)
3343 );
3344
3345 let exe = dir.join("contained_helpers.bin");
3346 let link = Command::new(compiler("armfortas"))
3347 .current_dir(&dir)
3348 .args([
3349 mod_a_obj.to_str().unwrap(),
3350 mod_b_obj.to_str().unwrap(),
3351 main_obj.to_str().unwrap(),
3352 "-o",
3353 exe.to_str().unwrap(),
3354 ])
3355 .output()
3356 .expect("contained helper link spawn failed");
3357 assert!(
3358 link.status.success(),
3359 "contained helpers in different objects should link cleanly: {}",
3360 String::from_utf8_lossy(&link.stderr)
3361 );
3362
3363 let _ = std::fs::remove_dir_all(&dir);
3364 }
3365
3366 #[test]
3367 fn program_internal_char_helper_assignment_uses_internal_symbol() {
3368 let dir = unique_dir("program_internal_char_helper");
3369 let src = write_program_in(
3370 &dir,
3371 "p.f90",
3372 "program p\n implicit none\n character(len=16) :: x\n x = helper('a')\ncontains\n function helper(v) result(out)\n character(len=*), intent(in) :: v\n character(len=16) :: out\n out = v\n end function helper\nend program p\n",
3373 );
3374
3375 let obj = dir.join("p.o");
3376 let compile = Command::new(compiler("armfortas"))
3377 .current_dir(&dir)
3378 .args(["-c", src.to_str().unwrap(), "-o", obj.to_str().unwrap()])
3379 .output()
3380 .expect("program compile spawn failed");
3381 assert!(
3382 compile.status.success(),
3383 "program-contained character helper should compile: {}",
3384 String::from_utf8_lossy(&compile.stderr)
3385 );
3386
3387 let undefined = undefined_symbols(&obj);
3388 assert!(
3389 !undefined.iter().any(|sym| sym == "_helper"),
3390 "program-contained character helper should not escape as a raw external symbol: {:?}",
3391 undefined
3392 );
3393
3394 let _ = std::fs::remove_dir_all(&dir);
3395 }
3396
3397 #[test]
3398 fn allocatable_result_helper_assignment_uses_resolved_symbol() {
3399 let dir = unique_dir("alloc_result_helper_symbol");
3400 let src = write_program_in(
3401 &dir,
3402 "m.f90",
3403 "module m\ncontains\n function helper(x) result(y)\n character(len=*), intent(in) :: x\n character(len=:), allocatable :: y\n y = trim(x)\n end function helper\n\n function run(x) result(y)\n character(len=*), intent(in) :: x\n character(len=:), allocatable :: y\n y = helper(x)\n end function run\nend module m\n",
3404 );
3405
3406 let obj = dir.join("m.o");
3407 let compile = Command::new(compiler("armfortas"))
3408 .current_dir(&dir)
3409 .args(["-c", src.to_str().unwrap(), "-o", obj.to_str().unwrap()])
3410 .output()
3411 .expect("module compile spawn failed");
3412 assert!(
3413 compile.status.success(),
3414 "allocatable-result helper source should compile: {}",
3415 String::from_utf8_lossy(&compile.stderr)
3416 );
3417
3418 let undefined = undefined_symbols(&obj);
3419 assert!(
3420 !undefined.iter().any(|sym| sym == "_helper"),
3421 "same-file allocatable-result helper should not lower to a raw external symbol: {:?}",
3422 undefined
3423 );
3424
3425 let _ = std::fs::remove_dir_all(&dir);
3426 }
3427
3428 #[test]
3429 fn derived_pointer_module_global_survives_amod_import() {
3430 let dir = unique_dir("derived_ptr_amod");
3431 let mod_src = write_program_in(
3432 &dir,
3433 "state_mod.f90",
3434 "module state_mod\n implicit none\n type :: node_t\n integer :: value = 0\n end type node_t\n type(node_t), target, save :: backing\n type(node_t), pointer, public, save :: current => null()\ncontains\n subroutine init_state()\n current => backing\n current%value = 1\n end subroutine init_state\nend module state_mod\n",
3435 );
3436 let user_src = write_program_in(
3437 &dir,
3438 "user_mod.f90",
3439 "module user_mod\n implicit none\ncontains\n subroutine bump()\n use state_mod\n if (.not. associated(current)) call init_state()\n current%value = current%value + 1\n end subroutine bump\nend module user_mod\n",
3440 );
3441
3442 let mod_obj = dir.join("state_mod.o");
3443 let compile_mod = Command::new(compiler("armfortas"))
3444 .current_dir(&dir)
3445 .args([
3446 "-c",
3447 "-J",
3448 dir.to_str().unwrap(),
3449 mod_src.to_str().unwrap(),
3450 "-o",
3451 mod_obj.to_str().unwrap(),
3452 ])
3453 .output()
3454 .expect("state module compile spawn failed");
3455 assert!(
3456 compile_mod.status.success(),
3457 "derived-pointer module should compile: {}",
3458 String::from_utf8_lossy(&compile_mod.stderr)
3459 );
3460
3461 let user_obj = dir.join("user_mod.o");
3462 let compile_user = Command::new(compiler("armfortas"))
3463 .current_dir(&dir)
3464 .args([
3465 "-c",
3466 "-I",
3467 dir.to_str().unwrap(),
3468 "-J",
3469 dir.to_str().unwrap(),
3470 user_src.to_str().unwrap(),
3471 "-o",
3472 user_obj.to_str().unwrap(),
3473 ])
3474 .output()
3475 .expect("state user compile spawn failed");
3476 assert!(
3477 compile_user.status.success(),
3478 "imported derived-pointer module globals should survive .amod export/import: {}",
3479 String::from_utf8_lossy(&compile_user.stderr)
3480 );
3481
3482 let _ = std::fs::remove_dir_all(&dir);
3483 }
3484
3485 #[test]
3486 fn deferred_char_pointer_component_compiles_string_pool_style_ops() {
3487 let src = write_program(
3488 "module m\n implicit none\n type :: string_ref\n integer :: str_len = 0\n character(:), pointer :: data => null()\n end type string_ref\n character(len=16), target :: pool(1)\ncontains\n subroutine bind_pool(ref, n)\n type(string_ref), intent(inout) :: ref\n integer, intent(in) :: n\n ref%str_len = n\n ref%data => pool(1)(1:n)\n if (associated(ref%data)) then\n ref%data = ' '\n ref%data(1:1) = 'x'\n end if\n end subroutine bind_pool\n\n subroutine own_alloc(ref, n)\n type(string_ref), intent(inout) :: ref\n integer, intent(in) :: n\n if (associated(ref%data)) deallocate(ref%data)\n allocate(character(len=n) :: ref%data)\n ref%data = 'abc'\n end subroutine own_alloc\nend module\n",
3489 "f90",
3490 );
3491 let out = unique_path("deferred_char_pointer_component", "o");
3492 let result = Command::new(compiler("armfortas"))
3493 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
3494 .env("NO_COLOR", "1")
3495 .output()
3496 .expect("spawn failed");
3497 assert!(
3498 result.status.success(),
3499 "deferred char pointer components should compile: {}",
3500 String::from_utf8_lossy(&result.stderr)
3501 );
3502 let _ = std::fs::remove_file(&out);
3503 let _ = std::fs::remove_file(&src);
3504 }
3505
3506 #[test]
3507 fn logical_allocatable_slice_assignment_compiles() {
3508 let src = write_program(
3509 "program p\n implicit none\n logical, allocatable :: a(:), b(:)\n integer :: n\n n = 4\n allocate(a(n), b(n))\n a = .false.\n b = .true.\n a(1:n) = b(1:n)\n b(2:n-1) = .false.\nend program\n",
3510 "f90",
3511 );
3512 let out = unique_path("logical_slice_assign", "o");
3513 let result = Command::new(compiler("armfortas"))
3514 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
3515 .env("NO_COLOR", "1")
3516 .output()
3517 .expect("spawn failed");
3518 assert!(
3519 result.status.success(),
3520 "logical allocatable slice assignment should compile: {}",
3521 String::from_utf8_lossy(&result.stderr)
3522 );
3523 let _ = std::fs::remove_file(&out);
3524 let _ = std::fs::remove_file(&src);
3525 }
3526
3527 #[test]
3528 fn c_interop_opaque_pointer_values_compile() {
3529 let src = write_program(
3530 "program p\n use iso_c_binding\n implicit none\n type(c_ptr) :: pbuf\n type(c_funptr) :: fptr\n pbuf = c_null_ptr\n fptr = c_null_funptr\nend program\n",
3531 "f90",
3532 );
3533 let out = unique_path("c_interop_opaque_values", "o");
3534 let result = Command::new(compiler("armfortas"))
3535 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
3536 .env("NO_COLOR", "1")
3537 .output()
3538 .expect("spawn failed");
3539 assert!(
3540 result.status.success(),
3541 "C interop opaque pointer values should compile: {}",
3542 String::from_utf8_lossy(&result.stderr)
3543 );
3544 let _ = std::fs::remove_file(&out);
3545 let _ = std::fs::remove_file(&src);
3546 }
3547
3548 #[test]
3549 fn allocated_eqv_on_allocatables_compiles() {
3550 let src = write_program(
3551 "program p\n implicit none\n logical, allocatable :: a(:), b(:)\n logical :: same\n allocate(a(1), b(1))\n same = allocated(a) .eqv. allocated(b)\nend program\n",
3552 "f90",
3553 );
3554 let out = unique_path("allocated_eqv", "o");
3555 let result = Command::new(compiler("armfortas"))
3556 .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
3557 .env("NO_COLOR", "1")
3558 .output()
3559 .expect("spawn failed");
3560 assert!(
3561 result.status.success(),
3562 "allocated() logical combinations should compile: {}",
3563 String::from_utf8_lossy(&result.stderr)
3564 );
3565 let _ = std::fs::remove_file(&out);
3566 let _ = std::fs::remove_file(&src);
3567 }
3568