Rust · 88692 bytes Raw Blame History
1 //! ARMFORTAS module file (.amod) v2 writer and reader.
2 //!
3 //! Format spec: see `.claude/plans/composed-questing-catmull.md`.
4 //!
5 //! The `.amod` file is a human-readable, self-documenting, diffable
6 //! description of a Fortran module's public interface — carrying
7 //! enough information for full ABI-correct separate compilation.
8 //!
9 //! Innovations over gfortran/flang/ifort:
10 //! - Explicit ABI annotations (@abi with register assignments)
11 //! - Optimization hints (@hint leaf, no_globals, cost)
12 //! - Linker symbol names (@ir) for direct FFI
13 //! - Source checksum for staleness detection
14 //! - Polymorphic type tags (@tag)
15 //! - Human-editable for hand-written FFI descriptions
16
17 use std::collections::{BTreeSet, HashMap};
18 use std::fmt::Write;
19 use std::path::Path;
20
21 use crate::ir::inst::{FuncRef, Function, InstKind, Module as IrModule};
22 use crate::ir::lower::ModuleGlobalInfo;
23 use crate::sema::symtab::*;
24 use crate::sema::type_layout::TypeLayoutRegistry;
25
26 /// Stringify a Vec<ArraySpec> as `(dim1; dim2; ...)` where each dim
27 /// is `lower:upper` or just `upper`. Returns None if any dim is not
28 /// `Explicit` (assumed-shape, deferred, etc. round-trip via the
29 /// existing rank-based reconstruction in `load_external_module`).
30 ///
31 /// Used to preserve runtime-shape result bounds across split-file
32 /// submodule compilation. Examples:
33 /// `(n)` → `Explicit { lower: None, upper: Name(n) }`
34 /// `(max(n, 0))` → `Explicit { lower: None, upper: max(n,0) }`
35 /// `(1:n, 1:m)` → two-dim Explicit with both bounds
36 fn stringify_array_bounds(specs: &[crate::ast::decl::ArraySpec]) -> Option<String> {
37 use crate::ast::decl::ArraySpec;
38 let mut parts: Vec<String> = Vec::with_capacity(specs.len());
39 for spec in specs {
40 match spec {
41 ArraySpec::Explicit { lower, upper } => {
42 let upper_s = upper.to_sexpr();
43 if let Some(lo) = lower {
44 parts.push(format!("{}:{}", lo.to_sexpr(), upper_s));
45 } else {
46 parts.push(upper_s);
47 }
48 }
49 _ => return None,
50 }
51 }
52 if parts.is_empty() {
53 return None;
54 }
55 Some(format!("({})", parts.join("; ")))
56 }
57
58 /// Parse a `(dim1; dim2; ...)`-encoded array bounds string back into
59 /// a Vec<ArraySpec> by re-lexing and re-parsing each bound expression
60 /// via the regular Fortran parser. Returns None if the string is
61 /// malformed or any bound expr fails to parse — in that case the
62 /// loader falls back to its rank-based AssumedShape reconstruction.
63 pub(crate) fn parse_array_bounds(s: &str) -> Option<Vec<crate::ast::decl::ArraySpec>> {
64 use crate::ast::decl::ArraySpec;
65 let inner = s.strip_prefix('(').and_then(|s| s.strip_suffix(')'))?;
66 let mut specs = Vec::new();
67 for dim in inner.split(';') {
68 let dim = dim.trim();
69 if dim.is_empty() {
70 return None;
71 }
72 // Find the first `:` at depth 0 (parens/brackets) to split
73 // lower:upper. Don't split on `:` inside function calls.
74 let mut depth: i32 = 0;
75 let mut split_at: Option<usize> = None;
76 for (idx, ch) in dim.char_indices() {
77 match ch {
78 '(' | '[' => depth += 1,
79 ')' | ']' => depth -= 1,
80 ':' if depth == 0 => {
81 split_at = Some(idx);
82 break;
83 }
84 _ => {}
85 }
86 }
87 let (lower_str, upper_str) = match split_at {
88 Some(i) => (Some(&dim[..i]), &dim[i + 1..]),
89 None => (None, dim),
90 };
91 let upper = parse_simple_expr(upper_str.trim())?;
92 let lower = match lower_str {
93 Some(s) => Some(parse_simple_expr(s.trim())?),
94 None => None,
95 };
96 specs.push(ArraySpec::Explicit { lower, upper });
97 }
98 Some(specs)
99 }
100
101 fn parse_simple_expr(src: &str) -> Option<crate::ast::expr::SpannedExpr> {
102 let tokens = crate::lexer::Lexer::tokenize(src, 0).ok()?;
103 let mut parser = crate::parser::Parser::new(&tokens);
104 parser.parse_expr().ok()
105 }
106
107 fn hex_encode_bytes(bytes: &[u8]) -> String {
108 let mut hex = String::with_capacity(bytes.len() * 2);
109 for byte in bytes {
110 hex.push_str(&format!("{:02x}", byte));
111 }
112 hex
113 }
114
115 fn hex_decode_bytes(value: &str) -> Option<Vec<u8>> {
116 if !value.len().is_multiple_of(2) {
117 return None;
118 }
119 let mut bytes = Vec::with_capacity(value.len() / 2);
120 let mut idx = 0usize;
121 while idx < value.len() {
122 let next = idx + 2;
123 let byte = u8::from_str_radix(&value[idx..next], 16).ok()?;
124 bytes.push(byte);
125 idx = next;
126 }
127 Some(bytes)
128 }
129
130 fn encode_nested_field_default_init(init: &crate::sema::type_layout::FieldDefaultInit) -> String {
131 use crate::sema::type_layout::FieldDefaultInit;
132 match init {
133 FieldDefaultInit::Character(value) => format!("C{}", hex_encode_bytes(value.as_bytes())),
134 FieldDefaultInit::Integer(value) => format!("I{}", value),
135 FieldDefaultInit::Logical(value) => format!("L{}", if *value { '1' } else { '0' }),
136 FieldDefaultInit::Derived(fields) => {
137 let rendered = fields
138 .iter()
139 .map(|(name, value)| format!("{name}={}", encode_nested_field_default_init(value)))
140 .collect::<Vec<_>>()
141 .join(",");
142 format!("D({rendered})")
143 }
144 FieldDefaultInit::ProcedurePointer(target) => {
145 format!("P{}", hex_encode_bytes(target.as_bytes()))
146 }
147 }
148 }
149
150 fn split_nested_default_fields(payload: &str) -> Option<Vec<&str>> {
151 let mut out = Vec::new();
152 let mut depth = 0i32;
153 let mut start = 0usize;
154 for (idx, ch) in payload.char_indices() {
155 match ch {
156 '(' => depth += 1,
157 ')' => depth -= 1,
158 ',' if depth == 0 => {
159 out.push(&payload[start..idx]);
160 start = idx + 1;
161 }
162 _ => {}
163 }
164 if depth < 0 {
165 return None;
166 }
167 }
168 if depth != 0 {
169 return None;
170 }
171 if !payload.is_empty() {
172 out.push(&payload[start..]);
173 }
174 Some(out)
175 }
176
177 fn decode_nested_field_default_init(
178 encoded: &str,
179 ) -> Option<crate::sema::type_layout::FieldDefaultInit> {
180 use crate::sema::type_layout::FieldDefaultInit;
181 if let Some(value) = encoded.strip_prefix('C') {
182 let decoded = String::from_utf8(hex_decode_bytes(value)?).ok()?;
183 return Some(FieldDefaultInit::Character(decoded));
184 }
185 if let Some(value) = encoded.strip_prefix('I') {
186 return value.parse::<i128>().ok().map(FieldDefaultInit::Integer);
187 }
188 if let Some(value) = encoded.strip_prefix('L') {
189 return match value {
190 "1" => Some(FieldDefaultInit::Logical(true)),
191 "0" => Some(FieldDefaultInit::Logical(false)),
192 _ => None,
193 };
194 }
195 if let Some(value) = encoded.strip_prefix("D(").and_then(|s| s.strip_suffix(')')) {
196 let mut fields = Vec::new();
197 for entry in split_nested_default_fields(value)? {
198 let (name, payload) = entry.split_once('=')?;
199 let init = decode_nested_field_default_init(payload)?;
200 fields.push((name.to_string(), init));
201 }
202 return Some(FieldDefaultInit::Derived(fields));
203 }
204 if let Some(value) = encoded.strip_prefix('P') {
205 let decoded = String::from_utf8(hex_decode_bytes(value)?).ok()?;
206 return Some(FieldDefaultInit::ProcedurePointer(decoded));
207 }
208 None
209 }
210
211 // =====================================================================
212 // Writer
213 // =====================================================================
214
215 /// Serialize a module's public interface to `.amod` v2 text.
216 pub fn write_amod(
217 module_name: &str,
218 source_path: &str,
219 source_content: &str,
220 st: &SymbolTable,
221 mod_scope_id: ScopeId,
222 globals: &HashMap<(String, String), ModuleGlobalInfo>,
223 type_layouts: &TypeLayoutRegistry,
224 ir_module: &IrModule,
225 descriptor_params: &HashMap<String, Vec<bool>>,
226 char_len_star_params: &HashMap<String, Vec<bool>>,
227 ) -> String {
228 let mut out = String::new();
229 let mod_key = module_name.to_lowercase();
230 let scope = st.scope(mod_scope_id);
231
232 // ---- Header ----
233 writeln!(out, "#!amod 2").unwrap();
234 writeln!(out, "# module: {}", mod_key).unwrap();
235 writeln!(out, "# source: {}", source_path).unwrap();
236 writeln!(out, "# checksum: fnv1a:{}", fnv1a_hex(source_content)).unwrap();
237 writeln!(out, "# compiled: {}", compile_timestamp()).unwrap();
238 writeln!(out, "# compiler: armfortas 0.1.0").unwrap();
239 writeln!(out, "# abi: arm64-apple-darwin").unwrap();
240 writeln!(out).unwrap();
241
242 // ---- Dependencies ----
243 let mut deps: Vec<String> = scope
244 .use_associations
245 .iter()
246 .filter_map(|ua| {
247 let src_scope = st.scope(ua.source_scope);
248 if let ScopeKind::Module(ref n) = src_scope.kind {
249 Some(n.to_lowercase())
250 } else {
251 None
252 }
253 })
254 .collect();
255 deps.sort();
256 deps.dedup();
257 for dep in &deps {
258 writeln!(out, "@uses {}", dep).unwrap();
259 }
260 if !deps.is_empty() {
261 writeln!(out).unwrap();
262 }
263
264 // ---- Use renames ----
265 // Record each `use M, only: a => b` as `@use_rename a = b from m`.
266 // Submodule bodies pulled in by host association need to resolve
267 // names like `block_kind` (renamed from `int64`) for kind selectors
268 // and intrinsic dispatch; without preserving the rename, the .amod
269 // can't reconstruct the kind constant and `integer(block_kind) ::
270 // dummy` falls back to the default kind.
271 let mut renames_out: Vec<(String, String, String)> = scope
272 .use_associations
273 .iter()
274 .filter_map(|ua| {
275 if ua.local_name == ua.original_name {
276 return None;
277 }
278 let src_scope = st.scope(ua.source_scope);
279 if let ScopeKind::Module(ref n) = src_scope.kind {
280 Some((
281 ua.local_name.clone(),
282 ua.original_name.clone(),
283 n.to_lowercase(),
284 ))
285 } else {
286 None
287 }
288 })
289 .collect();
290 renames_out.sort();
291 renames_out.dedup();
292 for (local, original, src) in &renames_out {
293 writeln!(out, "@use_rename {} = {} from {}", local, original, src).unwrap();
294 }
295 if !renames_out.is_empty() {
296 writeln!(out).unwrap();
297 }
298
299 // Collect and sort public symbols (used for procedures /
300 // interfaces / derived types — those still go out public-only).
301 let mut syms: Vec<(&String, &Symbol)> = scope
302 .symbols
303 .iter()
304 .filter(|(_, sym)| is_public(sym, scope))
305 .collect();
306 syms.sort_by_key(|(k, _)| k.to_lowercase());
307
308 // Per F2008 §11.2.3, submodules see ALL parent entities including
309 // private ones. Variables and parameters are emitted regardless
310 // of access; private ones carry a `private` attribute and are
311 // filtered out at ordinary USE-association time.
312 let mut all_syms: Vec<(&String, &Symbol)> = scope.symbols.iter().collect();
313 all_syms.sort_by_key(|(k, _)| k.to_lowercase());
314
315 // ---- Variables ----
316 let vars: Vec<_> = all_syms
317 .iter()
318 .filter(|(_, sym)| {
319 matches!(
320 sym.kind,
321 SymbolKind::Variable | SymbolKind::ProcedurePointer
322 ) && !sym.attrs.parameter
323 })
324 .collect();
325 for (name, sym) in &vars {
326 emit_variable(&mut out, &mod_key, name, sym, globals);
327 }
328 if !vars.is_empty() {
329 writeln!(out).unwrap();
330 }
331
332 // ---- Parameters ----
333 let params: Vec<_> = all_syms
334 .iter()
335 .filter(|(_, sym)| sym.attrs.parameter || matches!(sym.kind, SymbolKind::Parameter))
336 .collect();
337 for (name, sym) in &params {
338 emit_parameter(&mut out, &mod_key, name, sym, globals);
339 }
340 if !params.is_empty() {
341 writeln!(out).unwrap();
342 }
343
344 // ---- Procedures ----
345 let interface_specifics: BTreeSet<String> = syms
346 .iter()
347 .filter(|(_, sym)| {
348 matches!(sym.kind, SymbolKind::NamedInterface)
349 || (matches!(sym.kind, SymbolKind::DerivedType) && !sym.arg_names.is_empty())
350 })
351 .flat_map(|(_, sym)| sym.arg_names.iter().cloned())
352 .collect();
353 // Public derived types can expose private bound procedure targets across
354 // translation units. Those targets must be serialized too so imported
355 // type-bound calls can recover full dummy-argument ABI metadata such as
356 // OPTIONAL slots.
357 let mut proc_export_names: BTreeSet<String> = interface_specifics;
358 for (name, sym) in &syms {
359 if matches!(sym.kind, SymbolKind::Function | SymbolKind::Subroutine)
360 && is_public(sym, scope)
361 {
362 proc_export_names.insert(name.to_lowercase());
363 }
364 }
365 for (name, _sym) in syms
366 .iter()
367 .filter(|(_, sym)| matches!(sym.kind, SymbolKind::DerivedType))
368 {
369 if let Some(layout) = type_layouts.get(name) {
370 for bp in &layout.bound_procs {
371 proc_export_names.insert(bp.abi_name.to_lowercase());
372 }
373 }
374 }
375 let mut procs: Vec<_> = scope
376 .symbols
377 .iter()
378 .filter(|(name, sym)| {
379 matches!(sym.kind, SymbolKind::Function | SymbolKind::Subroutine)
380 && proc_export_names.contains(&name.to_lowercase())
381 })
382 .collect();
383 procs.sort_by_key(|(k, _)| k.to_lowercase());
384 for (name, sym) in &procs {
385 emit_procedure(
386 &mut out,
387 name,
388 sym,
389 st,
390 mod_scope_id,
391 ir_module,
392 descriptor_params,
393 char_len_star_params,
394 );
395 }
396
397 // ---- Types ----
398 // Include all derived types, even private ones — submodules need access
399 // to their parent module's private types per F2008 12.2.3.2.
400 let mut type_exports: BTreeSet<String> = BTreeSet::new();
401 for (name, sym) in &syms {
402 if matches!(sym.kind, SymbolKind::DerivedType) {
403 collect_exported_type_closure(&mut type_exports, name, type_layouts);
404 }
405 collect_exported_type_info_closure(&mut type_exports, sym.type_info.as_ref(), type_layouts);
406 }
407 for (name, sym) in scope.symbols.iter() {
408 if matches!(sym.kind, SymbolKind::DerivedType) {
409 collect_exported_type_closure(&mut type_exports, name, type_layouts);
410 }
411 }
412 for (_name, sym) in &procs {
413 collect_exported_type_info_closure(&mut type_exports, sym.type_info.as_ref(), type_layouts);
414 if let Some(pscope) = st
415 .scopes
416 .iter()
417 .find(|s| {
418 s.parent == Some(mod_scope_id)
419 && match &s.kind {
420 ScopeKind::Function(n) | ScopeKind::Subroutine(n) => {
421 n.eq_ignore_ascii_case(&sym.name)
422 }
423 _ => false,
424 }
425 })
426 .or_else(|| {
427 st.scopes.iter().find(|s| {
428 let matches_name = match &s.kind {
429 ScopeKind::Function(n) | ScopeKind::Subroutine(n) => {
430 n.eq_ignore_ascii_case(&sym.name)
431 }
432 _ => false,
433 };
434 if !matches_name {
435 return false;
436 }
437 let Some(parent_id) = s.parent else {
438 return false;
439 };
440 let parent = st.scope(parent_id);
441 matches!(parent.kind, ScopeKind::Interface)
442 && parent.parent == Some(mod_scope_id)
443 })
444 })
445 {
446 for arg_name in &pscope.arg_order {
447 if let Some(arg_sym) = pscope.symbols.get(&arg_name.to_lowercase()) {
448 collect_exported_type_info_closure(
449 &mut type_exports,
450 arg_sym.type_info.as_ref(),
451 type_layouts,
452 );
453 }
454 }
455 }
456 }
457 for key in &type_exports {
458 if let Some(layout) = type_layouts.get(key) {
459 emit_type(&mut out, &layout.name, type_layouts);
460 }
461 }
462
463 // ---- Interfaces ----
464 // Per F2018 §11.2.3, submodules see their parent module's PRIVATE
465 // generic interfaces. Emit every NamedInterface (and constructor
466 // interfaces represented as DerivedType with non-empty arg_names),
467 // tagging private ones with a `private` marker. Importing scopes
468 // that use the module without submodule access filter the private
469 // entries out via `Symbol::attrs.access == Private` (see
470 // SymbolTable::lookup_in_guarded). Without this, a submodule that
471 // dispatches a private parent generic emits a bare `bl _<name>`,
472 // since the loader-installed scope had no NamedInterface with that
473 // name to resolve against.
474 let ifaces: Vec<_> = all_syms
475 .iter()
476 .filter(|(_, sym)| {
477 matches!(sym.kind, SymbolKind::NamedInterface)
478 || (matches!(sym.kind, SymbolKind::DerivedType) && !sym.arg_names.is_empty())
479 })
480 .collect();
481 for (name, sym) in &ifaces {
482 emit_interface(&mut out, name, sym, scope);
483 }
484
485 out
486 }
487
488 fn is_public(sym: &Symbol, scope: &Scope) -> bool {
489 match sym.attrs.access {
490 Access::Private => false,
491 Access::Public => true,
492 Access::Default => !matches!(scope.default_access, Access::Private),
493 }
494 }
495
496 fn emit_variable(
497 out: &mut String,
498 mod_key: &str,
499 name: &str,
500 sym: &Symbol,
501 globals: &HashMap<(String, String), ModuleGlobalInfo>,
502 ) {
503 let global_key = (mod_key.to_string(), name.to_lowercase());
504 let global_info = globals.get(&global_key);
505 let type_str = if matches!(sym.kind, SymbolKind::ProcedurePointer) {
506 sym.attrs
507 .procedure_iface
508 .as_ref()
509 .map(|iface| format!("type({})", iface))
510 .unwrap_or_else(|| "unknown".to_string())
511 } else if let (
512 Some(TypeInfo::Character { len: None, .. }),
513 Some(ModuleGlobalInfo {
514 char_kind: crate::ir::lower::CharKind::Fixed(n),
515 ..
516 }),
517 ) = (sym.type_info.as_ref(), global_info)
518 {
519 format!("character(len={})", n)
520 } else {
521 type_info_to_string(sym.type_info.as_ref())
522 };
523 write!(out, "@var {} : {}", name, type_str).unwrap();
524
525 let mut attrs = Vec::new();
526 if sym.attrs.allocatable {
527 attrs.push("allocatable");
528 }
529 if sym.attrs.save {
530 attrs.push("save");
531 }
532 if sym.attrs.pointer {
533 attrs.push("pointer");
534 }
535 if matches!(sym.kind, SymbolKind::ProcedurePointer) {
536 attrs.push("procptr");
537 }
538 if sym.attrs.target {
539 attrs.push("target");
540 }
541 if sym.attrs.access == Access::Private {
542 attrs.push("private");
543 }
544 if !attrs.is_empty() {
545 write!(out, ", {}", attrs.join(", ")).unwrap();
546 }
547
548 if let Some(info) = global_info {
549 write!(out, " @ir {}", info.symbol).unwrap();
550 if info.deferred_char {
551 write!(out, " @deferred_char").unwrap();
552 }
553 if !info.dims.is_empty() {
554 write!(out, " @dims").unwrap();
555 for (lo, ext) in &info.dims {
556 write!(out, " {}:{}", lo, ext).unwrap();
557 }
558 }
559 }
560 writeln!(out).unwrap();
561 }
562
563 fn emit_parameter(
564 out: &mut String,
565 mod_key: &str,
566 name: &str,
567 sym: &Symbol,
568 globals: &HashMap<(String, String), ModuleGlobalInfo>,
569 ) {
570 let global_key = (mod_key.to_string(), name.to_lowercase());
571 let global_info = globals.get(&global_key);
572 let type_str = if let (
573 Some(TypeInfo::Character { len: None, .. }),
574 Some(ModuleGlobalInfo {
575 char_kind: crate::ir::lower::CharKind::Fixed(n),
576 ..
577 }),
578 ) = (sym.type_info.as_ref(), global_info)
579 {
580 format!("character(len={})", n)
581 } else {
582 type_info_to_string(sym.type_info.as_ref())
583 };
584 let is_private = sym.attrs.access == Access::Private;
585 if let Some(cv) = sym.const_value {
586 // Place `, private` after the value so parse_var's
587 // rfind(" = ") inside type_str continues to work.
588 let suf = if is_private { ", private" } else { "" };
589 writeln!(out, "@param {} : {} = {}{}", name, type_str, cv, suf).unwrap();
590 } else if let Some(info) = global_info {
591 // For @ir-backed params, attach `, private` to the type so
592 // the parser sees it in attr_str rather than after @ir.
593 let type_with_attr = if is_private {
594 format!("{}, private", type_str)
595 } else {
596 type_str
597 };
598 writeln!(
599 out,
600 "@param {} : {} @ir {}",
601 name, type_with_attr, info.symbol
602 )
603 .unwrap();
604 } else {
605 let suf = if is_private { ", private" } else { "" };
606 writeln!(out, "@param {} : {}{}", name, type_str, suf).unwrap();
607 }
608 }
609
610 fn emit_procedure(
611 out: &mut String,
612 name: &str,
613 sym: &Symbol,
614 st: &SymbolTable,
615 mod_scope_id: ScopeId,
616 ir_module: &IrModule,
617 descriptor_params: &HashMap<String, Vec<bool>>,
618 _char_len_star_params: &HashMap<String, Vec<bool>>,
619 ) {
620 let is_func = matches!(sym.kind, SymbolKind::Function);
621 let kind_str = if is_func { "function" } else { "subroutine" };
622
623 if is_func {
624 let ret_str = type_info_to_string(sym.type_info.as_ref());
625 write!(out, "@function {} -> {}", sym.name, ret_str).unwrap();
626 if sym.attrs.allocatable {
627 write!(out, ", result_allocatable").unwrap();
628 }
629 if sym.attrs.pointer {
630 write!(out, ", result_pointer").unwrap();
631 }
632 if sym.attrs.result_rank > 0 {
633 write!(out, ", result_rank={}", sym.attrs.result_rank).unwrap();
634 }
635 // Sprint35-SMP Phase 2: emit the result variable's user-declared
636 // name when it differs from the function name (i.e. the source
637 // had a `result(X)` clause). Submodule bodies that reference the
638 // result by its declared name need this preserved across the
639 // .amod boundary so sema can register the right symbol.
640 let result_var_name: Option<String> = st
641 .scopes
642 .iter()
643 .find(|s| {
644 let matches_name = match &s.kind {
645 ScopeKind::Function(n) | ScopeKind::Subroutine(n) => {
646 n.eq_ignore_ascii_case(name)
647 }
648 _ => false,
649 };
650 if !matches_name {
651 return false;
652 }
653 let Some(parent_id) = s.parent else {
654 return false;
655 };
656 parent_id == mod_scope_id
657 || matches!(st.scope(parent_id).kind, ScopeKind::Interface)
658 && st.scope(parent_id).parent == Some(mod_scope_id)
659 })
660 .and_then(|pscope| {
661 let arg_set: std::collections::HashSet<String> =
662 pscope.arg_order.iter().map(|n| n.to_lowercase()).collect();
663 pscope
664 .symbols
665 .iter()
666 .find(|(key, sym)| {
667 !arg_set.contains(*key)
668 && matches!(sym.kind, SymbolKind::Variable | SymbolKind::Parameter)
669 })
670 .map(|(_, sym)| sym.name.clone())
671 });
672 if let Some(result_var_name) = result_var_name {
673 if !result_var_name.eq_ignore_ascii_case(name) {
674 write!(out, ", result_name={}", result_var_name).unwrap();
675 }
676 }
677 // Sprint35-SMP Phase 3: serialize the result variable's
678 // explicit-shape bounds so split-file submodule bodies (where
679 // the body's TU loads the parent module from .amod) can rebuild
680 // a same-shape ArraySpec at load time. Without this, the body's
681 // `res(i) = …` lowers against an AssumedShape result and the
682 // function prologue fails to allocate the runtime-shape buffer.
683 if !sym.attrs.allocatable && !sym.attrs.pointer && sym.attrs.result_rank > 0 {
684 let bounds = st
685 .scopes
686 .iter()
687 .find(|s| {
688 let matches_name = match &s.kind {
689 ScopeKind::Function(n) | ScopeKind::Subroutine(n) => {
690 n.eq_ignore_ascii_case(name)
691 }
692 _ => false,
693 };
694 if !matches_name {
695 return false;
696 }
697 let Some(parent_id) = s.parent else {
698 return false;
699 };
700 parent_id == mod_scope_id
701 || matches!(st.scope(parent_id).kind, ScopeKind::Interface)
702 && st.scope(parent_id).parent == Some(mod_scope_id)
703 })
704 .and_then(|pscope| {
705 let arg_set: std::collections::HashSet<String> =
706 pscope.arg_order.iter().map(|n| n.to_lowercase()).collect();
707 pscope
708 .symbols
709 .iter()
710 .find(|(key, sym)| {
711 !arg_set.contains(*key)
712 && matches!(sym.kind, SymbolKind::Variable | SymbolKind::Parameter)
713 })
714 .map(|(_, sym)| sym.attrs.array_spec.clone())
715 })
716 .and_then(|specs| stringify_array_bounds(&specs));
717 if let Some(s) = bounds {
718 write!(out, ", result_array_bounds=\"{}\"", s).unwrap();
719 }
720 }
721 } else {
722 write!(out, "@subroutine {}", sym.name).unwrap();
723 }
724 if sym.attrs.pure {
725 write!(out, ", pure").unwrap();
726 }
727 if sym.attrs.elemental {
728 write!(out, ", elemental").unwrap();
729 }
730 if sym.attrs.access == Access::Private {
731 write!(out, ", private").unwrap();
732 }
733 if let Some(binding_label) = &sym.attrs.binding_label {
734 write!(out, ", bind={}", binding_label).unwrap();
735 }
736 writeln!(out).unwrap();
737
738 let name_lc = name.to_lowercase();
739 let ir_func = ir_module.functions.iter().find(|f| {
740 f.name.eq_ignore_ascii_case(name)
741 || f.name.eq_ignore_ascii_case(&name_lc)
742 || f.name.to_lowercase().ends_with(&format!("_{}", name_lc))
743 });
744 let visible_ir_params: Vec<_> = ir_func
745 .map(|func| {
746 func.params
747 .iter()
748 .filter(|param| {
749 param.name != "_sret"
750 && !param.name.starts_with("__len_")
751 && !param.name.starts_with("__host_")
752 })
753 .collect::<Vec<_>>()
754 })
755 .unwrap_or_default();
756
757 // Walk into the procedure's scope for full arg info. Interface-declared
758 // procedures sit under an intermediate Interface scope rather than
759 // directly under the module, so check both shapes.
760 let proc_scope = st
761 .scopes
762 .iter()
763 .find(|s| {
764 s.parent == Some(mod_scope_id)
765 && match &s.kind {
766 ScopeKind::Function(n) | ScopeKind::Subroutine(n) => {
767 n.eq_ignore_ascii_case(name)
768 }
769 _ => false,
770 }
771 })
772 .or_else(|| {
773 st.scopes.iter().find(|s| {
774 let matches_name = match &s.kind {
775 ScopeKind::Function(n) | ScopeKind::Subroutine(n) => {
776 n.eq_ignore_ascii_case(name)
777 }
778 _ => false,
779 };
780 if !matches_name {
781 return false;
782 }
783 let Some(parent_id) = s.parent else {
784 return false;
785 };
786 let parent = st.scope(parent_id);
787 matches!(parent.kind, ScopeKind::Interface) && parent.parent == Some(mod_scope_id)
788 })
789 });
790
791 let is_bind_c = sym.attrs.binding_label.is_some();
792 let declared_descriptor_params = descriptor_params.get(&name.to_lowercase());
793
794 // Compute hidden char-length count from the scope's arg types.
795 let mut hidden_count = 0usize;
796 if let Some(pscope) = proc_scope {
797 for arg_name in &pscope.arg_order {
798 if let Some(arg_sym) = pscope.symbols.get(&arg_name.to_lowercase()) {
799 if matches!(
800 arg_sym.type_info,
801 Some(TypeInfo::Character { len: None, .. })
802 ) && !arg_sym.attrs.allocatable
803 && !is_bind_c
804 {
805 hidden_count += 1;
806 }
807 }
808 }
809 }
810
811 // @abi line for the procedure.
812 writeln!(out, " @abi cc=aapcs64 hidden_char_lens={}", hidden_count).unwrap();
813
814 let mut reg_idx = 0usize;
815 if let Some(pscope) = proc_scope {
816 for (arg_idx, arg_name) in pscope.arg_order.iter().enumerate() {
817 if let Some(arg_sym) = pscope.symbols.get(&arg_name.to_lowercase()) {
818 let type_str = type_info_to_string(arg_sym.type_info.as_ref());
819 write!(out, " @arg {} : {}", arg_name, type_str).unwrap();
820 let mut arg_attrs = Vec::new();
821 if let Some(intent) = &arg_sym.attrs.intent {
822 arg_attrs.push(match intent {
823 Intent::In => "intent(in)",
824 Intent::Out => "intent(out)",
825 Intent::InOut => "intent(inout)",
826 });
827 }
828 if arg_sym.attrs.optional {
829 arg_attrs.push("optional");
830 }
831 if arg_sym.attrs.value {
832 arg_attrs.push("value");
833 }
834 let is_descriptor_arg = ir_func
835 .and_then(|_| visible_ir_params.get(arg_idx))
836 .map(|param| {
837 matches!(
838 &param.ty,
839 crate::ir::types::IrType::Ptr(inner)
840 if matches!(
841 inner.as_ref(),
842 crate::ir::types::IrType::Array(elem, 384)
843 if matches!(
844 elem.as_ref(),
845 crate::ir::types::IrType::Int(
846 crate::ir::types::IntWidth::I8
847 )
848 )
849 )
850 )
851 })
852 .unwrap_or(false)
853 || declared_descriptor_params
854 .and_then(|flags| flags.get(arg_idx))
855 .copied()
856 .unwrap_or(false)
857 || matches!(
858 arg_sym.type_info,
859 Some(TypeInfo::Class(_)) | Some(TypeInfo::ClassStar)
860 );
861 if is_descriptor_arg {
862 arg_attrs.push("descriptor");
863 }
864 if arg_sym.attrs.allocatable {
865 arg_attrs.push("allocatable");
866 }
867 if arg_sym.attrs.pointer {
868 arg_attrs.push("pointer");
869 }
870 // F2018 §15.4.3.6: a `procedure(iface) :: name` dummy is
871 // a procedure formal. The producer side stores this as a
872 // Variable with EXTERNAL set; without preserving the flag
873 // the consumer-side dispatch can't tell it apart from a
874 // data dummy and rejects valid procedure-actual binding
875 // (e.g. passing `do_not_select` into LAPACK `gees`).
876 if arg_sym.attrs.external {
877 arg_attrs.push("external");
878 }
879 let proc_iface_attr = arg_sym
880 .attrs
881 .procedure_iface
882 .as_ref()
883 .map(|n| format!("procedure({})", n));
884 if let Some(s) = proc_iface_attr.as_ref() {
885 arg_attrs.push(s.as_str());
886 }
887 // Sprint35-SMP Phase 1: emit the dummy's rank so SMP-body
888 // synthesis on the consumer side can rebuild a same-rank
889 // array_spec without re-walking the AST decls (which only
890 // exist on the producer side at .amod write time).
891 let rank_attr = format!("rank={}", arg_sym.attrs.array_spec.len());
892 if !arg_sym.attrs.array_spec.is_empty() {
893 arg_attrs.push(rank_attr.as_str());
894 }
895 if !arg_attrs.is_empty() {
896 write!(out, ", {}", arg_attrs.join(", ")).unwrap();
897 }
898 writeln!(out).unwrap();
899 // @abi per arg — ARM64 AAPCS64: first 8 int/ptr args in x0-x7.
900 let reg = if reg_idx < 8 {
901 format!("x{}", reg_idx)
902 } else {
903 format!("stack+{}", (reg_idx - 8) * 8)
904 };
905 writeln!(out, " @abi pass={} width=8", reg).unwrap();
906 reg_idx += 1;
907 } else {
908 writeln!(out, " @arg {}", arg_name).unwrap();
909 reg_idx += 1;
910 }
911 }
912 } else {
913 // Fallback: use arg_names from the symbol (no type info).
914 for arg_name in &sym.arg_names {
915 writeln!(out, " @arg {}", arg_name).unwrap();
916 reg_idx += 1;
917 }
918 }
919
920 // Hidden character-length args — infer from the scope's arg types.
921 // Any arg with TypeInfo::Character { len: None } that isn't
922 // allocatable is an assumed-length (len=*) dummy that gets a
923 // hidden i64 length parameter appended after the normal args.
924 if let Some(pscope) = proc_scope {
925 for arg_name in &pscope.arg_order {
926 if let Some(arg_sym) = pscope.symbols.get(&arg_name.to_lowercase()) {
927 let is_assumed_len = matches!(
928 arg_sym.type_info,
929 Some(TypeInfo::Character { len: None, .. })
930 ) && !arg_sym.attrs.allocatable
931 && !is_bind_c;
932 if is_assumed_len {
933 let reg = if reg_idx < 8 {
934 format!("x{}", reg_idx)
935 } else {
936 format!("stack+{}", (reg_idx - 8) * 8)
937 };
938 writeln!(out, " @arg {}@len : integer(8)", arg_name).unwrap();
939 writeln!(out, " @abi pass={} width=8 hidden", reg).unwrap();
940 reg_idx += 1;
941 }
942 }
943 }
944 }
945
946 // @hint line.
947 if let Some(func) = ir_func {
948 let mut hints = Vec::new();
949 if is_leaf(func) {
950 hints.push("leaf".to_string());
951 }
952 if !touches_globals(func) {
953 hints.push("no_globals".to_string());
954 }
955 let cost: usize = func.blocks.iter().map(|b| b.insts.len()).sum();
956 hints.push(format!("cost={}", cost));
957 writeln!(out, " @hint {}", hints.join(" ")).unwrap();
958 }
959
960 writeln!(out, "@end {}", kind_str).unwrap();
961 writeln!(out).unwrap();
962 }
963
964 fn is_leaf(func: &Function) -> bool {
965 for block in &func.blocks {
966 for inst in &block.insts {
967 match &inst.kind {
968 InstKind::Call(..) | InstKind::RuntimeCall(..) => return false,
969 _ => {}
970 }
971 }
972 }
973 true
974 }
975
976 fn touches_globals(func: &Function) -> bool {
977 for block in &func.blocks {
978 for inst in &block.insts {
979 match &inst.kind {
980 InstKind::GlobalAddr(_) => return true,
981 InstKind::Call(FuncRef::External(_), _) => return true,
982 _ => {}
983 }
984 }
985 }
986 false
987 }
988
989 fn emit_type(out: &mut String, name: &str, type_layouts: &TypeLayoutRegistry) {
990 writeln!(out, "@type {}", name).unwrap();
991 if let Some(layout) = type_layouts.get(&name.to_lowercase()) {
992 writeln!(out, " @layout size={} align={}", layout.size, layout.align).unwrap();
993 if let Some(ref parent) = layout.parent {
994 writeln!(out, " @extends {}", parent).unwrap();
995 }
996 if layout.is_abstract {
997 writeln!(out, " @abstract").unwrap();
998 }
999 for field in &layout.fields {
1000 let ft = type_info_to_string(Some(&field.type_info));
1001 let dims = if field.dims.is_empty() {
1002 String::new()
1003 } else {
1004 let rendered = field
1005 .dims
1006 .iter()
1007 .map(|(lower, extent)| {
1008 let upper = lower + extent - 1;
1009 format!("{}:{}", lower, upper)
1010 })
1011 .collect::<Vec<_>>()
1012 .join(",");
1013 format!(" @dims {}", rendered)
1014 };
1015 let mut attrs = String::new();
1016 if field.allocatable {
1017 attrs.push_str(" @allocatable");
1018 }
1019 if field.pointer {
1020 attrs.push_str(" @pointer");
1021 }
1022 if field.target {
1023 attrs.push_str(" @target");
1024 }
1025 if field.declared_array && field.dims.is_empty() {
1026 attrs.push_str(" @declared_array");
1027 }
1028 if let Some(default_init) = &field.default_init {
1029 attrs.push_str(&render_field_default_init(default_init));
1030 }
1031 writeln!(
1032 out,
1033 " @field {} : {} @offset {} @size {}{}{}",
1034 field.name, ft, field.offset, field.size, dims, attrs
1035 )
1036 .unwrap();
1037 }
1038 for bp in &layout.bound_procs {
1039 let abi_suffix = if bp.abi_name != bp.method_name.to_lowercase() {
1040 format!(" @abi {}", bp.abi_name)
1041 } else {
1042 String::new()
1043 };
1044 if bp.method_name == bp.target_name {
1045 if bp.nopass {
1046 writeln!(out, " @binds {}, nopass{}", bp.method_name, abi_suffix).unwrap();
1047 } else {
1048 writeln!(out, " @binds {}{}", bp.method_name, abi_suffix).unwrap();
1049 }
1050 } else {
1051 if bp.nopass {
1052 writeln!(
1053 out,
1054 " @binds {} => {}, nopass{}",
1055 bp.method_name, bp.target_name, abi_suffix
1056 )
1057 .unwrap();
1058 } else {
1059 writeln!(
1060 out,
1061 " @binds {} => {}{}",
1062 bp.method_name, bp.target_name, abi_suffix
1063 )
1064 .unwrap();
1065 }
1066 }
1067 }
1068
1069 fn render_field_default_init(init: &crate::sema::type_layout::FieldDefaultInit) -> String {
1070 match init {
1071 crate::sema::type_layout::FieldDefaultInit::Character(value) => {
1072 format!(" @init=charhex:{}", hex_encode_bytes(value.as_bytes()))
1073 }
1074 crate::sema::type_layout::FieldDefaultInit::Integer(value) => {
1075 format!(" @init=int:{}", value)
1076 }
1077 crate::sema::type_layout::FieldDefaultInit::Logical(value) => {
1078 format!(" @init=logical:{}", if *value { "true" } else { "false" })
1079 }
1080 crate::sema::type_layout::FieldDefaultInit::Derived(_) => {
1081 let encoded = encode_nested_field_default_init(init);
1082 format!(" @init=exprhex:{}", hex_encode_bytes(encoded.as_bytes()))
1083 }
1084 crate::sema::type_layout::FieldDefaultInit::ProcedurePointer(target) => {
1085 format!(" @init=procptr:{}", target)
1086 }
1087 }
1088 }
1089 for fp in &layout.final_procs {
1090 writeln!(out, " @final {}", fp).unwrap();
1091 }
1092 if let Some(owner_module) = &layout.owner_module {
1093 writeln!(out, " @owner {}", owner_module).unwrap();
1094 }
1095 writeln!(out, " @tag {}", layout.type_tag).unwrap();
1096 }
1097 writeln!(out, "@end type").unwrap();
1098 writeln!(out).unwrap();
1099 }
1100
1101 fn collect_exported_type_info_closure(
1102 out: &mut BTreeSet<String>,
1103 info: Option<&TypeInfo>,
1104 type_layouts: &TypeLayoutRegistry,
1105 ) {
1106 match info {
1107 Some(TypeInfo::Derived(name)) | Some(TypeInfo::Class(name)) => {
1108 collect_exported_type_closure(out, name, type_layouts);
1109 }
1110 _ => {}
1111 }
1112 }
1113
1114 fn collect_exported_type_closure(
1115 out: &mut BTreeSet<String>,
1116 type_name: &str,
1117 type_layouts: &TypeLayoutRegistry,
1118 ) {
1119 let key = type_name.to_lowercase();
1120 if !out.insert(key.clone()) {
1121 return;
1122 }
1123 let Some(layout) = type_layouts.get(&key) else {
1124 return;
1125 };
1126 if let Some(parent) = &layout.parent {
1127 collect_exported_type_closure(out, parent, type_layouts);
1128 }
1129 for field in &layout.fields {
1130 collect_exported_type_info_closure(out, Some(&field.type_info), type_layouts);
1131 }
1132 }
1133
1134 fn emit_interface(out: &mut String, name: &str, sym: &Symbol, scope: &Scope) {
1135 let effective_private = match sym.attrs.access {
1136 Access::Private => true,
1137 Access::Public => false,
1138 Access::Default => matches!(scope.default_access, Access::Private),
1139 };
1140 let suf = if effective_private { ", private" } else { "" };
1141 writeln!(out, "@interface {}{}", name, suf).unwrap();
1142 let mut specifics = sym.arg_names.clone(); // arg_names repurposed for specific list
1143 specifics.sort();
1144 for s in &specifics {
1145 writeln!(out, " @specific {}", s).unwrap();
1146 }
1147 writeln!(out, "@end interface").unwrap();
1148 writeln!(out).unwrap();
1149 }
1150
1151 // =====================================================================
1152 // Helpers
1153 // =====================================================================
1154
1155 fn type_info_to_string(info: Option<&TypeInfo>) -> String {
1156 match info {
1157 Some(TypeInfo::Integer { kind }) => match kind {
1158 Some(k) => format!("integer({})", k),
1159 None => "integer".to_string(),
1160 },
1161 Some(TypeInfo::Real { kind }) => match kind {
1162 Some(k) => format!("real({})", k),
1163 None => "real".to_string(),
1164 },
1165 Some(TypeInfo::DoublePrecision) => "double precision".to_string(),
1166 Some(TypeInfo::Complex { kind }) => match kind {
1167 Some(k) => format!("complex({})", k),
1168 None => "complex".to_string(),
1169 },
1170 Some(TypeInfo::Logical { kind }) => match kind {
1171 Some(k) => format!("logical({})", k),
1172 None => "logical".to_string(),
1173 },
1174 Some(TypeInfo::Character { len, kind: _ }) => match len {
1175 Some(n) => format!("character(len={})", n),
1176 None => "character(len=:)".to_string(),
1177 },
1178 Some(TypeInfo::Derived(name)) => format!("type({})", name),
1179 Some(TypeInfo::Class(name)) => format!("class({})", name),
1180 Some(TypeInfo::ClassStar) => "class(*)".to_string(),
1181 Some(TypeInfo::TypeStar) => "type(*)".to_string(),
1182 None => "unknown".to_string(),
1183 }
1184 }
1185
1186 fn fnv1a_hex(content: &str) -> String {
1187 // FNV-1a 64-bit hash for source content fingerprinting.
1188 let mut hash: u64 = 0xcbf29ce484222325;
1189 for byte in content.bytes() {
1190 hash ^= byte as u64;
1191 hash = hash.wrapping_mul(0x100000001b3);
1192 }
1193 format!("{:016x}", hash)
1194 }
1195
1196 fn compile_timestamp() -> String {
1197 // ISO-8601 timestamp. For deterministic builds, this could
1198 // be overridden by an environment variable.
1199 // TODO: use actual system time.
1200 "2026-01-01T00:00:00Z".to_string()
1201 }
1202
1203 // =====================================================================
1204 // Reader
1205 // =====================================================================
1206
1207 /// A procedure argument parsed from an .amod file.
1208 #[derive(Debug, Clone)]
1209 pub struct AmodArg {
1210 pub name: String,
1211 pub type_info: Option<TypeInfo>,
1212 pub intent: Option<Intent>,
1213 pub optional: bool,
1214 pub value: bool,
1215 pub descriptor: bool,
1216 pub allocatable: bool,
1217 pub pointer: bool,
1218 pub hidden: bool,
1219 /// True for `procedure(iface) :: name` dummies. The producer side
1220 /// stores these as Variable + EXTERNAL; the consumer-side dispatch
1221 /// uses this flag to identify procedure formals and skip the data
1222 /// type-matching that would otherwise reject procedure-actual
1223 /// binding (the .amod writer normalizes the type to the interface's
1224 /// return type).
1225 pub external: bool,
1226 /// For procedure dummy args (`procedure(iface) :: name`), the
1227 /// interface name. Without this the consumer side can't resolve
1228 /// the dummy to its abstract interface and falls back to emitting
1229 /// the dummy name as an external symbol — see the SGGES3 / selctg
1230 /// failure in stdlib_lapack_eigv_gen.
1231 pub procedure_iface: Option<String>,
1232 /// Sprint35-SMP Phase 1: rank of the dummy (number of array dimensions);
1233 /// 0 for scalar. When non-zero the loader reconstructs a SymbolAttrs
1234 /// `array_spec` of this rank, deriving each dim's kind from the
1235 /// `descriptor` / `allocatable` / `pointer` flags. Bound expressions
1236 /// (Explicit lower:upper) are not preserved across .amod boundaries —
1237 /// SMP-body synthesis only needs the shape kind and rank for Phase 2.
1238 pub rank: u8,
1239 }
1240
1241 /// A procedure parsed from an .amod file.
1242 #[derive(Debug, Clone)]
1243 pub struct AmodProc {
1244 pub name: String,
1245 pub kind: SymbolKind,
1246 pub return_type: Option<TypeInfo>,
1247 pub result_allocatable: bool,
1248 pub result_pointer: bool,
1249 pub result_rank: u8,
1250 /// Sprint35-SMP Phase 2: the result variable's user-declared name
1251 /// (from `result(X)` clause). None when the result name matches
1252 /// the function name. The submodule body lowering needs this to
1253 /// resolve `X = ...` assignments inside an SMP body when the body
1254 /// references the result by its declared name rather than by the
1255 /// function name.
1256 pub result_name: Option<String>,
1257 /// Stringified explicit-shape bounds for the result variable.
1258 /// `(b1; b2; ...)` per dim, where each is `lower:upper` or just
1259 /// `upper`. Preserves runtime-shape result sizing across split-file
1260 /// submodule compilation: SMP body lowering needs `Explicit { upper:
1261 /// Name(dummy) }` to allocate the result in the prologue. None for
1262 /// scalar / allocatable / pointer / non-runtime-shape results.
1263 pub result_array_bounds: Option<String>,
1264 pub pure: bool,
1265 pub elemental: bool,
1266 pub access: Access,
1267 pub binding_label: Option<String>,
1268 pub args: Vec<AmodArg>,
1269 }
1270
1271 /// A variable or parameter parsed from an .amod file.
1272 #[derive(Debug, Clone)]
1273 pub struct AmodVar {
1274 pub name: String,
1275 pub type_info: Option<TypeInfo>,
1276 pub is_parameter: bool,
1277 pub allocatable: bool,
1278 pub save: bool,
1279 pub pointer: bool,
1280 pub proc_pointer: bool,
1281 pub target: bool,
1282 pub ir_symbol: Option<String>,
1283 pub deferred_char: bool,
1284 pub dims: Vec<(i64, i64)>,
1285 pub const_value: Option<i64>,
1286 /// Access level. F2008 §11.2.3 requires private parent symbols to
1287 /// be visible in submodules, so the writer emits private entries
1288 /// with a `private` attribute and the loader honors them via
1289 /// host association without exposing them to ordinary USE.
1290 pub access: Access,
1291 }
1292
1293 /// A generic named interface parsed from an .amod file. Each entry
1294 /// maps the interface name (e.g. `add`) to the ordered list of
1295 /// specific procedure names it dispatches to. Used by importing
1296 /// compilation units to reconstruct a `NamedInterface` symbol so
1297 /// generic resolution works across .amod boundaries.
1298 #[derive(Debug, Clone)]
1299 pub struct AmodInterface {
1300 pub name: String,
1301 pub specifics: Vec<String>,
1302 pub access: Access,
1303 }
1304
1305 /// One renamed USE association from this module's source: `use M, only: A => B`
1306 /// becomes `UseRename { local: "a", original: "b", source_module: "m" }`. The
1307 /// rename is recorded so downstream consumers (esp. submodules) can resolve
1308 /// the local name at .amod-load time. Without this the kind constant
1309 /// `block_kind => int64` is irrecoverable from a binary-only build.
1310 #[derive(Debug, Clone)]
1311 pub struct UseRename {
1312 pub local: String,
1313 pub original: String,
1314 pub source_module: String,
1315 }
1316
1317 /// Complete module interface parsed from an .amod file.
1318 #[derive(Debug, Clone)]
1319 pub struct ModuleInterface {
1320 pub module_name: String,
1321 pub dependencies: Vec<String>,
1322 pub renames: Vec<UseRename>,
1323 pub variables: Vec<AmodVar>,
1324 pub procedures: Vec<AmodProc>,
1325 pub types: Vec<crate::sema::type_layout::TypeLayout>,
1326 pub interfaces: Vec<AmodInterface>,
1327 pub checksum: Option<String>,
1328 }
1329
1330 /// Read a `.amod` file and return the parsed module interface.
1331 pub fn read_amod(path: &Path) -> Result<ModuleInterface, String> {
1332 let content = std::fs::read_to_string(path)
1333 .map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
1334 parse_amod(&content, path)
1335 }
1336
1337 fn parse_amod(content: &str, path: &Path) -> Result<ModuleInterface, String> {
1338 let mut lines = content.lines().peekable();
1339
1340 // Header: #!amod N
1341 let magic = lines.next().ok_or("empty .amod file")?;
1342 if !magic.starts_with("#!amod ") {
1343 return Err(format!(
1344 "{}: not an .amod file (missing #!amod magic)",
1345 path.display()
1346 ));
1347 }
1348 let version: u32 = magic[7..]
1349 .trim()
1350 .parse()
1351 .map_err(|_| format!("{}: invalid .amod version", path.display()))?;
1352 if version > 2 {
1353 eprintln!("warning: {}: .amod version {} is newer than this compiler supports; some information may be ignored", path.display(), version);
1354 }
1355
1356 let mut module_name = String::new();
1357 let mut checksum = None;
1358
1359 // Parse # key: value header lines.
1360 while let Some(line) = lines.peek() {
1361 if let Some(rest) = line.strip_prefix("# ") {
1362 if let Some((key, val)) = rest.split_once(": ") {
1363 match key {
1364 "module" => module_name = val.trim().to_string(),
1365 "checksum" => checksum = Some(val.trim().to_string()),
1366 _ => {} // skip other metadata
1367 }
1368 }
1369 lines.next();
1370 } else if line.is_empty() {
1371 lines.next();
1372 } else {
1373 break;
1374 }
1375 }
1376
1377 if module_name.is_empty() {
1378 return Err(format!("{}: missing # module: header", path.display()));
1379 }
1380
1381 let mut dependencies = Vec::new();
1382 let mut renames: Vec<UseRename> = Vec::new();
1383 let mut variables = Vec::new();
1384 let mut procedures = Vec::new();
1385 let mut types = Vec::new();
1386 let mut interfaces = Vec::new();
1387
1388 while let Some(line) = lines.next() {
1389 let trimmed = line.trim();
1390 if trimmed.is_empty() || trimmed.starts_with('#') {
1391 continue;
1392 }
1393
1394 if let Some(dep) = trimmed.strip_prefix("@uses ") {
1395 dependencies.push(dep.trim().to_string());
1396 } else if let Some(rest) = trimmed.strip_prefix("@use_rename ") {
1397 // `@use_rename <local> = <original> from <module>`
1398 if let Some((lhs, mod_part)) = rest.split_once(" from ") {
1399 if let Some((local, original)) = lhs.split_once(" = ") {
1400 renames.push(UseRename {
1401 local: local.trim().to_string(),
1402 original: original.trim().to_string(),
1403 source_module: mod_part.trim().to_string(),
1404 });
1405 }
1406 }
1407 } else if trimmed.starts_with("@var ") {
1408 variables.push(parse_var(trimmed, false));
1409 } else if trimmed.starts_with("@param ") {
1410 variables.push(parse_var(&trimmed.replacen("@param", "@var", 1), true));
1411 } else if trimmed.starts_with("@function ") || trimmed.starts_with("@subroutine ") {
1412 let proc = parse_proc(trimmed, &mut lines);
1413 procedures.push(proc);
1414 } else if trimmed.starts_with("@type ") {
1415 let layout = parse_type(trimmed, &mut lines);
1416 types.push(layout);
1417 } else if let Some(name) = trimmed.strip_prefix("@interface ") {
1418 // Generic interface block: header is `@interface <name>[, private]`,
1419 // body lists `@specific <proc>` until `@end interface`.
1420 let header = name.trim();
1421 let (iface_name, access) = match header.split_once(", ") {
1422 Some((n, attr)) if attr.split(", ").any(|a| a == "private") => {
1423 (n.trim().to_string(), Access::Private)
1424 }
1425 _ => (header.to_string(), Access::Public),
1426 };
1427 let mut specifics = Vec::new();
1428 for iline in lines.by_ref() {
1429 let t = iline.trim();
1430 if t.starts_with("@end interface") {
1431 break;
1432 }
1433 if let Some(spec) = t.strip_prefix("@specific ") {
1434 specifics.push(spec.trim().to_string());
1435 }
1436 }
1437 interfaces.push(AmodInterface {
1438 name: iface_name,
1439 specifics,
1440 access,
1441 });
1442 }
1443 // Skip unrecognized directives (forward compatibility).
1444 }
1445
1446 Ok(ModuleInterface {
1447 module_name,
1448 dependencies,
1449 renames,
1450 variables,
1451 procedures,
1452 types,
1453 interfaces,
1454 checksum,
1455 })
1456 }
1457
1458 fn parse_var(line: &str, is_param: bool) -> AmodVar {
1459 // @var name : type[, attrs...] [@ir symbol] [@deferred_char] [@dims ...]
1460 let rest = line.strip_prefix("@var ").unwrap_or(line);
1461 let (name_type, ir_part) = if let Some(idx) = rest.find(" @ir ") {
1462 (&rest[..idx], Some(&rest[idx + 5..]))
1463 } else {
1464 (rest, None)
1465 };
1466
1467 let (name, type_and_attrs) = name_type
1468 .split_once(" : ")
1469 .unwrap_or((name_type, "unknown"));
1470 let name = name.trim().to_string();
1471
1472 // Split type from attrs on comma.
1473 let (type_str, attr_str) = if let Some(idx) = type_and_attrs.find(", ") {
1474 (&type_and_attrs[..idx], &type_and_attrs[idx + 2..])
1475 } else {
1476 (type_and_attrs, "")
1477 };
1478
1479 let mut const_value = None;
1480 // For @param with `= value`, strip the value suffix from the
1481 // type string before parsing the type.
1482 let clean_type_str = if is_param {
1483 if let Some(eq_idx) = type_str.rfind(" = ") {
1484 let val_str = type_str[eq_idx + 3..].trim();
1485 if let Ok(v) = val_str.parse::<i64>() {
1486 const_value = Some(v);
1487 }
1488 &type_str[..eq_idx]
1489 } else {
1490 type_str
1491 }
1492 } else {
1493 type_str
1494 };
1495
1496 let type_info = parse_type_info(clean_type_str.trim());
1497 let allocatable = attr_str.contains("allocatable");
1498 let save = attr_str.contains("save");
1499 let pointer = attr_str.contains("pointer");
1500 let proc_pointer = attr_str.contains("procptr");
1501 let target = attr_str.contains("target");
1502 let access = if attr_str.contains("private") {
1503 Access::Private
1504 } else {
1505 Access::Public
1506 };
1507
1508 let mut ir_symbol = None;
1509 let mut deferred_char = false;
1510 let mut dims = Vec::new();
1511
1512 if let Some(ir) = ir_part {
1513 let parts: Vec<&str> = ir.split_whitespace().collect();
1514 if !parts.is_empty() {
1515 ir_symbol = Some(parts[0].to_string());
1516 }
1517 let mut i = 1;
1518 while i < parts.len() {
1519 if parts[i] == "@deferred_char" {
1520 deferred_char = true;
1521 i += 1;
1522 } else if parts[i] == "@dims" {
1523 // Parse dimension pairs: @dims 1:5 1:10 ...
1524 i += 1;
1525 while i < parts.len() && parts[i].contains(':') && !parts[i].starts_with('@') {
1526 let pair = parts[i];
1527 if let Some((lo_s, ext_s)) = pair.split_once(':') {
1528 let lo = lo_s.parse::<i64>().unwrap_or(1);
1529 let ext = ext_s.parse::<i64>().unwrap_or(1);
1530 dims.push((lo, ext));
1531 }
1532 i += 1;
1533 }
1534 } else {
1535 i += 1;
1536 }
1537 }
1538 }
1539
1540 AmodVar {
1541 name,
1542 type_info,
1543 is_parameter: is_param,
1544 allocatable,
1545 save,
1546 pointer,
1547 proc_pointer,
1548 target,
1549 ir_symbol,
1550 deferred_char,
1551 dims,
1552 const_value,
1553 access,
1554 }
1555 }
1556
1557 /// Split a comma-separated attribute list while honoring paren depth and
1558 /// double-quoted strings. The naive `split(", ")` mangled values like
1559 /// `result_array_bounds="(max(n, 0))"` because the inner `, ` between
1560 /// `n` and `0` matched the separator and split the value across two
1561 /// chunks — losing the bounds and forcing the resolver to fall back to
1562 /// AssumedShape, which broke runtime-shape result allocation for
1563 /// abbreviated SMP bodies pulling specs out of .amod.
1564 fn split_attrs_top_level(attrs: &str) -> Vec<String> {
1565 let mut out = Vec::new();
1566 let mut depth: i32 = 0;
1567 let mut in_quote = false;
1568 let mut start = 0usize;
1569 let bytes = attrs.as_bytes();
1570 let mut i = 0usize;
1571 while i < bytes.len() {
1572 let ch = bytes[i] as char;
1573 match ch {
1574 '"' if !in_quote => in_quote = true,
1575 '"' if in_quote => in_quote = false,
1576 '(' | '[' if !in_quote => depth += 1,
1577 ')' | ']' if !in_quote => depth -= 1,
1578 ',' if !in_quote && depth == 0 => {
1579 let chunk = attrs[start..i].trim();
1580 if !chunk.is_empty() {
1581 out.push(chunk.to_string());
1582 }
1583 start = i + 1;
1584 }
1585 _ => {}
1586 }
1587 i += 1;
1588 }
1589 let tail = attrs[start..].trim();
1590 if !tail.is_empty() {
1591 out.push(tail.to_string());
1592 }
1593 out
1594 }
1595
1596 fn parse_proc(header: &str, lines: &mut std::iter::Peekable<std::str::Lines>) -> AmodProc {
1597 let is_func = header.starts_with("@function ");
1598 let rest = if is_func {
1599 header.strip_prefix("@function ").unwrap()
1600 } else {
1601 header.strip_prefix("@subroutine ").unwrap()
1602 };
1603
1604 // Parse: name [-> return_type][, pure][, elemental]
1605 let (name_and_ret, attrs_str) = {
1606 // Use depth-aware split so attribute values containing commas
1607 // inside parens (e.g. `result_array_bounds="(max(n, 0))"`)
1608 // don't split prematurely on the inner `, `.
1609 let mut depth: i32 = 0;
1610 let mut in_quote = false;
1611 let mut split_at: Option<usize> = None;
1612 let bytes = rest.as_bytes();
1613 let mut i = 0usize;
1614 while i < bytes.len() {
1615 let ch = bytes[i] as char;
1616 match ch {
1617 '"' if !in_quote => in_quote = true,
1618 '"' if in_quote => in_quote = false,
1619 '(' | '[' if !in_quote => depth += 1,
1620 ')' | ']' if !in_quote => depth -= 1,
1621 ',' if !in_quote && depth == 0 => {
1622 split_at = Some(i);
1623 break;
1624 }
1625 _ => {}
1626 }
1627 i += 1;
1628 }
1629 match split_at {
1630 Some(idx) => (rest[..idx].trim_end(), rest[idx + 1..].trim_start()),
1631 None => (rest.trim(), ""),
1632 }
1633 };
1634
1635 let (name, return_type) = if let Some(arrow_idx) = name_and_ret.find(" -> ") {
1636 let n = &name_and_ret[..arrow_idx];
1637 let rt = parse_type_info(name_and_ret[arrow_idx + 4..].trim());
1638 (n.trim().to_string(), rt)
1639 } else {
1640 (name_and_ret.trim().to_string(), None)
1641 };
1642
1643 let attr_chunks = split_attrs_top_level(attrs_str);
1644 let pure = attr_chunks.iter().any(|a| a == "pure");
1645 let elemental = attr_chunks.iter().any(|a| a == "elemental");
1646 let result_allocatable = attr_chunks.iter().any(|a| a == "result_allocatable");
1647 let result_pointer = attr_chunks.iter().any(|a| a == "result_pointer");
1648 let result_rank = attr_chunks
1649 .iter()
1650 .find_map(|attr| attr.strip_prefix("result_rank="))
1651 .and_then(|s| s.parse::<u8>().ok())
1652 .unwrap_or(0);
1653 // Sprint35-SMP Phase 2: optional `result_name=NAME` when the
1654 // source used a `result(NAME)` clause that differs from the
1655 // function name. Otherwise the result variable shares the name.
1656 let result_name = attr_chunks
1657 .iter()
1658 .find_map(|attr| attr.strip_prefix("result_name="))
1659 .map(|s| s.trim().to_string());
1660 let access = if attr_chunks.iter().any(|attr| attr == "private") {
1661 Access::Private
1662 } else {
1663 Access::Public
1664 };
1665 let binding_label = attr_chunks
1666 .iter()
1667 .find_map(|attr| attr.strip_prefix("bind=").map(|label| label.to_string()));
1668
1669 let kind = if is_func {
1670 SymbolKind::Function
1671 } else {
1672 SymbolKind::Subroutine
1673 };
1674
1675 let mut args = Vec::new();
1676
1677 // Parse body lines until @end.
1678 for line in lines.by_ref() {
1679 let trimmed = line.trim();
1680 if trimmed.starts_with("@end ") {
1681 break;
1682 }
1683 if trimmed.starts_with("@arg ") {
1684 args.push(parse_arg(trimmed));
1685 }
1686 // Skip @abi and @hint lines (informational; reader uses
1687 // them for optimization but not for correctness).
1688 }
1689
1690 let result_array_bounds = attr_chunks
1691 .iter()
1692 .find_map(|attr| attr.strip_prefix("result_array_bounds="))
1693 .map(|s| s.trim_matches('"').to_string());
1694
1695 AmodProc {
1696 name,
1697 kind,
1698 return_type,
1699 result_allocatable,
1700 result_pointer,
1701 result_rank,
1702 result_name,
1703 result_array_bounds,
1704 pure,
1705 elemental,
1706 access,
1707 binding_label,
1708 args,
1709 }
1710 }
1711
1712 fn parse_arg(line: &str) -> AmodArg {
1713 // @arg name : type[, intent(in/out/inout)][, optional][, value][, ...]
1714 let rest = line.strip_prefix("@arg ").unwrap_or(line);
1715
1716 let (name, type_and_attrs) = if let Some(idx) = rest.find(" : ") {
1717 (&rest[..idx], &rest[idx + 3..])
1718 } else {
1719 (rest.trim(), "unknown")
1720 };
1721
1722 let name = name.trim().to_string();
1723 let hidden = name.contains('@'); // e.g., label@len
1724
1725 let (type_str, attr_str) = if let Some(idx) = type_and_attrs.find(", ") {
1726 (&type_and_attrs[..idx], &type_and_attrs[idx + 2..])
1727 } else {
1728 (type_and_attrs, "")
1729 };
1730
1731 let type_info = parse_type_info(type_str.trim());
1732 let intent = if attr_str.contains("intent(in)") && !attr_str.contains("intent(inout)") {
1733 Some(Intent::In)
1734 } else if attr_str.contains("intent(out)") {
1735 Some(Intent::Out)
1736 } else if attr_str.contains("intent(inout)") {
1737 Some(Intent::InOut)
1738 } else {
1739 None
1740 };
1741
1742 let optional = attr_str.contains("optional");
1743 let value = attr_str.contains("value");
1744 let descriptor = attr_str.contains("descriptor");
1745 let allocatable = attr_str.contains("allocatable");
1746 let pointer = attr_str.contains("pointer");
1747 let external = attr_str
1748 .split(", ")
1749 .any(|tok| tok.trim().eq_ignore_ascii_case("external"));
1750 // Sprint35-SMP Phase 1: parse `rank=N` if present. Emitted only when
1751 // the dummy is array-shaped; absence means rank 0 (scalar).
1752 let rank = attr_str
1753 .split(", ")
1754 .find_map(|tok| tok.strip_prefix("rank="))
1755 .and_then(|s| s.trim().parse::<u8>().ok())
1756 .unwrap_or(0);
1757 let procedure_iface = attr_str.split(", ").find_map(|tok| {
1758 let t = tok.trim();
1759 let inner = t.strip_prefix("procedure(")?;
1760 inner.strip_suffix(')').map(|s| s.trim().to_string())
1761 });
1762
1763 AmodArg {
1764 name,
1765 type_info,
1766 intent,
1767 optional,
1768 value,
1769 descriptor,
1770 allocatable,
1771 pointer,
1772 hidden,
1773 external,
1774 procedure_iface,
1775 rank,
1776 }
1777 }
1778
1779 fn parse_type(
1780 header: &str,
1781 lines: &mut std::iter::Peekable<std::str::Lines>,
1782 ) -> crate::sema::type_layout::TypeLayout {
1783 use crate::sema::type_layout::*;
1784
1785 fn parse_field_default_init_token(token: &str) -> Option<FieldDefaultInit> {
1786 let payload = token.strip_prefix("@init=")?;
1787 if let Some(value) = payload.strip_prefix("int:") {
1788 return value.parse::<i128>().ok().map(FieldDefaultInit::Integer);
1789 }
1790 if let Some(value) = payload.strip_prefix("logical:") {
1791 return match value {
1792 "true" => Some(FieldDefaultInit::Logical(true)),
1793 "false" => Some(FieldDefaultInit::Logical(false)),
1794 _ => None,
1795 };
1796 }
1797 if let Some(value) = payload.strip_prefix("charhex:") {
1798 let decoded = String::from_utf8(hex_decode_bytes(value)?).ok()?;
1799 return Some(FieldDefaultInit::Character(decoded));
1800 }
1801 if let Some(value) = payload.strip_prefix("exprhex:") {
1802 let decoded = String::from_utf8(hex_decode_bytes(value)?).ok()?;
1803 return decode_nested_field_default_init(&decoded);
1804 }
1805 if let Some(value) = payload.strip_prefix("procptr:") {
1806 return Some(FieldDefaultInit::ProcedurePointer(value.to_string()));
1807 }
1808 None
1809 }
1810
1811 let name = header
1812 .strip_prefix("@type ")
1813 .unwrap_or("unknown")
1814 .trim()
1815 .to_string();
1816 let mut size = 0;
1817 let mut align = 1;
1818 let mut parent = None;
1819 let mut fields = Vec::new();
1820 let mut bound_procs = Vec::new();
1821 let mut final_procs = Vec::new();
1822 let mut owner_module = None;
1823 let mut type_tag = 0u64;
1824 let mut is_abstract = false;
1825
1826 for line in lines.by_ref() {
1827 let trimmed = line.trim();
1828 if trimmed.starts_with("@end type") {
1829 break;
1830 }
1831
1832 if let Some(rest) = trimmed.strip_prefix("@layout ") {
1833 for part in rest.split_whitespace() {
1834 if let Some(v) = part.strip_prefix("size=") {
1835 size = v.parse().unwrap_or(0);
1836 } else if let Some(v) = part.strip_prefix("align=") {
1837 align = v.parse().unwrap_or(1);
1838 }
1839 }
1840 } else if let Some(rest) = trimmed.strip_prefix("@extends ") {
1841 parent = Some(rest.trim().to_string());
1842 } else if let Some(rest) = trimmed.strip_prefix("@field ") {
1843 // @field name : type @offset N @size M [@allocatable] [@pointer] [@target]
1844 if let Some((name_type, offset_part)) = rest.split_once(" @offset ") {
1845 let (fname, ftype_str) = name_type
1846 .split_once(" : ")
1847 .unwrap_or((name_type, "unknown"));
1848 // Split off the size and any trailing attribute flags.
1849 let (offset_str, after_offset) = match offset_part.find(" @size ") {
1850 Some(idx) => (&offset_part[..idx], &offset_part[idx + 7..]),
1851 None => (offset_part, "0"),
1852 };
1853 let mut size_str = after_offset;
1854 let mut dims: Vec<(i64, i64)> = Vec::new();
1855 let mut flag_tail: &str = "";
1856 if let Some(idx) = after_offset.find(" @dims ") {
1857 size_str = &after_offset[..idx];
1858 let dims_part = &after_offset[idx + 7..];
1859 let (dims_str, tail) = if let Some(flag_idx) = dims_part.find(" @") {
1860 (&dims_part[..flag_idx], &dims_part[flag_idx..])
1861 } else {
1862 (dims_part, "")
1863 };
1864 for dim in dims_str
1865 .split(',')
1866 .map(str::trim)
1867 .filter(|dim| !dim.is_empty())
1868 {
1869 if let Some((lower_str, upper_str)) = dim.split_once(':') {
1870 let lower = lower_str.trim().parse().unwrap_or(1);
1871 let upper = upper_str.trim().parse().unwrap_or(lower - 1);
1872 dims.push((lower, (upper - lower + 1).max(0)));
1873 }
1874 }
1875 flag_tail = tail;
1876 } else if let Some(idx) = after_offset.find(" @") {
1877 size_str = &after_offset[..idx];
1878 flag_tail = &after_offset[idx..];
1879 }
1880 let mut allocatable = false;
1881 let mut pointer = false;
1882 let mut target = false;
1883 let mut declared_array = false;
1884 let mut default_init = None;
1885 for token in flag_tail.split_whitespace() {
1886 match token {
1887 "@allocatable" => allocatable = true,
1888 "@pointer" => pointer = true,
1889 "@target" => target = true,
1890 "@declared_array" => declared_array = true,
1891 _ => {
1892 if let Some(init) = parse_field_default_init_token(token) {
1893 default_init = Some(init);
1894 }
1895 }
1896 }
1897 }
1898 declared_array |= !dims.is_empty();
1899 let ftype = parse_type_info(ftype_str.trim());
1900 fields.push(FieldLayout {
1901 name: fname.trim().to_string(),
1902 offset: offset_str.trim().parse().unwrap_or(0),
1903 size: size_str.trim().parse().unwrap_or(0),
1904 dims,
1905 declared_array,
1906 type_info: ftype.unwrap_or(TypeInfo::Integer { kind: None }),
1907 allocatable,
1908 pointer,
1909 target,
1910 default_init,
1911 });
1912 }
1913 } else if let Some(rest) = trimmed.strip_prefix("@binds ") {
1914 let (clean, abi_name) = if let Some((bind_part, abi_part)) = rest.split_once(" @abi ") {
1915 (bind_part.trim().to_string(), abi_part.trim().to_lowercase())
1916 } else {
1917 (rest.trim().to_string(), String::new())
1918 };
1919 let nopass = clean.contains(", nopass");
1920 let clean = clean.replace(", nopass", "");
1921 let (method, target) = if let Some((m, t)) = clean.split_once(" => ") {
1922 (m.trim().to_string(), t.trim().to_string())
1923 } else {
1924 let m = clean.trim().to_string();
1925 (m.clone(), m)
1926 };
1927 let parsed_abi_name = if abi_name.is_empty() {
1928 method.to_lowercase()
1929 } else {
1930 abi_name
1931 };
1932 bound_procs.push(BoundProc {
1933 method_name: method,
1934 target_name: target,
1935 abi_name: parsed_abi_name,
1936 nopass,
1937 });
1938 } else if let Some(rest) = trimmed.strip_prefix("@final ") {
1939 final_procs.push(rest.trim().to_string());
1940 } else if let Some(rest) = trimmed.strip_prefix("@owner ") {
1941 owner_module = Some(rest.trim().to_string());
1942 } else if let Some(rest) = trimmed.strip_prefix("@tag ") {
1943 type_tag = rest.trim().parse().unwrap_or(0);
1944 } else if trimmed == "@abstract" {
1945 is_abstract = true;
1946 }
1947 }
1948
1949 TypeLayout {
1950 name,
1951 owner_module,
1952 size,
1953 align,
1954 fields,
1955 bound_procs,
1956 final_procs,
1957 type_tag,
1958 parent,
1959 is_abstract,
1960 }
1961 }
1962
1963 fn parse_type_info(s: &str) -> Option<TypeInfo> {
1964 let s = s.trim();
1965 if s == "unknown" || s.is_empty() {
1966 return None;
1967 }
1968 if s == "double precision" {
1969 return Some(TypeInfo::DoublePrecision);
1970 }
1971 if s == "class(*)" {
1972 return Some(TypeInfo::ClassStar);
1973 }
1974 if s == "type(*)" {
1975 return Some(TypeInfo::TypeStar);
1976 }
1977
1978 // integer[(K)]
1979 if s.starts_with("integer") {
1980 let kind = extract_kind(s);
1981 return Some(TypeInfo::Integer { kind });
1982 }
1983 if s.starts_with("real") {
1984 let kind = extract_kind(s);
1985 return Some(TypeInfo::Real { kind });
1986 }
1987 if s.starts_with("complex") {
1988 let kind = extract_kind(s);
1989 return Some(TypeInfo::Complex { kind });
1990 }
1991 if s.starts_with("logical") {
1992 let kind = extract_kind(s);
1993 return Some(TypeInfo::Logical { kind });
1994 }
1995 if s.starts_with("character") {
1996 // character(len=N) or character(len=:)
1997 if let Some(inner) = s
1998 .strip_prefix("character(len=")
1999 .and_then(|r| r.strip_suffix(')'))
2000 {
2001 if inner == ":" {
2002 return Some(TypeInfo::Character {
2003 len: None,
2004 kind: None,
2005 });
2006 } else if let Ok(n) = inner.parse::<i64>() {
2007 return Some(TypeInfo::Character {
2008 len: Some(n),
2009 kind: None,
2010 });
2011 }
2012 }
2013 return Some(TypeInfo::Character {
2014 len: None,
2015 kind: None,
2016 });
2017 }
2018 if let Some(inner) = s.strip_prefix("type(").and_then(|r| r.strip_suffix(')')) {
2019 return Some(TypeInfo::Derived(inner.to_string()));
2020 }
2021 if let Some(inner) = s.strip_prefix("class(").and_then(|r| r.strip_suffix(')')) {
2022 return Some(TypeInfo::Class(inner.to_string()));
2023 }
2024
2025 None
2026 }
2027
2028 fn extract_kind(s: &str) -> Option<u8> {
2029 if let Some(start) = s.find('(') {
2030 if let Some(end) = s.find(')') {
2031 return s[start + 1..end].parse().ok();
2032 }
2033 }
2034 None
2035 }
2036
2037 /// Convert a loaded ModuleInterface's variables into ModuleGlobalInfo
2038 /// entries for the lowering pass.
2039 pub fn extract_module_globals(
2040 iface: &ModuleInterface,
2041 ) -> HashMap<(String, String), crate::ir::lower::ModuleGlobalInfo> {
2042 let mod_key = iface.module_name.to_lowercase();
2043 let mut out = HashMap::new();
2044 for var in &iface.variables {
2045 // Private vars/params get included so submodules can resolve
2046 // host-associated references through the same globals map.
2047 // The `private` flag lets the "filtered out by USE ONLY"
2048 // diagnostic skip them — ordinary USE would never see them.
2049 if var.is_parameter && var.ir_symbol.is_none() {
2050 continue;
2051 } // PARAMETERs with folded values inline; others still need storage
2052 if let Some(ref ir_sym) = var.ir_symbol {
2053 let derived_type = match var.type_info.as_ref() {
2054 Some(TypeInfo::Derived(name))
2055 if !matches!(name.to_lowercase().as_str(), "c_ptr" | "c_funptr") =>
2056 {
2057 Some(name.clone())
2058 }
2059 _ => None,
2060 };
2061 let ir_ty = if var.proc_pointer {
2062 crate::ir::types::IrType::Ptr(Box::new(crate::ir::types::IrType::Int(
2063 crate::ir::types::IntWidth::I8,
2064 )))
2065 } else if var.pointer {
2066 match derived_type.as_deref() {
2067 Some("c_ptr") | Some("c_funptr") => {
2068 crate::ir::types::IrType::Int(crate::ir::types::IntWidth::I64)
2069 }
2070 Some(_) => crate::ir::types::IrType::Ptr(Box::new(
2071 crate::ir::types::IrType::Int(crate::ir::types::IntWidth::I8),
2072 )),
2073 None => type_info_to_ir_type(var.type_info.as_ref()),
2074 }
2075 } else if let Some(type_name) = &derived_type {
2076 if let Some(layout) = iface
2077 .types
2078 .iter()
2079 .find(|layout| layout.name.eq_ignore_ascii_case(type_name))
2080 {
2081 crate::ir::types::IrType::Array(
2082 Box::new(crate::ir::types::IrType::Int(
2083 crate::ir::types::IntWidth::I8,
2084 )),
2085 layout.size as u64,
2086 )
2087 } else {
2088 type_info_to_ir_type(var.type_info.as_ref())
2089 }
2090 } else if let Some(TypeInfo::Character { len: Some(n), .. }) = var.type_info.as_ref() {
2091 if *n <= 1 {
2092 crate::ir::types::IrType::Int(crate::ir::types::IntWidth::I8)
2093 } else {
2094 crate::ir::types::IrType::Array(
2095 Box::new(crate::ir::types::IrType::Int(
2096 crate::ir::types::IntWidth::I8,
2097 )),
2098 *n as u64,
2099 )
2100 }
2101 } else {
2102 type_info_to_ir_type(var.type_info.as_ref())
2103 };
2104 out.insert(
2105 (mod_key.clone(), var.name.to_lowercase()),
2106 crate::ir::lower::ModuleGlobalInfo {
2107 symbol: ir_sym.clone(),
2108 ty: ir_ty,
2109 dims: var.dims.clone(),
2110 allocatable: var.allocatable,
2111 is_pointer: var.pointer,
2112 deferred_char: var.deferred_char,
2113 derived_type,
2114 char_kind: match var.type_info.as_ref() {
2115 Some(crate::sema::symtab::TypeInfo::Character { len: Some(n), .. }) => {
2116 crate::ir::lower::CharKind::Fixed(*n)
2117 }
2118 _ if var.deferred_char => crate::ir::lower::CharKind::Deferred,
2119 _ => crate::ir::lower::CharKind::None,
2120 },
2121 external: true,
2122 private: var.access == Access::Private,
2123 },
2124 );
2125 }
2126 }
2127 out
2128 }
2129
2130 /// Extract char_len_star_params from a loaded ModuleInterface.
2131 /// For each procedure with character(len=*) args, produces a
2132 /// Vec<bool> (per-position, true = assumed-length character).
2133 pub fn extract_optional_params(iface: &ModuleInterface) -> HashMap<String, Vec<bool>> {
2134 let mut out = HashMap::new();
2135 for proc in &iface.procedures {
2136 let visible_args: Vec<&AmodArg> = proc.args.iter().filter(|a| !a.hidden).collect();
2137 let flags: Vec<bool> = visible_args.iter().map(|a| a.optional).collect();
2138 if !flags.is_empty() {
2139 let key = proc.name.to_lowercase();
2140 out.insert(key.clone(), flags.clone());
2141 out.insert(
2142 format!("afs_modproc_{}_{}", iface.module_name.to_lowercase(), key),
2143 flags,
2144 );
2145 }
2146 }
2147 out
2148 }
2149
2150 /// Extract char_len_star_params from a loaded ModuleInterface.
2151 /// For each procedure with character(len=*) args, produces a
2152 /// Vec<bool> (per-position, true = assumed-length character).
2153 pub fn extract_char_len_star_params(iface: &ModuleInterface) -> HashMap<String, Vec<bool>> {
2154 let mut out = HashMap::new();
2155 for proc in &iface.procedures {
2156 let is_bind_c = proc.binding_label.is_some();
2157 let visible_args: Vec<&AmodArg> = proc.args.iter().filter(|a| !a.hidden).collect();
2158 let flags: Vec<bool> = visible_args
2159 .iter()
2160 .map(|a| {
2161 matches!(a.type_info, Some(TypeInfo::Character { len: None, .. }))
2162 && !a.allocatable
2163 && !is_bind_c
2164 })
2165 .collect();
2166 if !flags.is_empty() {
2167 let key = proc.name.to_lowercase();
2168 out.insert(key.clone(), flags.clone());
2169 out.insert(
2170 format!("afs_modproc_{}_{}", iface.module_name.to_lowercase(), key),
2171 flags,
2172 );
2173 }
2174 }
2175 out
2176 }
2177
2178 /// Extract descriptor_params from a loaded ModuleInterface.
2179 /// For each procedure with descriptor-backed dummies, produces a
2180 /// Vec<bool> (per-position, true = pass the 384-byte descriptor).
2181 pub fn extract_descriptor_params(iface: &ModuleInterface) -> HashMap<String, Vec<bool>> {
2182 let mut out = HashMap::new();
2183 for proc in &iface.procedures {
2184 let visible_args: Vec<&AmodArg> = proc.args.iter().filter(|a| !a.hidden).collect();
2185 let flags: Vec<bool> = visible_args.iter().map(|a| a.descriptor).collect();
2186 if !flags.is_empty() {
2187 let key = proc.name.to_lowercase();
2188 out.insert(key.clone(), flags.clone());
2189 out.insert(
2190 format!("afs_modproc_{}_{}", iface.module_name.to_lowercase(), key),
2191 flags,
2192 );
2193 }
2194 }
2195 out
2196 }
2197
2198 fn type_info_to_ir_type(info: Option<&TypeInfo>) -> crate::ir::types::IrType {
2199 use crate::ir::types::{FloatWidth, IntWidth, IrType};
2200 match info {
2201 Some(TypeInfo::Derived(name)) => {
2202 let lower = name.to_lowercase();
2203 if lower == "c_ptr" || lower == "c_funptr" {
2204 IrType::Int(IntWidth::I64)
2205 } else {
2206 IrType::Ptr(Box::new(IrType::Int(IntWidth::I8)))
2207 }
2208 }
2209 Some(TypeInfo::Integer { kind }) => IrType::Int(match kind {
2210 Some(1) => IntWidth::I8,
2211 Some(2) => IntWidth::I16,
2212 Some(8) => IntWidth::I64,
2213 Some(16) => IntWidth::I128,
2214 _ => IntWidth::I32,
2215 }),
2216 Some(TypeInfo::Real { kind }) => IrType::Float(match kind {
2217 Some(8) => FloatWidth::F64,
2218 _ => FloatWidth::F32,
2219 }),
2220 Some(TypeInfo::DoublePrecision) => IrType::Float(FloatWidth::F64),
2221 Some(TypeInfo::Complex { kind }) => {
2222 let fw = match kind {
2223 Some(8) => FloatWidth::F64,
2224 _ => FloatWidth::F32,
2225 };
2226 IrType::Array(Box::new(IrType::Float(fw)), 2)
2227 }
2228 Some(TypeInfo::Logical { .. }) => IrType::Bool,
2229 Some(TypeInfo::Character { .. }) => IrType::Int(IntWidth::I8),
2230 _ => IrType::Int(IntWidth::I32),
2231 }
2232 }
2233
2234 #[cfg(test)]
2235 mod tests {
2236 use super::*;
2237
2238 #[test]
2239 fn round_trip_physics_amod() {
2240 let amod_text = r#"#!amod 2
2241 # module: physics
2242 # source: physics.f90
2243 # checksum: sha256:abc123
2244
2245 @uses iso_c_binding
2246
2247 @var call_count : integer, save @ir afs_mod_physics_call_count
2248
2249 @param gravity : real = 9
2250
2251 @function kinetic_energy -> real, pure
2252 @abi cc=aapcs64 hidden_char_lens=0
2253 @arg self : class(particle), intent(in)
2254 @abi pass=x0 width=8
2255 @arg vx : real, intent(in)
2256 @abi pass=x1 width=8
2257 @hint leaf cost=27
2258 @end function
2259
2260 @subroutine apply_force
2261 @abi cc=aapcs64 hidden_char_lens=0
2262 @arg p : type(particle), intent(inout)
2263 @abi pass=x0 width=8
2264 @arg dt : real, intent(in), optional
2265 @abi pass=x1 width=8
2266 @hint leaf cost=14
2267 @end subroutine
2268
2269 @type particle
2270 @layout size=12 align=4
2271 @field x : real @offset 0 @size 4
2272 @field y : real @offset 4 @size 4
2273 @field mass : real @offset 8 @size 4
2274 @binds kinetic_energy
2275 @tag 1
2276 @end type
2277 "#;
2278 let iface = parse_amod(amod_text, Path::new("test.amod")).unwrap();
2279 assert_eq!(iface.module_name, "physics");
2280 assert_eq!(iface.dependencies, vec!["iso_c_binding"]);
2281 assert_eq!(iface.variables.len(), 2); // call_count + gravity
2282 assert!(iface.variables.iter().any(|v| v.name == "call_count"
2283 && v.ir_symbol.as_deref() == Some("afs_mod_physics_call_count")));
2284 assert!(iface
2285 .variables
2286 .iter()
2287 .any(|v| v.name == "gravity" && v.is_parameter));
2288 assert!(iface.variables.iter().all(|v| !v.proc_pointer));
2289 assert_eq!(iface.procedures.len(), 2);
2290 let ke = iface
2291 .procedures
2292 .iter()
2293 .find(|p| p.name == "kinetic_energy")
2294 .unwrap();
2295 assert!(ke.pure);
2296 assert_eq!(ke.args.len(), 2);
2297 assert_eq!(ke.args[0].name, "self");
2298 assert!(matches!(ke.args[0].intent, Some(Intent::In)));
2299 let af = iface
2300 .procedures
2301 .iter()
2302 .find(|p| p.name == "apply_force")
2303 .unwrap();
2304 assert_eq!(af.args.len(), 2);
2305 assert!(af.args[1].optional);
2306 assert_eq!(iface.types.len(), 1);
2307 let pt = &iface.types[0];
2308 assert_eq!(pt.name, "particle");
2309 assert_eq!(pt.size, 12);
2310 assert_eq!(pt.fields.len(), 3);
2311 assert_eq!(pt.bound_procs.len(), 1);
2312 assert_eq!(pt.bound_procs[0].method_name, "kinetic_energy");
2313 }
2314
2315 #[test]
2316 fn proc_pointer_var_round_trips_with_global_storage() {
2317 let amod_text = r#"#!amod 2
2318 # module: control_flow
2319 # source: control_flow.f90
2320 # checksum: sha256:def456
2321
2322 @subroutine evaluate_condition_interface
2323 @abi cc=aapcs64 hidden_char_lens=0
2324 @arg n : integer, intent(inout)
2325 @abi pass=x0 width=8
2326 @end subroutine
2327
2328 @var evaluate_condition : type(evaluate_condition_interface), pointer, procptr @ir afs_mod_control_flow_evaluate_condition
2329 "#;
2330 let iface = parse_amod(amod_text, Path::new("test.amod")).unwrap();
2331 let var = iface
2332 .variables
2333 .iter()
2334 .find(|v| v.name == "evaluate_condition")
2335 .unwrap();
2336 assert!(var.pointer);
2337 assert!(var.proc_pointer);
2338 assert!(matches!(
2339 var.type_info,
2340 Some(TypeInfo::Derived(ref name)) if name == "evaluate_condition_interface"
2341 ));
2342
2343 let globals = extract_module_globals(&iface);
2344 let info = globals
2345 .get(&("control_flow".into(), "evaluate_condition".into()))
2346 .unwrap();
2347 assert!(info.is_pointer);
2348 assert_eq!(info.symbol, "afs_mod_control_flow_evaluate_condition");
2349 assert_eq!(
2350 info.ty,
2351 crate::ir::types::IrType::Ptr(Box::new(crate::ir::types::IrType::Int(
2352 crate::ir::types::IntWidth::I8
2353 )))
2354 );
2355 }
2356
2357 #[test]
2358 fn arg_rank_round_trips_for_array_dummies() {
2359 // Sprint35-SMP Phase 1: the rank=N attribute on @arg lines must
2360 // round-trip so the consumer can rebuild a SymbolAttrs::array_spec
2361 // of the right rank for SMP-body synthesis. Scalar args (no
2362 // rank=) parse as rank 0; descriptor-passed assumed-shape arrays
2363 // carry their rank.
2364 let amod_text = r#"#!amod 2
2365 # module: shapes
2366 # source: shapes.f90
2367 # checksum: sha256:abc
2368
2369 @subroutine takes_assumed_shape
2370 @abi cc=aapcs64 hidden_char_lens=0
2371 @arg a : real, intent(in), descriptor, rank=1
2372 @abi pass=x0 width=8
2373 @arg b : real, intent(in), descriptor, rank=2
2374 @abi pass=x1 width=8
2375 @arg n : integer, intent(in)
2376 @abi pass=x2 width=8
2377 @end subroutine
2378
2379 @subroutine takes_alloc_array
2380 @abi cc=aapcs64 hidden_char_lens=0
2381 @arg buf : real, intent(out), descriptor, allocatable, rank=1
2382 @abi pass=x0 width=8
2383 @end subroutine
2384 "#;
2385 let iface = parse_amod(amod_text, Path::new("test.amod")).unwrap();
2386 let assumed = iface
2387 .procedures
2388 .iter()
2389 .find(|p| p.name == "takes_assumed_shape")
2390 .unwrap();
2391 assert_eq!(assumed.args[0].rank, 1);
2392 assert_eq!(assumed.args[1].rank, 2);
2393 assert_eq!(assumed.args[2].rank, 0); // scalar n: no rank= attribute
2394
2395 let alloc = iface
2396 .procedures
2397 .iter()
2398 .find(|p| p.name == "takes_alloc_array")
2399 .unwrap();
2400 assert_eq!(alloc.args[0].rank, 1);
2401 assert!(alloc.args[0].allocatable);
2402 }
2403 }
2404