Rust · 38488 bytes Raw Blame History
1 //! Symbol table infrastructure.
2 //!
3 //! Provides scope-based symbol management with Fortran's four association
4 //! mechanisms: local declaration, USE association, host association, and
5 //! IMPORT. Handles implicit typing and case-insensitive lookup.
6
7 use crate::ast::decl::ArraySpec;
8 use crate::ast::expr::SpannedExpr;
9 use crate::lexer::Span;
10 use std::borrow::Cow;
11 use std::collections::HashMap;
12
13 /// Sprint 07: borrow when the input is already canonical lowercase,
14 /// allocate only when at least one ASCII uppercase byte needs folding.
15 /// Symbol-table keys are stored in canonical lowercase, so most
16 /// callers (lowering, type-spec resolution) hand us a pre-lowercased
17 /// string — this skips ~one allocation per `lookup_in` /
18 /// `find_symbol_any_scope` call on the hot lookup paths.
19 fn ensure_ascii_lowercase(s: &str) -> Cow<'_, str> {
20 if s.bytes().any(|b| b.is_ascii_uppercase()) {
21 Cow::Owned(s.to_ascii_lowercase())
22 } else {
23 Cow::Borrowed(s)
24 }
25 }
26
27 /// Scope identifier — an index into the SymbolTable's scope list.
28 pub type ScopeId = usize;
29
30 /// F77 §15.4 statement function: a single-line function defined inside
31 /// the host procedure's declaration prologue, scoped to that procedure
32 /// only. Stored on the SymbolTable as a side table so lowering can skip
33 /// the recognized definition statement and inline-substitute call sites.
34 #[derive(Debug, Clone)]
35 pub struct StatementFunctionDef {
36 /// Dummy parameter names (lowercase), in declaration order.
37 pub params: Vec<String>,
38 /// Body expression, exactly as written on the RHS of `name(...) = expr`.
39 pub body: SpannedExpr,
40 /// Declared result type (from the `type :: name` declaration).
41 pub result_type: TypeInfo,
42 }
43
44 /// The symbol table — manages all scopes in a compilation.
45 #[derive(Debug)]
46 pub struct SymbolTable {
47 pub(crate) scopes: Vec<Scope>,
48 pub(crate) current: ScopeId,
49 /// (scope_id, lowercase fname) → statement function definition.
50 /// Populated by sema's `detect_statement_functions` pass during
51 /// `resolve_unit` for Subroutine/Function/Program arms.
52 pub statement_functions: HashMap<(ScopeId, String), StatementFunctionDef>,
53 }
54
55 impl SymbolTable {
56 pub fn new() -> Self {
57 let global = Scope {
58 id: 0,
59 parent: None,
60 kind: ScopeKind::Global,
61 symbols: HashMap::new(),
62 implicit_rules: ImplicitRules::default_fortran(),
63 has_explicit_implicit_stmt: false,
64 use_associations: Vec::new(),
65 default_access: Access::Public,
66 pending_access: HashMap::new(),
67 arg_order: Vec::new(),
68 };
69 Self {
70 scopes: vec![global],
71 current: 0,
72 statement_functions: HashMap::new(),
73 }
74 }
75
76 /// Lookup a statement function by (scope, name). Caller passes
77 /// the scope where the call site appears; we walk up the parent
78 /// chain so a statement function defined in the host is visible
79 /// to nested constructs (DO/IF/SELECT bodies don't get their own
80 /// procedure scope, so this typically resolves at the same scope).
81 pub fn lookup_statement_function(
82 &self,
83 scope_id: ScopeId,
84 name: &str,
85 ) -> Option<&StatementFunctionDef> {
86 let key = name.to_lowercase();
87 let mut cur = Some(scope_id);
88 while let Some(sid) = cur {
89 if let Some(def) = self.statement_functions.get(&(sid, key.clone())) {
90 return Some(def);
91 }
92 // Statement functions are scope-local to the containing
93 // procedure (Subroutine/Function/Program). Stop walking
94 // when we leave a procedure scope.
95 match self.scopes[sid].kind {
96 ScopeKind::Subroutine(_) | ScopeKind::Function(_) | ScopeKind::Program(_) => {
97 return None
98 }
99 _ => cur = self.scopes[sid].parent,
100 }
101 }
102 None
103 }
104 }
105
106 impl Default for SymbolTable {
107 fn default() -> Self {
108 Self::new()
109 }
110 }
111
112 impl SymbolTable {
113 /// Create a new child scope of the current scope.
114 pub fn push_scope(&mut self, kind: ScopeKind) -> ScopeId {
115 let id = self.scopes.len();
116 let parent_implicit = self.scopes[self.current].implicit_rules.clone();
117 let scope = Scope {
118 id,
119 parent: Some(self.current),
120 kind,
121 symbols: HashMap::new(),
122 implicit_rules: parent_implicit, // inherit from parent, may be overridden
123 has_explicit_implicit_stmt: false,
124 use_associations: Vec::new(),
125 default_access: Access::Public,
126 pending_access: HashMap::new(),
127 arg_order: Vec::new(),
128 };
129 self.scopes.push(scope);
130 self.current = id;
131 id
132 }
133
134 /// Enter an existing scope by ID without creating a new one.
135 /// Returns the previous scope ID for later restoration.
136 pub fn enter_scope(&mut self, id: ScopeId) -> ScopeId {
137 let saved = self.current;
138 self.current = id;
139 saved
140 }
141
142 /// Return to the parent scope.
143 pub fn pop_scope(&mut self) {
144 if let Some(parent) = self.scopes[self.current].parent {
145 self.current = parent;
146 }
147 }
148
149 /// Get the current scope ID.
150 pub fn current_scope(&self) -> ScopeId {
151 self.current
152 }
153
154 /// Get a scope by ID.
155 pub fn scope(&self, id: ScopeId) -> &Scope {
156 &self.scopes[id]
157 }
158
159 /// Get a mutable scope by ID.
160 pub fn scope_mut(&mut self, id: ScopeId) -> &mut Scope {
161 &mut self.scopes[id]
162 }
163
164 /// Define a symbol in the current scope.
165 pub fn define(&mut self, symbol: Symbol) -> Result<(), SemaError> {
166 let key = symbol.name.to_lowercase();
167 let scope = &mut self.scopes[self.current];
168 if scope.symbols.contains_key(&key) {
169 return Err(SemaError {
170 span: symbol.defined_at,
171 msg: format!("symbol '{}' already defined in this scope", symbol.name),
172 });
173 }
174 let mut symbol = symbol;
175 if let Some(access) = scope.pending_access.get(&key).copied() {
176 symbol.attrs.access = access;
177 }
178 scope.symbols.insert(key, symbol);
179 Ok(())
180 }
181
182 /// Define a symbol in a specific scope.
183 pub fn define_in(&mut self, scope_id: ScopeId, symbol: Symbol) -> Result<(), SemaError> {
184 let key = symbol.name.to_lowercase();
185 let scope = &mut self.scopes[scope_id];
186 if scope.symbols.contains_key(&key) {
187 return Err(SemaError {
188 span: symbol.defined_at,
189 msg: format!("symbol '{}' already defined in this scope", symbol.name),
190 });
191 }
192 let mut symbol = symbol;
193 if let Some(access) = scope.pending_access.get(&key).copied() {
194 symbol.attrs.access = access;
195 }
196 scope.symbols.insert(key, symbol);
197 Ok(())
198 }
199
200 /// Look up a name in the current scope with Fortran resolution order:
201 /// Local > USE association > Host association > Implicit typing
202 pub fn lookup(&self, name: &str) -> Option<&Symbol> {
203 self.lookup_in(self.current, name)
204 }
205
206 /// Look up a name starting from a specific scope.
207 pub fn lookup_in(&self, scope_id: ScopeId, name: &str) -> Option<&Symbol> {
208 // Sprint 07: avoid the unconditional `to_ascii_lowercase`
209 // allocation. Symtab keys live in canonical lowercase, but
210 // most callers (lowering, type-spec resolution) already pass
211 // pre-canonicalized names — borrow when there's nothing to
212 // fold, allocate only when uppercase bytes are present.
213 let key = ensure_ascii_lowercase(name);
214 let mut visited = Vec::new();
215 self.lookup_in_guarded(scope_id, key.as_ref(), &mut visited)
216 }
217
218 fn lookup_in_guarded(
219 &self,
220 scope_id: ScopeId,
221 key: &str,
222 visited: &mut Vec<ScopeId>,
223 ) -> Option<&Symbol> {
224 if visited.contains(&scope_id) {
225 return None;
226 }
227 visited.push(scope_id);
228
229 let scope = &self.scopes[scope_id];
230
231 let result = (|| {
232 // 1. Local declaration.
233 if let Some(sym) = scope.symbols.get(key) {
234 return Some(sym);
235 }
236
237 // 2. Direct USE association — check the source module's own
238 // symbols first, then chase through that module's USE chain
239 // for the SAME name (handles re-exports like `use
240 // stdlib_kinds, only: int32` where int32 itself is a USE-
241 // associated re-export from iso_fortran_env). Only the UA's
242 // original_name is followed, so unrelated names cannot leak.
243 for assoc in &scope.use_associations {
244 if assoc.local_name == key {
245 if let Some(sym) = self.scopes[assoc.source_scope]
246 .symbols
247 .get(&assoc.original_name)
248 {
249 if sym.attrs.access != Access::Private || assoc.is_submodule_access {
250 return Some(sym);
251 }
252 }
253 if let Some(sym) =
254 self.lookup_in_guarded(assoc.source_scope, &assoc.original_name, visited)
255 {
256 if sym.attrs.access != Access::Private || assoc.is_submodule_access {
257 return Some(sym);
258 }
259 }
260 }
261 }
262
263 // 2b. Transitive USE: look through each USE'd module's own
264 // public symbols and its transitive USE chain. Only applies
265 // to bare `USE M` — `use M, only: x` and `use M, only: x =>
266 // y` must NOT expose other names from M, including
267 // same-named generic interfaces whose specifics would
268 // otherwise be silently merged into a user-scope generic of
269 // the same name.
270 let mut seen_use_scopes = Vec::new();
271 for assoc in &scope.use_associations {
272 if !assoc.from_bare_use {
273 continue;
274 }
275 if assoc.local_name != assoc.original_name {
276 continue;
277 }
278 if seen_use_scopes.contains(&assoc.source_scope) {
279 continue;
280 }
281 seen_use_scopes.push(assoc.source_scope);
282 if let Some(sym) = self.lookup_in_guarded(assoc.source_scope, key, visited) {
283 if sym.attrs.access != Access::Private {
284 return Some(sym);
285 }
286 }
287 }
288
289 // 3. Host association — look in parent scope.
290 if let Some(parent) = scope.parent {
291 if self.scopes[parent].kind != ScopeKind::Global {
292 return self.lookup_in_guarded(parent, key, visited);
293 }
294 }
295
296 None
297 })();
298
299 visited.pop();
300 result
301 }
302
303 /// Sprint 08: scope-correct lookup with all-scope fallback.
304 ///
305 /// Tries `lookup_in(proc_scope_id, name)` first — that walks
306 /// local → USE associations → transitive USE → host scope per
307 /// Fortran's normal name resolution order, returning the symbol
308 /// the source actually refers to. If `proc_scope_id` is `None`
309 /// (or the scoped lookup misses), falls back to
310 /// `find_symbol_any_scope`, which scans every scope without
311 /// regard to visibility.
312 ///
313 /// The fallback preserves prior behavior — this helper is a
314 /// stepping stone, not a behavior change. Call sites in
315 /// `src/ir/lower/**` should migrate to this helper as part of
316 /// killing `find_symbol_any_scope` (see `feedback_lookup_discipline.md`).
317 pub fn lookup_local_then_any(
318 &self,
319 proc_scope_id: Option<ScopeId>,
320 name: &str,
321 ) -> Option<&Symbol> {
322 if let Some(scope_id) = proc_scope_id {
323 if let Some(sym) = self.lookup_in(scope_id, name) {
324 return Some(sym);
325 }
326 }
327 self.find_symbol_any_scope(name)
328 }
329
330 /// Search ALL scopes for a symbol by name.
331 /// Used during lowering when the current scope may not be set correctly.
332 /// Prefers parameter symbols (for kind resolution) but returns any match.
333 pub fn find_symbol_any_scope(&self, name: &str) -> Option<&Symbol> {
334 let key_cow = ensure_ascii_lowercase(name);
335 let key: &str = key_cow.as_ref();
336 // Track the best fallback seen so far. A typed
337 // Function/Subroutine carries the most useful information
338 // (return type, kind, ABI) for callers that use this helper to
339 // resolve a procedure reference. A NamedInterface with the same
340 // name (common when a stdlib module re-exports a function via a
341 // generic interface block) shadows the typed entry on the first
342 // scope-iteration hit but provides only a list of specifics —
343 // not enough for return-type or character-ABI lookup. Prefer
344 // typed callable kinds over NamedInterface so callers don't
345 // have to walk every scope themselves.
346 let mut fallback: Option<&Symbol> = None;
347 let mut typed_callable: Option<&Symbol> = None;
348 for scope in &self.scopes {
349 if let Some(sym) = scope.symbols.get(key) {
350 if sym.attrs.parameter {
351 return Some(sym);
352 }
353 if matches!(
354 sym.kind,
355 SymbolKind::Function
356 | SymbolKind::Subroutine
357 | SymbolKind::ExternalProc
358 | SymbolKind::IntrinsicProc
359 | SymbolKind::ProcedurePointer
360 ) && typed_callable.is_none()
361 {
362 typed_callable = Some(sym);
363 }
364 if fallback.is_none() {
365 fallback = Some(sym);
366 }
367 }
368 }
369 if let Some(sym) = typed_callable {
370 return Some(sym);
371 }
372 if fallback.is_some() {
373 return fallback;
374 }
375 // Second pass: resolve USE renames. `use m, only: a => add`
376 // installs a UseAssociation with local_name="a" and
377 // original_name="add" but no symbol named "a" on the
378 // enclosing scope. Direct-symbol scans miss the rename; walk
379 // every scope's UseAssociations and follow the source to pick
380 // up the underlying symbol (NamedInterface for generic
381 // dispatch, Function for ordinary calls, etc.).
382 //
383 // The source-scope lookup MUST chase through transitive USE
384 // chains: `stdlib_kinds` re-exports `int64` from
385 // `iso_fortran_env`, so `use stdlib_kinds, only: block_kind => int64`
386 // can't find `int64` in stdlib_kinds's own symbols — the kind
387 // constant lives one hop further up. Without the chase,
388 // `integer(block_kind) :: dummy` falls back to default kind=4
389 // inside the submodule body, silently truncating the local
390 // from 8 bytes to 4 even though the parent type's `block`
391 // field is correctly laid out.
392 for scope in &self.scopes {
393 for assoc in &scope.use_associations {
394 if assoc.local_name == key {
395 if let Some(sym) = self.scopes[assoc.source_scope]
396 .symbols
397 .get(&assoc.original_name)
398 {
399 return Some(sym);
400 }
401 if let Some(sym) = self.lookup_in(assoc.source_scope, &assoc.original_name) {
402 return Some(sym);
403 }
404 }
405 }
406 }
407 None
408 }
409
410 /// Check if a name would be implicitly typed in the current scope.
411 /// Returns the implicit type if applicable, or None if implicit none.
412 pub fn implicit_type(&self, name: &str) -> Option<ImplicitType> {
413 let scope = &self.scopes[self.current];
414 scope.implicit_rules.type_for(name)
415 }
416
417 /// Set implicit none for the current scope.
418 pub fn set_implicit_none(&mut self, type_: bool, external: bool) {
419 let scope = &mut self.scopes[self.current];
420 scope.has_explicit_implicit_stmt = true;
421 if type_ {
422 scope.implicit_rules.none_type = true;
423 }
424 if external {
425 scope.implicit_rules.none_external = true;
426 }
427 }
428
429 /// Force IMPLICIT NONE on every program-unit-level scope in the
430 /// table (Program, Module, Submodule, Subroutine, Function,
431 /// BlockData). Used by the driver's `-fimplicit-none` flag,
432 /// which mirrors the gfortran option of the same name and tells
433 /// validate.rs to flag every undeclared name even in scopes that
434 /// don't have an explicit `implicit none` statement.
435 pub fn force_implicit_none_all_units(&mut self) {
436 for scope in &mut self.scopes {
437 if matches!(
438 scope.kind,
439 ScopeKind::Program(_)
440 | ScopeKind::Module(_)
441 | ScopeKind::Submodule(_)
442 | ScopeKind::Subroutine(_)
443 | ScopeKind::Function(_)
444 ) && !scope.has_explicit_implicit_stmt
445 {
446 scope.implicit_rules.none_type = true;
447 }
448 }
449 }
450
451 /// Set an implicit typing rule for the current scope.
452 pub fn set_implicit_rule(&mut self, start: char, end: char, itype: ImplicitType) {
453 let scope = &mut self.scopes[self.current];
454 scope.has_explicit_implicit_stmt = true;
455 scope.implicit_rules.none_type = false;
456 for c in start..=end {
457 scope
458 .implicit_rules
459 .rules
460 .insert(c.to_ascii_lowercase(), itype);
461 }
462 }
463
464 /// Add a USE association to the current scope.
465 pub fn add_use_association(&mut self, assoc: UseAssociation) {
466 let assoc = UseAssociation {
467 local_name: assoc.local_name.to_ascii_lowercase(),
468 original_name: assoc.original_name.to_ascii_lowercase(),
469 source_scope: assoc.source_scope,
470 is_submodule_access: assoc.is_submodule_access,
471 from_bare_use: assoc.from_bare_use,
472 };
473 self.scopes[self.current].use_associations.push(assoc);
474 }
475
476 /// Set the default accessibility for the current scope.
477 pub fn set_default_access(&mut self, access: Access) {
478 self.scopes[self.current].default_access = access;
479 }
480
481 /// Set the access level on a specific symbol in the current scope.
482 /// Used for `PUBLIC :: name` and `PRIVATE :: name` statements.
483 pub fn set_symbol_access(&mut self, name: &str, access: Access) {
484 let key = name.to_lowercase();
485 self.scopes[self.current]
486 .pending_access
487 .insert(key.clone(), access);
488 if let Some(sym) = self.scopes[self.current].symbols.get_mut(&key) {
489 sym.attrs.access = access;
490 }
491 }
492
493 /// Iterate all scopes (for generic interface resolution during lowering).
494 pub fn all_scopes(&self) -> &[Scope] {
495 &self.scopes
496 }
497
498 /// Check whether implicit none (type) is active in a scope.
499 pub fn is_implicit_none(&self, scope_id: ScopeId) -> bool {
500 self.scopes[scope_id].implicit_rules.none_type
501 }
502
503 /// Get the default accessibility for a scope.
504 pub fn default_access(&self, scope_id: ScopeId) -> Access {
505 self.scopes[scope_id].default_access
506 }
507
508 /// Find a module scope by name (for USE resolution within the same file).
509 pub fn find_module_scope(&self, name: &str) -> Option<ScopeId> {
510 self.scopes.iter().find_map(|s| {
511 if let ScopeKind::Module(ref n) = s.kind {
512 if n.eq_ignore_ascii_case(name) {
513 Some(s.id)
514 } else {
515 None
516 }
517 } else {
518 None
519 }
520 })
521 }
522 }
523
524 /// A scope in the symbol table.
525 #[derive(Debug)]
526 pub struct Scope {
527 pub id: ScopeId,
528 pub parent: Option<ScopeId>,
529 pub kind: ScopeKind,
530 pub symbols: HashMap<String, Symbol>,
531 pub implicit_rules: ImplicitRules,
532 pub has_explicit_implicit_stmt: bool,
533 pub use_associations: Vec<UseAssociation>,
534 pub default_access: Access,
535 pub pending_access: HashMap<String, Access>,
536 /// Ordered dummy argument names (for function/subroutine scopes).
537 pub arg_order: Vec<String>,
538 }
539
540 /// What kind of scope this is.
541 #[derive(Debug, Clone, PartialEq, Eq)]
542 pub enum ScopeKind {
543 Global,
544 Module(String),
545 Submodule(String),
546 Program(String),
547 Subroutine(String),
548 Function(String),
549 Block,
550 Interface,
551 DerivedType(String),
552 Forall,
553 Associate,
554 Critical,
555 }
556
557 /// A symbol — a named entity in a scope.
558 #[derive(Debug, Clone)]
559 pub struct Symbol {
560 pub name: String,
561 pub kind: SymbolKind,
562 pub type_info: Option<TypeInfo>,
563 pub attrs: SymbolAttrs,
564 pub defined_at: Span,
565 pub scope: ScopeId,
566 /// Ordered dummy argument names (for functions/subroutines).
567 pub arg_names: Vec<String>,
568 /// Compile-time constant value (for PARAMETERs like c_int=4).
569 pub const_value: Option<i64>,
570 }
571
572 /// What kind of entity this symbol represents.
573 #[derive(Debug, Clone, PartialEq, Eq)]
574 pub enum SymbolKind {
575 Variable,
576 Parameter,
577 Function,
578 Subroutine,
579 Module,
580 DerivedType,
581 NamedInterface,
582 Enumerator,
583 Namelist,
584 CommonBlock,
585 ExternalProc,
586 IntrinsicProc,
587 ProcedurePointer,
588 Label(u64),
589 }
590
591 /// Type information for a symbol.
592 #[derive(Debug, Clone, PartialEq)]
593 pub enum TypeInfo {
594 Integer { kind: Option<u8> },
595 Real { kind: Option<u8> },
596 DoublePrecision,
597 Complex { kind: Option<u8> },
598 Logical { kind: Option<u8> },
599 Character { len: Option<i64>, kind: Option<u8> },
600 Derived(String),
601 Class(String),
602 ClassStar,
603 TypeStar,
604 }
605
606 /// Symbol attributes.
607 #[derive(Debug, Clone)]
608 pub struct SymbolAttrs {
609 pub access: Access,
610 pub allocatable: bool,
611 pub pointer: bool,
612 /// For BIND(C, NAME="...") procedures, preserve the actual link
613 /// symbol so lowering can call the declared external name rather
614 /// than the local Fortran alias.
615 pub binding_label: Option<String>,
616 /// For `procedure(iface), pointer :: p`, preserve the declared
617 /// interface name so `.amod` can round-trip the symbol truthfully.
618 pub procedure_iface: Option<String>,
619 pub target: bool,
620 pub optional: bool,
621 pub save: bool,
622 pub parameter: bool,
623 pub value: bool,
624 pub intent: Option<Intent>,
625 pub external: bool,
626 pub intrinsic: bool,
627 /// Procedure declared with the PURE prefix.
628 pub pure: bool,
629 /// Procedure declared with the ELEMENTAL prefix.
630 pub elemental: bool,
631 /// For Function symbols whose result is an array (allocatable,
632 /// automatic, or fixed-shape): rank of the result. 0 for scalar
633 /// results. Used by lowering to route array-returning calls
634 /// through the descriptor-return ABI even when the result isn't
635 /// ALLOCATABLE — e.g. `real(sp), dimension(size(x)) :: w` is rank 1.
636 pub result_rank: u8,
637 /// Per-entity array specification — the same value the AST carries
638 /// on `EntityDecl::array_spec` (or, when missing, derived from the
639 /// `dimension(...)` attribute). Empty when the symbol is scalar.
640 /// Sema populates this so consumers (notably SMP-body lowering)
641 /// can recover full shape metadata without re-walking the AST decls.
642 pub array_spec: Vec<ArraySpec>,
643 /// Subroutine/Function declared with `module` prefix inside a
644 /// submodule — the body of a separate module procedure declared
645 /// in the parent module's interface block. Codegen links these
646 /// under the parent module's name, not the submodule's, so call
647 /// sites match `_afs_modproc_<parent>_<proc>`.
648 pub is_separate_module_procedure: bool,
649 }
650
651 impl Default for SymbolAttrs {
652 fn default() -> Self {
653 Self {
654 access: Access::Default,
655 allocatable: false,
656 pointer: false,
657 binding_label: None,
658 procedure_iface: None,
659 target: false,
660 optional: false,
661 save: false,
662 parameter: false,
663 value: false,
664 intent: None,
665 external: false,
666 intrinsic: false,
667 pure: false,
668 elemental: false,
669 result_rank: 0,
670 array_spec: Vec::new(),
671 is_separate_module_procedure: false,
672 }
673 }
674 }
675
676 /// Accessibility level.
677 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
678 pub enum Access {
679 Public,
680 Private,
681 Default, // determined by module's default
682 }
683
684 /// Intent specification.
685 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
686 pub enum Intent {
687 In,
688 Out,
689 InOut,
690 }
691
692 /// USE association — links a local name to a symbol in another scope.
693 ///
694 /// `from_bare_use` distinguishes `use M` (true — full re-export visibility,
695 /// transitive lookup walks the source module's USE chain) from `use M, only:
696 /// x` (false — only the explicitly named symbols are visible). Without this
697 /// flag the transitive walk in `lookup_in_guarded` happily resolves any
698 /// generic interface in `M` even when only an unrelated name was imported,
699 /// silently merging foreign specifics into a same-named generic in the user
700 /// scope.
701 #[derive(Debug, Clone)]
702 pub struct UseAssociation {
703 pub local_name: String,
704 pub original_name: String,
705 pub source_scope: ScopeId,
706 pub is_submodule_access: bool,
707 pub from_bare_use: bool,
708 }
709
710 /// Implicit typing rules for a scope.
711 #[derive(Debug, Clone)]
712 pub struct ImplicitRules {
713 pub none_type: bool,
714 pub none_external: bool,
715 pub rules: HashMap<char, ImplicitType>,
716 }
717
718 impl ImplicitRules {
719 /// Standard Fortran default: I-N integer, everything else real.
720 pub fn default_fortran() -> Self {
721 let mut rules = HashMap::new();
722 for c in 'a'..='h' {
723 rules.insert(c, ImplicitType::Real);
724 }
725 for c in 'i'..='n' {
726 rules.insert(c, ImplicitType::Integer);
727 }
728 for c in 'o'..='z' {
729 rules.insert(c, ImplicitType::Real);
730 }
731 Self {
732 none_type: false,
733 none_external: false,
734 rules,
735 }
736 }
737
738 /// Look up the implicit type for a name's first letter.
739 pub fn type_for(&self, name: &str) -> Option<ImplicitType> {
740 if self.none_type {
741 return None;
742 }
743 let first = name.chars().next()?.to_ascii_lowercase();
744 self.rules.get(&first).copied()
745 }
746 }
747
748 /// Implicit type assignment.
749 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
750 pub enum ImplicitType {
751 Integer,
752 Real,
753 DoublePrecision,
754 Complex,
755 Logical,
756 Character,
757 }
758
759 /// Semantic analysis error.
760 #[derive(Debug, Clone)]
761 pub struct SemaError {
762 pub span: Span,
763 pub msg: String,
764 }
765
766 impl std::fmt::Display for SemaError {
767 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768 write!(
769 f,
770 "{}:{}: error: {}",
771 self.span.start.line, self.span.start.col, self.msg
772 )
773 }
774 }
775
776 impl std::error::Error for SemaError {}
777
778 #[cfg(test)]
779 mod tests {
780 use super::*;
781 use crate::lexer::{Position, Span};
782
783 fn dummy_span() -> Span {
784 Span {
785 file_id: 0,
786 start: Position { line: 1, col: 1 },
787 end: Position { line: 1, col: 1 },
788 }
789 }
790
791 fn make_symbol(name: &str, kind: SymbolKind) -> Symbol {
792 Symbol {
793 name: name.into(),
794 kind,
795 type_info: None,
796 attrs: SymbolAttrs::default(),
797 defined_at: dummy_span(),
798 scope: 0,
799 arg_names: vec![],
800 const_value: None,
801 }
802 }
803
804 // ---- Basic scope operations ----
805
806 #[test]
807 fn define_and_lookup() {
808 let mut st = SymbolTable::new();
809 st.push_scope(ScopeKind::Program("main".into()));
810 st.define(make_symbol("x", SymbolKind::Variable)).unwrap();
811 assert!(st.lookup("x").is_some());
812 assert!(st.lookup("X").is_some()); // case insensitive
813 assert!(st.lookup("y").is_none());
814 }
815
816 #[test]
817 fn duplicate_definition_errors() {
818 let mut st = SymbolTable::new();
819 st.push_scope(ScopeKind::Program("main".into()));
820 st.define(make_symbol("x", SymbolKind::Variable)).unwrap();
821 assert!(st.define(make_symbol("x", SymbolKind::Variable)).is_err());
822 assert!(st.define(make_symbol("X", SymbolKind::Variable)).is_err()); // case insensitive
823 }
824
825 #[test]
826 fn case_insensitive_lookup() {
827 let mut st = SymbolTable::new();
828 st.push_scope(ScopeKind::Program("main".into()));
829 st.define(make_symbol("MyVar", SymbolKind::Variable))
830 .unwrap();
831 assert!(st.lookup("myvar").is_some());
832 assert!(st.lookup("MYVAR").is_some());
833 assert!(st.lookup("MyVar").is_some());
834 }
835
836 // ---- Host association ----
837
838 #[test]
839 fn host_association() {
840 let mut st = SymbolTable::new();
841 st.push_scope(ScopeKind::Subroutine("outer".into()));
842 st.define(make_symbol("x", SymbolKind::Variable)).unwrap();
843 st.push_scope(ScopeKind::Subroutine("inner".into()));
844 // Inner sees outer's x via host association.
845 assert!(st.lookup("x").is_some());
846 }
847
848 #[test]
849 fn host_association_survives_private_symbol_seen_on_use_branch() {
850 let mut st = SymbolTable::new();
851
852 let host_scope = st.push_scope(ScopeKind::Module("host".into()));
853 let mut host_sym = make_symbol("color_red", SymbolKind::Parameter);
854 host_sym.attrs.access = Access::Private;
855 st.define(host_sym).unwrap();
856 st.pop_scope();
857
858 let imported_scope = st.push_scope(ScopeKind::Module("dep".into()));
859 // Model a pathological search branch where transitive USE walks through a
860 // scope whose parent is the eventual host scope. The private host symbol
861 // must not poison the later host-association search.
862 st.scope_mut(imported_scope).parent = Some(host_scope);
863 st.pop_scope();
864
865 st.push_scope(ScopeKind::Subroutine("inner".into()));
866 st.scope_mut(st.current_scope()).parent = Some(host_scope);
867 st.add_use_association(UseAssociation {
868 local_name: "dep_item".into(),
869 original_name: "dep_item".into(),
870 source_scope: imported_scope,
871 is_submodule_access: false,
872 from_bare_use: true,
873 });
874
875 assert!(
876 st.lookup("color_red").is_some(),
877 "host association should still find private host symbols even after a failed USE branch"
878 );
879 }
880
881 #[test]
882 fn local_shadows_host() {
883 let mut st = SymbolTable::new();
884 st.push_scope(ScopeKind::Subroutine("outer".into()));
885 let mut outer_sym = make_symbol("x", SymbolKind::Variable);
886 outer_sym.type_info = Some(TypeInfo::Integer { kind: None });
887 st.define(outer_sym).unwrap();
888
889 st.push_scope(ScopeKind::Subroutine("inner".into()));
890 let mut inner_sym = make_symbol("x", SymbolKind::Variable);
891 inner_sym.type_info = Some(TypeInfo::Real { kind: None });
892 st.define(inner_sym).unwrap();
893
894 // Inner's x shadows outer's x.
895 let found = st.lookup("x").unwrap();
896 assert!(matches!(found.type_info, Some(TypeInfo::Real { .. })));
897 }
898
899 // ---- USE association ----
900
901 #[test]
902 fn use_association() {
903 let mut st = SymbolTable::new();
904
905 // Create module scope with a public symbol.
906 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
907 st.define(make_symbol("foo", SymbolKind::Variable)).unwrap();
908 st.pop_scope();
909
910 // Create program scope that USEs the module.
911 st.push_scope(ScopeKind::Program("main".into()));
912 st.add_use_association(UseAssociation {
913 local_name: "foo".into(),
914 original_name: "foo".into(),
915 source_scope: mod_scope,
916 is_submodule_access: false,
917 from_bare_use: true,
918 });
919
920 assert!(st.lookup("foo").is_some());
921 }
922
923 #[test]
924 fn pending_access_applies_to_late_defined_symbol() {
925 let mut st = SymbolTable::new();
926 st.push_scope(ScopeKind::Module("m".into()));
927 st.set_default_access(Access::Private);
928 st.set_symbol_access("create_list", Access::Public);
929
930 let mut sym = make_symbol("create_list", SymbolKind::Function);
931 sym.attrs.access = st.default_access(st.current_scope());
932 st.define(sym).unwrap();
933
934 let found = st.lookup("create_list").unwrap();
935 assert_eq!(found.attrs.access, Access::Public);
936 }
937
938 #[test]
939 fn use_rename() {
940 let mut st = SymbolTable::new();
941
942 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
943 st.define(make_symbol("original_name", SymbolKind::Variable))
944 .unwrap();
945 st.pop_scope();
946
947 st.push_scope(ScopeKind::Program("main".into()));
948 st.add_use_association(UseAssociation {
949 local_name: "local_name".into(),
950 original_name: "original_name".into(),
951 source_scope: mod_scope,
952 is_submodule_access: false,
953 from_bare_use: true,
954 });
955
956 assert!(st.lookup("local_name").is_some());
957 assert!(st.lookup("original_name").is_none()); // not accessible by original name
958 }
959
960 #[test]
961 fn use_private_not_accessible() {
962 let mut st = SymbolTable::new();
963
964 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
965 let mut sym = make_symbol("hidden", SymbolKind::Variable);
966 sym.attrs.access = Access::Private;
967 st.define(sym).unwrap();
968 st.pop_scope();
969
970 st.push_scope(ScopeKind::Program("main".into()));
971 st.add_use_association(UseAssociation {
972 local_name: "hidden".into(),
973 original_name: "hidden".into(),
974 source_scope: mod_scope,
975 is_submodule_access: false,
976 from_bare_use: true,
977 });
978
979 assert!(st.lookup("hidden").is_none()); // private, not accessible
980 }
981
982 #[test]
983 fn local_shadows_use() {
984 let mut st = SymbolTable::new();
985
986 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
987 let mut mod_sym = make_symbol("x", SymbolKind::Variable);
988 mod_sym.type_info = Some(TypeInfo::Integer { kind: None });
989 st.define(mod_sym).unwrap();
990 st.pop_scope();
991
992 st.push_scope(ScopeKind::Program("main".into()));
993 st.add_use_association(UseAssociation {
994 local_name: "x".into(),
995 original_name: "x".into(),
996 source_scope: mod_scope,
997 is_submodule_access: false,
998 from_bare_use: true,
999 });
1000 let mut local_sym = make_symbol("x", SymbolKind::Variable);
1001 local_sym.type_info = Some(TypeInfo::Real { kind: None });
1002 st.define(local_sym).unwrap();
1003
1004 // Local shadows USE.
1005 let found = st.lookup("x").unwrap();
1006 assert!(matches!(found.type_info, Some(TypeInfo::Real { .. })));
1007 }
1008
1009 // ---- Implicit typing ----
1010
1011 #[test]
1012 fn implicit_default_rules() {
1013 let st = SymbolTable::new();
1014 // i-n → integer.
1015 assert_eq!(
1016 st.scopes[0].implicit_rules.type_for("index"),
1017 Some(ImplicitType::Integer)
1018 );
1019 assert_eq!(
1020 st.scopes[0].implicit_rules.type_for("jmax"),
1021 Some(ImplicitType::Integer)
1022 );
1023 // a-h, o-z → real.
1024 assert_eq!(
1025 st.scopes[0].implicit_rules.type_for("x"),
1026 Some(ImplicitType::Real)
1027 );
1028 assert_eq!(
1029 st.scopes[0].implicit_rules.type_for("alpha"),
1030 Some(ImplicitType::Real)
1031 );
1032 }
1033
1034 #[test]
1035 fn implicit_none_disables() {
1036 let mut st = SymbolTable::new();
1037 st.push_scope(ScopeKind::Program("main".into()));
1038 st.set_implicit_none(true, false);
1039 assert_eq!(st.implicit_type("x"), None);
1040 assert_eq!(st.implicit_type("index"), None);
1041 }
1042
1043 #[test]
1044 fn implicit_custom_rules() {
1045 let mut st = SymbolTable::new();
1046 st.push_scope(ScopeKind::Program("main".into()));
1047 st.set_implicit_rule('a', 'z', ImplicitType::DoublePrecision);
1048 assert_eq!(st.implicit_type("x"), Some(ImplicitType::DoublePrecision));
1049 assert_eq!(
1050 st.implicit_type("index"),
1051 Some(ImplicitType::DoublePrecision)
1052 );
1053 }
1054
1055 // ---- Module scope finding ----
1056
1057 #[test]
1058 fn find_module_scope() {
1059 let mut st = SymbolTable::new();
1060 let mod_id = st.push_scope(ScopeKind::Module("my_module".into()));
1061 st.pop_scope();
1062 assert_eq!(st.find_module_scope("my_module"), Some(mod_id));
1063 assert_eq!(st.find_module_scope("MY_MODULE"), Some(mod_id)); // case insensitive
1064 assert_eq!(st.find_module_scope("other"), None);
1065 }
1066
1067 // ---- Scope hierarchy ----
1068
1069 #[test]
1070 fn scope_push_pop() {
1071 let mut st = SymbolTable::new();
1072 assert_eq!(st.current_scope(), 0); // global
1073 let s1 = st.push_scope(ScopeKind::Module("m".into()));
1074 assert_eq!(st.current_scope(), s1);
1075 let s2 = st.push_scope(ScopeKind::Subroutine("sub".into()));
1076 assert_eq!(st.current_scope(), s2);
1077 st.pop_scope();
1078 assert_eq!(st.current_scope(), s1);
1079 st.pop_scope();
1080 assert_eq!(st.current_scope(), 0);
1081 }
1082
1083 // ---- Default access ----
1084
1085 #[test]
1086 fn module_default_access() {
1087 let mut st = SymbolTable::new();
1088 st.push_scope(ScopeKind::Module("m".into()));
1089 assert_eq!(st.default_access(st.current_scope()), Access::Public);
1090 st.set_default_access(Access::Private);
1091 assert_eq!(st.default_access(st.current_scope()), Access::Private);
1092 }
1093 }
1094