| 1 | //! Lowering context — `LowerCtx`, `LocalInfo`, RAII guards for the |
| 2 | //! thread-local proc-scope/submodule state, loop-scope tracking, and |
| 3 | //! the small helper enums (`CharKind`, `HiddenResultAbi`) that |
| 4 | //! `LocalInfo` depends on. |
| 5 | //! |
| 6 | //! Extracted from `lower::core` in sprint 04 step 2. Visibility is |
| 7 | //! kept tight (`pub(super)` for the bulk of items, `pub(crate)` only |
| 8 | //! where the type already crossed a module boundary — namely |
| 9 | //! `CharKind`, which `sema::amod` constructs by name). |
| 10 | |
| 11 | use crate::ir::inst::{BlockId, ValueId}; |
| 12 | use crate::ir::types::IrType; |
| 13 | use crate::sema::symtab::SymbolTable; |
| 14 | use std::cell::RefCell; |
| 15 | use std::collections::{HashMap, HashSet}; |
| 16 | use std::rc::Rc; |
| 17 | |
| 18 | use super::const_scalar::ConstScalar; |
| 19 | use super::core::ModuleGlobalInfo; |
| 20 | |
| 21 | pub(super) type AmbiguousUseWarnings = Rc<RefCell<HashSet<(String, String, String)>>>; |
| 22 | |
| 23 | thread_local! { |
| 24 | /// Sema scope id of the procedure currently being lowered. Set by |
| 25 | /// `LowerCtx::with_proc_scope` around `lower_stmts` so the |
| 26 | /// stateless `lower_expr_full` recursion can intercept F77 |
| 27 | /// statement-function call sites without threading an extra |
| 28 | /// parameter through 60-plus call sites. `None` when lowering |
| 29 | /// outside any procedure body (e.g. expression evaluation during |
| 30 | /// `init_decls`). |
| 31 | static CURRENT_PROC_SCOPE: RefCell<Option<crate::sema::symtab::ScopeId>> = |
| 32 | const { RefCell::new(None) }; |
| 33 | |
| 34 | /// For SMP body lowering: the submodule containing the body. The |
| 35 | /// procedure's link name lives under the parent module, but the |
| 36 | /// install_globals_as_locals path also needs the submodule so it |
| 37 | /// can pull in the submodule's locally-declared parameters |
| 38 | /// (mangled under the submodule name post-d770b77). |
| 39 | static SMP_EXTRA_HOST: RefCell<Option<String>> = const { RefCell::new(None) }; |
| 40 | } |
| 41 | |
| 42 | pub(super) fn current_proc_scope() -> Option<crate::sema::symtab::ScopeId> { |
| 43 | CURRENT_PROC_SCOPE.with(|c| *c.borrow()) |
| 44 | } |
| 45 | |
| 46 | pub(super) fn current_smp_extra_host() -> Option<String> { |
| 47 | SMP_EXTRA_HOST.with(|c| c.borrow().clone()) |
| 48 | } |
| 49 | |
| 50 | /// RAII guard: install `scope` as the current procedure scope until |
| 51 | /// the guard is dropped, then restore the previous value. Lets nested |
| 52 | /// contained-subprogram lowering recover its outer scope cleanly. |
| 53 | pub(super) struct ProcScopeGuard(Option<crate::sema::symtab::ScopeId>); |
| 54 | |
| 55 | impl ProcScopeGuard { |
| 56 | pub(super) fn enter(scope: Option<crate::sema::symtab::ScopeId>) -> Self { |
| 57 | let prev = CURRENT_PROC_SCOPE.with(|c| c.replace(scope)); |
| 58 | ProcScopeGuard(prev) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | impl Drop for ProcScopeGuard { |
| 63 | fn drop(&mut self) { |
| 64 | let prev = self.0; |
| 65 | CURRENT_PROC_SCOPE.with(|c| *c.borrow_mut() = prev); |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | pub(super) struct SmpExtraHostGuard(Option<String>); |
| 70 | |
| 71 | impl SmpExtraHostGuard { |
| 72 | pub(super) fn set(name: String) -> Self { |
| 73 | let prev = SMP_EXTRA_HOST.with(|c| c.replace(Some(name))); |
| 74 | SmpExtraHostGuard(prev) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | impl Drop for SmpExtraHostGuard { |
| 79 | fn drop(&mut self) { |
| 80 | let prev = self.0.take(); |
| 81 | SMP_EXTRA_HOST.with(|c| *c.borrow_mut() = prev); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /// Maximum array rank (Fortran allows up to 15). |
| 86 | #[allow(dead_code)] |
| 87 | pub(super) const MAX_RANK: usize = 15; |
| 88 | |
| 89 | /// Loop context for EXIT/CYCLE targeting. |
| 90 | pub(super) struct LoopScope { |
| 91 | pub(super) name: Option<String>, |
| 92 | pub(super) header: BlockId, // CYCLE target |
| 93 | pub(super) exit: BlockId, // EXIT target |
| 94 | } |
| 95 | |
| 96 | /// Character variable kind: how string storage is managed. |
| 97 | #[derive(Clone, PartialEq)] |
| 98 | pub(crate) enum CharKind { |
| 99 | /// Not a character variable. |
| 100 | None, |
| 101 | /// Fixed-length character(N): addr points to N-byte stack buffer. |
| 102 | Fixed(i64), |
| 103 | /// Fixed-length character whose length is only known at runtime. |
| 104 | /// For locals, `addr` is a stack slot holding the heap buffer |
| 105 | /// pointer. For by-ref dummies, `addr` is the usual slot holding |
| 106 | /// the caller's pointer. `len_addr` stores the runtime length. |
| 107 | FixedRuntime { len_addr: ValueId }, |
| 108 | /// Deferred-length character(:), allocatable: addr points to 32-byte StringDescriptor. |
| 109 | Deferred, |
| 110 | /// Assumed-length character(*) dummy parameter. The runtime |
| 111 | /// length is held in a hidden i64 parameter appended after the |
| 112 | /// normal positional args. `len_addr` is the alloca holding |
| 113 | /// the hidden param's value so reads can load it at runtime. |
| 114 | AssumedLen { len_addr: ValueId }, |
| 115 | } |
| 116 | |
| 117 | #[derive(Clone, Copy, PartialEq, Eq)] |
| 118 | pub(super) enum HiddenResultAbi { |
| 119 | None, |
| 120 | ArrayDescriptor, |
| 121 | StringDescriptor, |
| 122 | DerivedAggregate, |
| 123 | /// Complex scalar function result. Caller allocates an 8-byte |
| 124 | /// (real(sp)) or 16-byte (real(dp)) buffer, passes its address as |
| 125 | /// the hidden first param; callee writes the two float lanes |
| 126 | /// through that pointer and returns void. Without this, the |
| 127 | /// IR-level return type was `[Float x 2]` aggregate which codegen |
| 128 | /// packed into x0 as 8 bytes — the caller then memcpy'd from x0 |
| 129 | /// treating the value AS a pointer (SEGV on first complex-returning |
| 130 | /// call to e.g. stdlib's `gamma_dist_pdf_csp`). |
| 131 | ComplexBuffer, |
| 132 | } |
| 133 | |
| 134 | /// Info about a local variable. |
| 135 | #[derive(Clone)] |
| 136 | pub(super) struct LocalInfo { |
| 137 | pub(super) addr: ValueId, |
| 138 | pub(super) ty: IrType, |
| 139 | /// For arrays: (lower_bound, extent) per dimension. Empty for scalars. |
| 140 | pub(super) dims: Vec<(i64, i64)>, |
| 141 | /// Is this an allocatable variable? |
| 142 | pub(super) allocatable: bool, |
| 143 | /// Does this local carry runtime array metadata through a descriptor even |
| 144 | /// though it is not allocatable (for example an assumed-shape dummy)? |
| 145 | pub(super) descriptor_arg: bool, |
| 146 | /// Is this a pass-by-reference parameter? If true, `addr` holds a pointer |
| 147 | /// to the caller's storage. Reads/writes go through the pointer. |
| 148 | pub(super) by_ref: bool, |
| 149 | /// Character variable kind (fixed-length, deferred, or not character). |
| 150 | pub(super) char_kind: CharKind, |
| 151 | /// Derived type name (for component access resolution). Empty for non-derived. |
| 152 | pub(super) derived_type: Option<String>, |
| 153 | /// For PARAMETER-attributed locals whose initializer const-folds: |
| 154 | /// the compile-time value to inline at every use. When `Some`, |
| 155 | /// `Expr::Name` lookups should materialize this constant |
| 156 | /// directly via `b.const_i32`/`b.const_i64`/etc., instead of |
| 157 | /// loading through `addr`. Audit MAJOR-4: this lets parameters |
| 158 | /// avoid wasting a `.data` slot per scope. |
| 159 | pub(super) inline_const: Option<ConstScalar>, |
| 160 | /// Fortran `POINTER` attribute on a scalar local. When true, |
| 161 | /// `addr` is an `alloca ptr<ty>` — a pointer slot that holds |
| 162 | /// the address of the associated target (or null when |
| 163 | /// unassociated). Reads/writes go through the slot just like |
| 164 | /// `by_ref`, but `by_ref` is reserved for dummy arguments that |
| 165 | /// cannot carry a Fortran `POINTER` attribute's semantics |
| 166 | /// (reassociation via `=>`, dereference on plain assignment). |
| 167 | pub(super) is_pointer: bool, |
| 168 | /// Per-dimension runtime upper bound (i64 value id) for arrays |
| 169 | /// whose bounds are not compile-time constants — most commonly |
| 170 | /// explicit-shape dummies like `xs(n)` where `n` is a dummy |
| 171 | /// argument. When `Some`, bounds checks and cumulative-stride |
| 172 | /// computation consult this value instead of `dims[i].1`. |
| 173 | /// Empty vec (or an all-`None` vec) means the compile-time |
| 174 | /// `dims` is authoritative. Parallel to `dims`. |
| 175 | pub(super) runtime_dim_upper: Vec<Option<ValueId>>, |
| 176 | /// True when the declaration used CLASS(...) rather than TYPE(...). |
| 177 | pub(super) is_class: bool, |
| 178 | /// Logical kind from the source declaration (1, 2, 4, or 8) when the |
| 179 | /// variable is `logical(kind)`. Default-kind logical leaves this as |
| 180 | /// Some(4); non-logical declarations leave it None. Needed because |
| 181 | /// kind=1/2/8 logicals are stored as `IrType::Int(I8/I16/I64)` to get |
| 182 | /// kind-correct storage size, which would otherwise be indistinguishable |
| 183 | /// from real integer locals at later lookup points |
| 184 | /// (semantic-type recovery, generic dispatch, print formatting, etc.). |
| 185 | pub(super) logical_kind: Option<u8>, |
| 186 | /// True for explicit-shape dummy arrays whose last dimension is |
| 187 | /// `*` (assumed-size, F2018 §8.5.8.5). Such a dummy carries no |
| 188 | /// upper bound on the last dim — accesses past `size(actual)` are |
| 189 | /// legal as long as the underlying storage permits — so bounds |
| 190 | /// checks must be skipped on that dim. The caller's descriptor |
| 191 | /// (or the static `(1, 0)` sentinel emitted by extract_array_dims) |
| 192 | /// would otherwise reject every legal access. |
| 193 | pub(super) last_dim_assumed_size: bool, |
| 194 | } |
| 195 | |
| 196 | /// Lowering context — tracks locals, loop scopes, and symbol table. |
| 197 | pub(super) struct LowerCtx<'a> { |
| 198 | pub(super) locals: HashMap<String, LocalInfo>, |
| 199 | /// Lowercase names of OPTIONAL dummy arguments in the current subprogram. |
| 200 | /// Hidden character-length forwarding must treat an absent optional |
| 201 | /// character dummy as length zero instead of dereferencing its null slot. |
| 202 | pub(super) optional_locals: HashSet<String>, |
| 203 | pub(super) loops: Vec<LoopScope>, |
| 204 | pub(super) st: &'a SymbolTable, |
| 205 | /// Module-scoped globals visible by (lowercase module name, |
| 206 | /// lowercase variable name). Populated by the lower_file |
| 207 | /// pre-pass over `ProgramUnit::Module` units so any subsequent |
| 208 | /// function that USE-imports the module can resolve the name |
| 209 | /// to a `GlobalAddr`. Keying by (module, var) is what lets |
| 210 | /// install_globals_as_locals filter by the current function's |
| 211 | /// USE statements, honor ONLY lists, and apply renames. |
| 212 | pub(super) globals: &'a HashMap<(String, String), ModuleGlobalInfo>, |
| 213 | pub(super) type_layouts: &'a crate::sema::type_layout::TypeLayoutRegistry, |
| 214 | /// Names that a `use mod, only: ...` statement explicitly |
| 215 | /// excluded. install_globals_as_locals populates this from the |
| 216 | /// difference between a module's exported globals and the |
| 217 | /// only-list. Audit MAJOR-1: a reference to a name in this |
| 218 | /// set must produce a compile error rather than silently |
| 219 | /// lowering to const_int 0. |
| 220 | pub(super) filtered_names: HashSet<String>, |
| 221 | /// For functions: address of the result variable (for RETURN). |
| 222 | pub(super) result_addr: Option<ValueId>, |
| 223 | /// For functions: the return type. |
| 224 | pub(super) result_type: Option<IrType>, |
| 225 | /// For functions: lowercase result variable name when one exists. |
| 226 | pub(super) result_name: Option<String>, |
| 227 | /// Hidden result ABI for this function, if any. |
| 228 | pub(super) hidden_result_abi: HiddenResultAbi, |
| 229 | /// Names of functions in the compilation unit that return allocatable |
| 230 | /// arrays (sret convention). Used at call sites to detect when to |
| 231 | /// pass a temp descriptor as the hidden first arg. Audit6 BLOCKING-1. |
| 232 | pub(super) alloc_return_funcs: &'a HashSet<String>, |
| 233 | /// Per-subroutine optional-parameter bitmap: maps lowercase callee name |
| 234 | /// to a Vec<bool> (one entry per positional parameter, true = OPTIONAL). |
| 235 | /// Pre-populated by `collect_optional_params` so call sites can pass |
| 236 | /// null pointers for absent optional arguments (PRESENT support). |
| 237 | pub(super) optional_params: &'a HashMap<String, Vec<bool>>, |
| 238 | /// Per-subroutine/function descriptor-parameter bitmap: maps lowercase |
| 239 | /// callee name to a Vec<bool> (one entry per positional parameter, |
| 240 | /// true = lower this dummy through an ArrayDescriptor). |
| 241 | pub(super) descriptor_params: &'a HashMap<String, Vec<bool>>, |
| 242 | /// Lowercase same-module subprogram name → Module::functions index. |
| 243 | /// Used so same-compilation-unit calls lower to FuncRef::Internal instead |
| 244 | /// of pretending to be external references. |
| 245 | pub(super) internal_funcs: &'a HashMap<String, u32>, |
| 246 | /// Lowercase names of functions declared ELEMENTAL in this compilation unit. |
| 247 | pub(super) elemental_funcs: &'a HashSet<String>, |
| 248 | /// Per-callee bitmap of which params are character(len=*). |
| 249 | /// Call sites append the string length as a hidden i64 arg for |
| 250 | /// each flagged position. |
| 251 | pub(super) char_len_star_params: &'a HashMap<String, Vec<bool>>, |
| 252 | /// Per-callee list of host-associated variable names the callee |
| 253 | /// needs threaded in as hidden trailing pointer args. See the |
| 254 | /// closure-passing ABI documented on `lower_file::contained_host_refs`. |
| 255 | pub(super) contained_host_refs: &'a HashMap<String, Vec<String>>, |
| 256 | /// Map from Fortran statement label (u64) to the IR basic block that |
| 257 | /// begins at that label. Pre-populated by `collect_label_blocks` before |
| 258 | /// lowering so that GOTO can branch forward as well as backward. |
| 259 | pub(super) label_blocks: HashMap<u64, BlockId>, |
| 260 | /// Cross-function dedupe for ambiguous USE-import warnings emitted by |
| 261 | /// install_globals_as_locals. Large fortsh units can otherwise print the |
| 262 | /// exact same ambiguity hundreds or thousands of times while lowering each |
| 263 | /// contained procedure separately. |
| 264 | pub(super) ambiguous_use_warnings: AmbiguousUseWarnings, |
| 265 | /// Sema's `ScopeId` for the procedure currently being lowered. |
| 266 | /// Set in the Program/Subroutine/Function arms of `lower_unit`. |
| 267 | /// Statement-function lookup keys off this — without it we can't |
| 268 | /// distinguish `cabs1` defined in one stdlib BLAS routine from a |
| 269 | /// homonymous statement function in another. |
| 270 | pub(super) proc_scope_id: Option<crate::sema::symtab::ScopeId>, |
| 271 | } |
| 272 | |
| 273 | impl<'a> LowerCtx<'a> { |
| 274 | #[allow(clippy::too_many_arguments)] |
| 275 | pub(super) fn new( |
| 276 | st: &'a SymbolTable, |
| 277 | globals: &'a HashMap<(String, String), ModuleGlobalInfo>, |
| 278 | type_layouts: &'a crate::sema::type_layout::TypeLayoutRegistry, |
| 279 | alloc_return_funcs: &'a HashSet<String>, |
| 280 | optional_params: &'a HashMap<String, Vec<bool>>, |
| 281 | descriptor_params: &'a HashMap<String, Vec<bool>>, |
| 282 | internal_funcs: &'a HashMap<String, u32>, |
| 283 | elemental_funcs: &'a HashSet<String>, |
| 284 | char_len_star_params: &'a HashMap<String, Vec<bool>>, |
| 285 | contained_host_refs: &'a HashMap<String, Vec<String>>, |
| 286 | ambiguous_use_warnings: AmbiguousUseWarnings, |
| 287 | ) -> Self { |
| 288 | Self { |
| 289 | locals: HashMap::new(), |
| 290 | optional_locals: HashSet::new(), |
| 291 | loops: Vec::new(), |
| 292 | st, |
| 293 | globals, |
| 294 | type_layouts, |
| 295 | filtered_names: HashSet::new(), |
| 296 | result_addr: None, |
| 297 | result_type: None, |
| 298 | result_name: None, |
| 299 | hidden_result_abi: HiddenResultAbi::None, |
| 300 | alloc_return_funcs, |
| 301 | optional_params, |
| 302 | descriptor_params, |
| 303 | internal_funcs, |
| 304 | elemental_funcs, |
| 305 | char_len_star_params, |
| 306 | contained_host_refs, |
| 307 | label_blocks: HashMap::new(), |
| 308 | ambiguous_use_warnings, |
| 309 | proc_scope_id: None, |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | pub(super) fn insert_scalar(&mut self, name: String, addr: ValueId, ty: IrType) { |
| 314 | self.locals.insert( |
| 315 | name, |
| 316 | LocalInfo { |
| 317 | addr, |
| 318 | ty, |
| 319 | dims: vec![], |
| 320 | allocatable: false, |
| 321 | descriptor_arg: false, |
| 322 | by_ref: false, |
| 323 | char_kind: CharKind::None, |
| 324 | derived_type: None, |
| 325 | inline_const: None, |
| 326 | is_pointer: false, |
| 327 | runtime_dim_upper: vec![], |
| 328 | is_class: false, |
| 329 | logical_kind: None, |
| 330 | last_dim_assumed_size: false, |
| 331 | }, |
| 332 | ); |
| 333 | } |
| 334 | |
| 335 | pub(super) fn insert_param_by_ref(&mut self, name: String, addr: ValueId, ty: IrType) { |
| 336 | self.locals.insert( |
| 337 | name, |
| 338 | LocalInfo { |
| 339 | addr, |
| 340 | ty, |
| 341 | dims: vec![], |
| 342 | allocatable: false, |
| 343 | descriptor_arg: false, |
| 344 | by_ref: true, |
| 345 | char_kind: CharKind::None, |
| 346 | derived_type: None, |
| 347 | inline_const: None, |
| 348 | is_pointer: false, |
| 349 | runtime_dim_upper: vec![], |
| 350 | is_class: false, |
| 351 | logical_kind: None, |
| 352 | last_dim_assumed_size: false, |
| 353 | }, |
| 354 | ); |
| 355 | } |
| 356 | |
| 357 | pub(super) fn push_loop(&mut self, name: Option<String>, header: BlockId, exit: BlockId) { |
| 358 | self.loops.push(LoopScope { name, header, exit }); |
| 359 | } |
| 360 | |
| 361 | pub(super) fn pop_loop(&mut self) { |
| 362 | self.loops.pop(); |
| 363 | } |
| 364 | |
| 365 | /// Look up an F77 statement function by name in the current |
| 366 | /// procedure scope. Returns `None` when the name doesn't refer to |
| 367 | /// a statement function (or no procedure scope is set). |
| 368 | pub(super) fn lookup_statement_function( |
| 369 | &self, |
| 370 | name: &str, |
| 371 | ) -> Option<&'a crate::sema::symtab::StatementFunctionDef> { |
| 372 | let scope_id = self.proc_scope_id?; |
| 373 | self.st.lookup_statement_function(scope_id, name) |
| 374 | } |
| 375 | |
| 376 | /// Find loop by construct name (or innermost if None). |
| 377 | pub(super) fn find_loop(&self, name: &Option<String>) -> Option<&LoopScope> { |
| 378 | if let Some(n) = name { |
| 379 | self.loops.iter().rev().find(|l| { |
| 380 | l.name |
| 381 | .as_deref() |
| 382 | .map(|s| s.eq_ignore_ascii_case(n)) |
| 383 | .unwrap_or(false) |
| 384 | }) |
| 385 | } else { |
| 386 | self.loops.last() |
| 387 | } |
| 388 | } |
| 389 | } |
| 390 |