Rust · 28810 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::lexer::Span;
8 use std::collections::HashMap;
9
10 /// Scope identifier — an index into the SymbolTable's scope list.
11 pub type ScopeId = usize;
12
13 /// The symbol table — manages all scopes in a compilation.
14 #[derive(Debug)]
15 pub struct SymbolTable {
16 pub(crate) scopes: Vec<Scope>,
17 pub(crate) current: ScopeId,
18 }
19
20 impl SymbolTable {
21 pub fn new() -> Self {
22 let global = Scope {
23 id: 0,
24 parent: None,
25 kind: ScopeKind::Global,
26 symbols: HashMap::new(),
27 implicit_rules: ImplicitRules::default_fortran(),
28 use_associations: Vec::new(),
29 default_access: Access::Public,
30 pending_access: HashMap::new(),
31 arg_order: Vec::new(),
32 };
33 Self {
34 scopes: vec![global],
35 current: 0,
36 }
37 }
38 }
39
40 impl Default for SymbolTable {
41 fn default() -> Self {
42 Self::new()
43 }
44 }
45
46 impl SymbolTable {
47 /// Create a new child scope of the current scope.
48 pub fn push_scope(&mut self, kind: ScopeKind) -> ScopeId {
49 let id = self.scopes.len();
50 let parent_implicit = self.scopes[self.current].implicit_rules.clone();
51 let scope = Scope {
52 id,
53 parent: Some(self.current),
54 kind,
55 symbols: HashMap::new(),
56 implicit_rules: parent_implicit, // inherit from parent, may be overridden
57 use_associations: Vec::new(),
58 default_access: Access::Public,
59 pending_access: HashMap::new(),
60 arg_order: Vec::new(),
61 };
62 self.scopes.push(scope);
63 self.current = id;
64 id
65 }
66
67 /// Enter an existing scope by ID without creating a new one.
68 /// Returns the previous scope ID for later restoration.
69 pub fn enter_scope(&mut self, id: ScopeId) -> ScopeId {
70 let saved = self.current;
71 self.current = id;
72 saved
73 }
74
75 /// Return to the parent scope.
76 pub fn pop_scope(&mut self) {
77 if let Some(parent) = self.scopes[self.current].parent {
78 self.current = parent;
79 }
80 }
81
82 /// Get the current scope ID.
83 pub fn current_scope(&self) -> ScopeId {
84 self.current
85 }
86
87 /// Get a scope by ID.
88 pub fn scope(&self, id: ScopeId) -> &Scope {
89 &self.scopes[id]
90 }
91
92 /// Get a mutable scope by ID.
93 pub fn scope_mut(&mut self, id: ScopeId) -> &mut Scope {
94 &mut self.scopes[id]
95 }
96
97 /// Define a symbol in the current scope.
98 pub fn define(&mut self, symbol: Symbol) -> Result<(), SemaError> {
99 let key = symbol.name.to_lowercase();
100 let scope = &mut self.scopes[self.current];
101 if scope.symbols.contains_key(&key) {
102 return Err(SemaError {
103 span: symbol.defined_at,
104 msg: format!("symbol '{}' already defined in this scope", symbol.name),
105 });
106 }
107 let mut symbol = symbol;
108 if let Some(access) = scope.pending_access.get(&key).copied() {
109 symbol.attrs.access = access;
110 }
111 scope.symbols.insert(key, symbol);
112 Ok(())
113 }
114
115 /// Define a symbol in a specific scope.
116 pub fn define_in(&mut self, scope_id: ScopeId, symbol: Symbol) -> Result<(), SemaError> {
117 let key = symbol.name.to_lowercase();
118 let scope = &mut self.scopes[scope_id];
119 if scope.symbols.contains_key(&key) {
120 return Err(SemaError {
121 span: symbol.defined_at,
122 msg: format!("symbol '{}' already defined in this scope", symbol.name),
123 });
124 }
125 let mut symbol = symbol;
126 if let Some(access) = scope.pending_access.get(&key).copied() {
127 symbol.attrs.access = access;
128 }
129 scope.symbols.insert(key, symbol);
130 Ok(())
131 }
132
133 /// Look up a name in the current scope with Fortran resolution order:
134 /// Local > USE association > Host association > Implicit typing
135 pub fn lookup(&self, name: &str) -> Option<&Symbol> {
136 self.lookup_in(self.current, name)
137 }
138
139 /// Look up a name starting from a specific scope.
140 pub fn lookup_in(&self, scope_id: ScopeId, name: &str) -> Option<&Symbol> {
141 let key = name.to_ascii_lowercase();
142 let mut visited = Vec::new();
143 self.lookup_in_guarded(scope_id, &key, &mut visited)
144 }
145
146 fn lookup_in_guarded(
147 &self,
148 scope_id: ScopeId,
149 key: &str,
150 visited: &mut Vec<ScopeId>,
151 ) -> Option<&Symbol> {
152 if visited.contains(&scope_id) {
153 return None;
154 }
155 visited.push(scope_id);
156
157 let scope = &self.scopes[scope_id];
158
159 let result = (|| {
160 // 1. Local declaration.
161 if let Some(sym) = scope.symbols.get(key) {
162 return Some(sym);
163 }
164
165 // 2. Direct USE association.
166 for assoc in &scope.use_associations {
167 if assoc.local_name == key {
168 if let Some(sym) = self.scopes[assoc.source_scope]
169 .symbols
170 .get(&assoc.original_name)
171 {
172 if sym.attrs.access != Access::Private || assoc.is_submodule_access {
173 return Some(sym);
174 }
175 }
176 }
177 }
178
179 // 2b. Transitive USE: look through each USE'd module's own
180 // public symbols and its transitive USE chain. Only applies
181 // to bare `USE M` (local_name == original_name); renamed
182 // USE associations are intentional restrictions.
183 let mut seen_use_scopes = Vec::new();
184 for assoc in &scope.use_associations {
185 if assoc.local_name != assoc.original_name {
186 continue;
187 }
188 if seen_use_scopes.contains(&assoc.source_scope) {
189 continue;
190 }
191 seen_use_scopes.push(assoc.source_scope);
192 if let Some(sym) = self.lookup_in_guarded(assoc.source_scope, key, visited) {
193 if sym.attrs.access != Access::Private {
194 return Some(sym);
195 }
196 }
197 }
198
199 // 3. Host association — look in parent scope.
200 if let Some(parent) = scope.parent {
201 if self.scopes[parent].kind != ScopeKind::Global {
202 return self.lookup_in_guarded(parent, key, visited);
203 }
204 }
205
206 None
207 })();
208
209 visited.pop();
210 result
211 }
212
213 /// Search ALL scopes for a symbol by name.
214 /// Used during lowering when the current scope may not be set correctly.
215 /// Prefers parameter symbols (for kind resolution) but returns any match.
216 pub fn find_symbol_any_scope(&self, name: &str) -> Option<&Symbol> {
217 let key = name.to_ascii_lowercase();
218 let mut fallback: Option<&Symbol> = None;
219 for scope in &self.scopes {
220 if let Some(sym) = scope.symbols.get(&key) {
221 if sym.attrs.parameter {
222 return Some(sym);
223 }
224 if fallback.is_none() {
225 fallback = Some(sym);
226 }
227 }
228 }
229 if fallback.is_some() {
230 return fallback;
231 }
232 // Second pass: resolve USE renames. `use m, only: a => add`
233 // installs a UseAssociation with local_name="a" and
234 // original_name="add" but no symbol named "a" on the
235 // enclosing scope. Direct-symbol scans miss the rename; walk
236 // every scope's UseAssociations and follow the source to pick
237 // up the underlying symbol (NamedInterface for generic
238 // dispatch, Function for ordinary calls, etc.).
239 for scope in &self.scopes {
240 for assoc in &scope.use_associations {
241 if assoc.local_name == key {
242 if let Some(sym) = self.scopes[assoc.source_scope]
243 .symbols
244 .get(&assoc.original_name)
245 {
246 return Some(sym);
247 }
248 }
249 }
250 }
251 None
252 }
253
254 /// Check if a name would be implicitly typed in the current scope.
255 /// Returns the implicit type if applicable, or None if implicit none.
256 pub fn implicit_type(&self, name: &str) -> Option<ImplicitType> {
257 let scope = &self.scopes[self.current];
258 scope.implicit_rules.type_for(name)
259 }
260
261 /// Set implicit none for the current scope.
262 pub fn set_implicit_none(&mut self, type_: bool, external: bool) {
263 let scope = &mut self.scopes[self.current];
264 if type_ {
265 scope.implicit_rules.none_type = true;
266 }
267 if external {
268 scope.implicit_rules.none_external = true;
269 }
270 }
271
272 /// Force IMPLICIT NONE on every program-unit-level scope in the
273 /// table (Program, Module, Submodule, Subroutine, Function,
274 /// BlockData). Used by the driver's `-fimplicit-none` flag,
275 /// which mirrors the gfortran option of the same name and tells
276 /// validate.rs to flag every undeclared name even in scopes that
277 /// don't have an explicit `implicit none` statement.
278 pub fn force_implicit_none_all_units(&mut self) {
279 for scope in &mut self.scopes {
280 if matches!(
281 scope.kind,
282 ScopeKind::Program(_)
283 | ScopeKind::Module(_)
284 | ScopeKind::Submodule(_)
285 | ScopeKind::Subroutine(_)
286 | ScopeKind::Function(_)
287 ) {
288 scope.implicit_rules.none_type = true;
289 }
290 }
291 }
292
293 /// Set an implicit typing rule for the current scope.
294 pub fn set_implicit_rule(&mut self, start: char, end: char, itype: ImplicitType) {
295 let scope = &mut self.scopes[self.current];
296 for c in start..=end {
297 scope
298 .implicit_rules
299 .rules
300 .insert(c.to_ascii_lowercase(), itype);
301 }
302 }
303
304 /// Add a USE association to the current scope.
305 pub fn add_use_association(&mut self, assoc: UseAssociation) {
306 let assoc = UseAssociation {
307 local_name: assoc.local_name.to_ascii_lowercase(),
308 original_name: assoc.original_name.to_ascii_lowercase(),
309 source_scope: assoc.source_scope,
310 is_submodule_access: assoc.is_submodule_access,
311 };
312 self.scopes[self.current].use_associations.push(assoc);
313 }
314
315 /// Set the default accessibility for the current scope.
316 pub fn set_default_access(&mut self, access: Access) {
317 self.scopes[self.current].default_access = access;
318 }
319
320 /// Set the access level on a specific symbol in the current scope.
321 /// Used for `PUBLIC :: name` and `PRIVATE :: name` statements.
322 pub fn set_symbol_access(&mut self, name: &str, access: Access) {
323 let key = name.to_lowercase();
324 self.scopes[self.current]
325 .pending_access
326 .insert(key.clone(), access);
327 if let Some(sym) = self.scopes[self.current].symbols.get_mut(&key) {
328 sym.attrs.access = access;
329 }
330 }
331
332 /// Iterate all scopes (for generic interface resolution during lowering).
333 pub fn all_scopes(&self) -> &[Scope] {
334 &self.scopes
335 }
336
337 /// Check whether implicit none (type) is active in a scope.
338 pub fn is_implicit_none(&self, scope_id: ScopeId) -> bool {
339 self.scopes[scope_id].implicit_rules.none_type
340 }
341
342 /// Get the default accessibility for a scope.
343 pub fn default_access(&self, scope_id: ScopeId) -> Access {
344 self.scopes[scope_id].default_access
345 }
346
347 /// Find a module scope by name (for USE resolution within the same file).
348 pub fn find_module_scope(&self, name: &str) -> Option<ScopeId> {
349 let key = name.to_lowercase();
350 self.scopes.iter().find_map(|s| {
351 if let ScopeKind::Module(ref n) = s.kind {
352 if n.to_lowercase() == key {
353 Some(s.id)
354 } else {
355 None
356 }
357 } else {
358 None
359 }
360 })
361 }
362 }
363
364 /// A scope in the symbol table.
365 #[derive(Debug)]
366 pub struct Scope {
367 pub id: ScopeId,
368 pub parent: Option<ScopeId>,
369 pub kind: ScopeKind,
370 pub symbols: HashMap<String, Symbol>,
371 pub implicit_rules: ImplicitRules,
372 pub use_associations: Vec<UseAssociation>,
373 pub default_access: Access,
374 pub pending_access: HashMap<String, Access>,
375 /// Ordered dummy argument names (for function/subroutine scopes).
376 pub arg_order: Vec<String>,
377 }
378
379 /// What kind of scope this is.
380 #[derive(Debug, Clone, PartialEq, Eq)]
381 pub enum ScopeKind {
382 Global,
383 Module(String),
384 Submodule(String),
385 Program(String),
386 Subroutine(String),
387 Function(String),
388 Block,
389 Interface,
390 DerivedType(String),
391 Forall,
392 Associate,
393 Critical,
394 }
395
396 /// A symbol — a named entity in a scope.
397 #[derive(Debug, Clone)]
398 pub struct Symbol {
399 pub name: String,
400 pub kind: SymbolKind,
401 pub type_info: Option<TypeInfo>,
402 pub attrs: SymbolAttrs,
403 pub defined_at: Span,
404 pub scope: ScopeId,
405 /// Ordered dummy argument names (for functions/subroutines).
406 pub arg_names: Vec<String>,
407 /// Compile-time constant value (for PARAMETERs like c_int=4).
408 pub const_value: Option<i64>,
409 }
410
411 /// What kind of entity this symbol represents.
412 #[derive(Debug, Clone, PartialEq, Eq)]
413 pub enum SymbolKind {
414 Variable,
415 Parameter,
416 Function,
417 Subroutine,
418 Module,
419 DerivedType,
420 NamedInterface,
421 Enumerator,
422 Namelist,
423 CommonBlock,
424 ExternalProc,
425 IntrinsicProc,
426 ProcedurePointer,
427 Label(u64),
428 }
429
430 /// Type information for a symbol.
431 #[derive(Debug, Clone, PartialEq)]
432 pub enum TypeInfo {
433 Integer { kind: Option<u8> },
434 Real { kind: Option<u8> },
435 DoublePrecision,
436 Complex { kind: Option<u8> },
437 Logical { kind: Option<u8> },
438 Character { len: Option<i64>, kind: Option<u8> },
439 Derived(String),
440 Class(String),
441 ClassStar,
442 TypeStar,
443 }
444
445 /// Symbol attributes.
446 #[derive(Debug, Clone)]
447 pub struct SymbolAttrs {
448 pub access: Access,
449 pub allocatable: bool,
450 pub pointer: bool,
451 /// For BIND(C, NAME="...") procedures, preserve the actual link
452 /// symbol so lowering can call the declared external name rather
453 /// than the local Fortran alias.
454 pub binding_label: Option<String>,
455 /// For `procedure(iface), pointer :: p`, preserve the declared
456 /// interface name so `.amod` can round-trip the symbol truthfully.
457 pub procedure_iface: Option<String>,
458 pub target: bool,
459 pub optional: bool,
460 pub save: bool,
461 pub parameter: bool,
462 pub value: bool,
463 pub intent: Option<Intent>,
464 pub external: bool,
465 pub intrinsic: bool,
466 /// Procedure declared with the PURE prefix.
467 pub pure: bool,
468 /// Procedure declared with the ELEMENTAL prefix.
469 pub elemental: bool,
470 }
471
472 impl Default for SymbolAttrs {
473 fn default() -> Self {
474 Self {
475 access: Access::Default,
476 allocatable: false,
477 pointer: false,
478 binding_label: None,
479 procedure_iface: None,
480 target: false,
481 optional: false,
482 save: false,
483 parameter: false,
484 value: false,
485 intent: None,
486 external: false,
487 intrinsic: false,
488 pure: false,
489 elemental: false,
490 }
491 }
492 }
493
494 /// Accessibility level.
495 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
496 pub enum Access {
497 Public,
498 Private,
499 Default, // determined by module's default
500 }
501
502 /// Intent specification.
503 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
504 pub enum Intent {
505 In,
506 Out,
507 InOut,
508 }
509
510 /// USE association — links a local name to a symbol in another scope.
511 #[derive(Debug, Clone)]
512 pub struct UseAssociation {
513 pub local_name: String,
514 pub original_name: String,
515 pub source_scope: ScopeId,
516 pub is_submodule_access: bool,
517 }
518
519 /// Implicit typing rules for a scope.
520 #[derive(Debug, Clone)]
521 pub struct ImplicitRules {
522 pub none_type: bool,
523 pub none_external: bool,
524 pub rules: HashMap<char, ImplicitType>,
525 }
526
527 impl ImplicitRules {
528 /// Standard Fortran default: I-N integer, everything else real.
529 pub fn default_fortran() -> Self {
530 let mut rules = HashMap::new();
531 for c in 'a'..='h' {
532 rules.insert(c, ImplicitType::Real);
533 }
534 for c in 'i'..='n' {
535 rules.insert(c, ImplicitType::Integer);
536 }
537 for c in 'o'..='z' {
538 rules.insert(c, ImplicitType::Real);
539 }
540 Self {
541 none_type: false,
542 none_external: false,
543 rules,
544 }
545 }
546
547 /// Look up the implicit type for a name's first letter.
548 pub fn type_for(&self, name: &str) -> Option<ImplicitType> {
549 if self.none_type {
550 return None;
551 }
552 let first = name.chars().next()?.to_ascii_lowercase();
553 self.rules.get(&first).copied()
554 }
555 }
556
557 /// Implicit type assignment.
558 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
559 pub enum ImplicitType {
560 Integer,
561 Real,
562 DoublePrecision,
563 Complex,
564 Logical,
565 Character,
566 }
567
568 /// Semantic analysis error.
569 #[derive(Debug, Clone)]
570 pub struct SemaError {
571 pub span: Span,
572 pub msg: String,
573 }
574
575 impl std::fmt::Display for SemaError {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 write!(
578 f,
579 "{}:{}: error: {}",
580 self.span.start.line, self.span.start.col, self.msg
581 )
582 }
583 }
584
585 impl std::error::Error for SemaError {}
586
587 #[cfg(test)]
588 mod tests {
589 use super::*;
590 use crate::lexer::{Position, Span};
591
592 fn dummy_span() -> Span {
593 Span {
594 file_id: 0,
595 start: Position { line: 1, col: 1 },
596 end: Position { line: 1, col: 1 },
597 }
598 }
599
600 fn make_symbol(name: &str, kind: SymbolKind) -> Symbol {
601 Symbol {
602 name: name.into(),
603 kind,
604 type_info: None,
605 attrs: SymbolAttrs::default(),
606 defined_at: dummy_span(),
607 scope: 0,
608 arg_names: vec![],
609 const_value: None,
610 }
611 }
612
613 // ---- Basic scope operations ----
614
615 #[test]
616 fn define_and_lookup() {
617 let mut st = SymbolTable::new();
618 st.push_scope(ScopeKind::Program("main".into()));
619 st.define(make_symbol("x", SymbolKind::Variable)).unwrap();
620 assert!(st.lookup("x").is_some());
621 assert!(st.lookup("X").is_some()); // case insensitive
622 assert!(st.lookup("y").is_none());
623 }
624
625 #[test]
626 fn duplicate_definition_errors() {
627 let mut st = SymbolTable::new();
628 st.push_scope(ScopeKind::Program("main".into()));
629 st.define(make_symbol("x", SymbolKind::Variable)).unwrap();
630 assert!(st.define(make_symbol("x", SymbolKind::Variable)).is_err());
631 assert!(st.define(make_symbol("X", SymbolKind::Variable)).is_err()); // case insensitive
632 }
633
634 #[test]
635 fn case_insensitive_lookup() {
636 let mut st = SymbolTable::new();
637 st.push_scope(ScopeKind::Program("main".into()));
638 st.define(make_symbol("MyVar", SymbolKind::Variable))
639 .unwrap();
640 assert!(st.lookup("myvar").is_some());
641 assert!(st.lookup("MYVAR").is_some());
642 assert!(st.lookup("MyVar").is_some());
643 }
644
645 // ---- Host association ----
646
647 #[test]
648 fn host_association() {
649 let mut st = SymbolTable::new();
650 st.push_scope(ScopeKind::Subroutine("outer".into()));
651 st.define(make_symbol("x", SymbolKind::Variable)).unwrap();
652 st.push_scope(ScopeKind::Subroutine("inner".into()));
653 // Inner sees outer's x via host association.
654 assert!(st.lookup("x").is_some());
655 }
656
657 #[test]
658 fn host_association_survives_private_symbol_seen_on_use_branch() {
659 let mut st = SymbolTable::new();
660
661 let host_scope = st.push_scope(ScopeKind::Module("host".into()));
662 let mut host_sym = make_symbol("color_red", SymbolKind::Parameter);
663 host_sym.attrs.access = Access::Private;
664 st.define(host_sym).unwrap();
665 st.pop_scope();
666
667 let imported_scope = st.push_scope(ScopeKind::Module("dep".into()));
668 // Model a pathological search branch where transitive USE walks through a
669 // scope whose parent is the eventual host scope. The private host symbol
670 // must not poison the later host-association search.
671 st.scope_mut(imported_scope).parent = Some(host_scope);
672 st.pop_scope();
673
674 st.push_scope(ScopeKind::Subroutine("inner".into()));
675 st.scope_mut(st.current_scope()).parent = Some(host_scope);
676 st.add_use_association(UseAssociation {
677 local_name: "dep_item".into(),
678 original_name: "dep_item".into(),
679 source_scope: imported_scope,
680 is_submodule_access: false,
681 });
682
683 assert!(
684 st.lookup("color_red").is_some(),
685 "host association should still find private host symbols even after a failed USE branch"
686 );
687 }
688
689 #[test]
690 fn local_shadows_host() {
691 let mut st = SymbolTable::new();
692 st.push_scope(ScopeKind::Subroutine("outer".into()));
693 let mut outer_sym = make_symbol("x", SymbolKind::Variable);
694 outer_sym.type_info = Some(TypeInfo::Integer { kind: None });
695 st.define(outer_sym).unwrap();
696
697 st.push_scope(ScopeKind::Subroutine("inner".into()));
698 let mut inner_sym = make_symbol("x", SymbolKind::Variable);
699 inner_sym.type_info = Some(TypeInfo::Real { kind: None });
700 st.define(inner_sym).unwrap();
701
702 // Inner's x shadows outer's x.
703 let found = st.lookup("x").unwrap();
704 assert!(matches!(found.type_info, Some(TypeInfo::Real { .. })));
705 }
706
707 // ---- USE association ----
708
709 #[test]
710 fn use_association() {
711 let mut st = SymbolTable::new();
712
713 // Create module scope with a public symbol.
714 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
715 st.define(make_symbol("foo", SymbolKind::Variable)).unwrap();
716 st.pop_scope();
717
718 // Create program scope that USEs the module.
719 st.push_scope(ScopeKind::Program("main".into()));
720 st.add_use_association(UseAssociation {
721 local_name: "foo".into(),
722 original_name: "foo".into(),
723 source_scope: mod_scope,
724 is_submodule_access: false,
725 });
726
727 assert!(st.lookup("foo").is_some());
728 }
729
730 #[test]
731 fn pending_access_applies_to_late_defined_symbol() {
732 let mut st = SymbolTable::new();
733 st.push_scope(ScopeKind::Module("m".into()));
734 st.set_default_access(Access::Private);
735 st.set_symbol_access("create_list", Access::Public);
736
737 let mut sym = make_symbol("create_list", SymbolKind::Function);
738 sym.attrs.access = st.default_access(st.current_scope());
739 st.define(sym).unwrap();
740
741 let found = st.lookup("create_list").unwrap();
742 assert_eq!(found.attrs.access, Access::Public);
743 }
744
745 #[test]
746 fn use_rename() {
747 let mut st = SymbolTable::new();
748
749 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
750 st.define(make_symbol("original_name", SymbolKind::Variable))
751 .unwrap();
752 st.pop_scope();
753
754 st.push_scope(ScopeKind::Program("main".into()));
755 st.add_use_association(UseAssociation {
756 local_name: "local_name".into(),
757 original_name: "original_name".into(),
758 source_scope: mod_scope,
759 is_submodule_access: false,
760 });
761
762 assert!(st.lookup("local_name").is_some());
763 assert!(st.lookup("original_name").is_none()); // not accessible by original name
764 }
765
766 #[test]
767 fn use_private_not_accessible() {
768 let mut st = SymbolTable::new();
769
770 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
771 let mut sym = make_symbol("hidden", SymbolKind::Variable);
772 sym.attrs.access = Access::Private;
773 st.define(sym).unwrap();
774 st.pop_scope();
775
776 st.push_scope(ScopeKind::Program("main".into()));
777 st.add_use_association(UseAssociation {
778 local_name: "hidden".into(),
779 original_name: "hidden".into(),
780 source_scope: mod_scope,
781 is_submodule_access: false,
782 });
783
784 assert!(st.lookup("hidden").is_none()); // private, not accessible
785 }
786
787 #[test]
788 fn local_shadows_use() {
789 let mut st = SymbolTable::new();
790
791 let mod_scope = st.push_scope(ScopeKind::Module("mymod".into()));
792 let mut mod_sym = make_symbol("x", SymbolKind::Variable);
793 mod_sym.type_info = Some(TypeInfo::Integer { kind: None });
794 st.define(mod_sym).unwrap();
795 st.pop_scope();
796
797 st.push_scope(ScopeKind::Program("main".into()));
798 st.add_use_association(UseAssociation {
799 local_name: "x".into(),
800 original_name: "x".into(),
801 source_scope: mod_scope,
802 is_submodule_access: false,
803 });
804 let mut local_sym = make_symbol("x", SymbolKind::Variable);
805 local_sym.type_info = Some(TypeInfo::Real { kind: None });
806 st.define(local_sym).unwrap();
807
808 // Local shadows USE.
809 let found = st.lookup("x").unwrap();
810 assert!(matches!(found.type_info, Some(TypeInfo::Real { .. })));
811 }
812
813 // ---- Implicit typing ----
814
815 #[test]
816 fn implicit_default_rules() {
817 let st = SymbolTable::new();
818 // i-n → integer.
819 assert_eq!(
820 st.scopes[0].implicit_rules.type_for("index"),
821 Some(ImplicitType::Integer)
822 );
823 assert_eq!(
824 st.scopes[0].implicit_rules.type_for("jmax"),
825 Some(ImplicitType::Integer)
826 );
827 // a-h, o-z → real.
828 assert_eq!(
829 st.scopes[0].implicit_rules.type_for("x"),
830 Some(ImplicitType::Real)
831 );
832 assert_eq!(
833 st.scopes[0].implicit_rules.type_for("alpha"),
834 Some(ImplicitType::Real)
835 );
836 }
837
838 #[test]
839 fn implicit_none_disables() {
840 let mut st = SymbolTable::new();
841 st.push_scope(ScopeKind::Program("main".into()));
842 st.set_implicit_none(true, false);
843 assert_eq!(st.implicit_type("x"), None);
844 assert_eq!(st.implicit_type("index"), None);
845 }
846
847 #[test]
848 fn implicit_custom_rules() {
849 let mut st = SymbolTable::new();
850 st.push_scope(ScopeKind::Program("main".into()));
851 st.set_implicit_rule('a', 'z', ImplicitType::DoublePrecision);
852 assert_eq!(st.implicit_type("x"), Some(ImplicitType::DoublePrecision));
853 assert_eq!(
854 st.implicit_type("index"),
855 Some(ImplicitType::DoublePrecision)
856 );
857 }
858
859 // ---- Module scope finding ----
860
861 #[test]
862 fn find_module_scope() {
863 let mut st = SymbolTable::new();
864 let mod_id = st.push_scope(ScopeKind::Module("my_module".into()));
865 st.pop_scope();
866 assert_eq!(st.find_module_scope("my_module"), Some(mod_id));
867 assert_eq!(st.find_module_scope("MY_MODULE"), Some(mod_id)); // case insensitive
868 assert_eq!(st.find_module_scope("other"), None);
869 }
870
871 // ---- Scope hierarchy ----
872
873 #[test]
874 fn scope_push_pop() {
875 let mut st = SymbolTable::new();
876 assert_eq!(st.current_scope(), 0); // global
877 let s1 = st.push_scope(ScopeKind::Module("m".into()));
878 assert_eq!(st.current_scope(), s1);
879 let s2 = st.push_scope(ScopeKind::Subroutine("sub".into()));
880 assert_eq!(st.current_scope(), s2);
881 st.pop_scope();
882 assert_eq!(st.current_scope(), s1);
883 st.pop_scope();
884 assert_eq!(st.current_scope(), 0);
885 }
886
887 // ---- Default access ----
888
889 #[test]
890 fn module_default_access() {
891 let mut st = SymbolTable::new();
892 st.push_scope(ScopeKind::Module("m".into()));
893 assert_eq!(st.default_access(st.current_scope()), Access::Public);
894 st.set_default_access(Access::Private);
895 assert_eq!(st.default_access(st.current_scope()), Access::Private);
896 }
897 }
898