Rust · 94987 bytes Raw Blame History
1 //! Semantic validation — checks that go beyond type checking.
2 //!
3 //! Allocatable/pointer semantics, intent enforcement, pure/elemental
4 //! constraints, label validation, and standard conformance. Runs after
5 //! symbol resolution (resolve.rs) and type checking (types.rs).
6
7 use super::symtab::*;
8 use crate::ast::decl::{Attribute, Decl, TypeAttr, TypeSpec};
9 use crate::ast::expr::Expr;
10 use crate::ast::stmt::*;
11 use crate::ast::unit::*;
12 use crate::lexer::Span;
13 use std::cell::RefCell;
14
15 /// Fortran standard level for --std= conformance checking.
16 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17 pub enum FortranStandard {
18 F77,
19 F90,
20 F95,
21 F2003,
22 F2008,
23 F2018,
24 F2023,
25 }
26
27 impl FortranStandard {
28 pub fn parse_flag(s: &str) -> Option<Self> {
29 match s.to_lowercase().as_str() {
30 "f77" | "fortran77" => Some(Self::F77),
31 "f90" | "fortran90" => Some(Self::F90),
32 "f95" | "fortran95" => Some(Self::F95),
33 "f2003" | "fortran2003" => Some(Self::F2003),
34 "f2008" | "fortran2008" => Some(Self::F2008),
35 "f2018" | "fortran2018" => Some(Self::F2018),
36 "f2023" | "fortran2023" => Some(Self::F2023),
37 _ => None,
38 }
39 }
40 }
41
42 /// A diagnostic produced by validation.
43 #[derive(Debug, Clone)]
44 pub struct Diagnostic {
45 pub span: Span,
46 pub kind: DiagKind,
47 pub msg: String,
48 }
49
50 /// Diagnostic severity.
51 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
52 pub enum DiagKind {
53 Error,
54 Warning,
55 }
56
57 impl std::fmt::Display for Diagnostic {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 let label = match self.kind {
60 DiagKind::Error => "error",
61 DiagKind::Warning => "warning",
62 };
63 write!(
64 f,
65 "{}:{}: {}: {}",
66 self.span.start.line, self.span.start.col, label, self.msg
67 )
68 }
69 }
70
71 /// Validation context — accumulates diagnostics while walking the AST.
72 struct Ctx<'a> {
73 st: &'a SymbolTable,
74 diags: Vec<Diagnostic>,
75 /// Current scope ID for symbol lookups.
76 scope_id: ScopeId,
77 /// Are we inside a pure procedure?
78 in_pure: bool,
79 /// Are we inside an elemental procedure?
80 in_elemental: bool,
81 /// Target standard for conformance checking (None = allow everything).
82 std: Option<FortranStandard>,
83 /// Labels defined in the current scope.
84 labels_defined: Vec<u64>,
85 /// Labels referenced (GOTO targets) in the current scope.
86 labels_referenced: Vec<(u64, Span)>,
87 /// Derived-type layouts — consulted when validating attribute-
88 /// sensitive targets on a component access (`obj%field`), where
89 /// the base variable's attributes aren't the right thing to check.
90 type_layouts: Option<&'a crate::sema::type_layout::TypeLayoutRegistry>,
91 lookup_cache: RefCell<std::collections::HashMap<(ScopeId, String), Option<&'a Symbol>>>,
92 warn_pedantic: bool,
93 warn_deprecated: bool,
94 }
95
96 impl<'a> Ctx<'a> {
97 fn new(
98 st: &'a SymbolTable,
99 std: Option<FortranStandard>,
100 warn_pedantic: bool,
101 warn_deprecated: bool,
102 ) -> Self {
103 Self {
104 st,
105 diags: Vec::new(),
106 scope_id: 0,
107 in_pure: false,
108 in_elemental: false,
109 std,
110 labels_defined: Vec::new(),
111 labels_referenced: Vec::new(),
112 type_layouts: None,
113 lookup_cache: RefCell::new(std::collections::HashMap::new()),
114 warn_pedantic,
115 warn_deprecated,
116 }
117 }
118
119 fn new_with_layouts(
120 st: &'a SymbolTable,
121 std: Option<FortranStandard>,
122 type_layouts: &'a crate::sema::type_layout::TypeLayoutRegistry,
123 warn_pedantic: bool,
124 warn_deprecated: bool,
125 ) -> Self {
126 let mut ctx = Self::new(st, std, warn_pedantic, warn_deprecated);
127 ctx.type_layouts = Some(type_layouts);
128 ctx
129 }
130
131 /// Emit an error if a feature requires a newer standard than selected.
132 fn require_std(&mut self, span: Span, min: FortranStandard, feature: &str) {
133 if let Some(selected) = self.std {
134 if selected < min {
135 self.error(
136 span,
137 format!("{} requires --std={:?} or later", feature, min),
138 );
139 }
140 }
141 }
142
143 /// Look up a symbol in the current validation scope.
144 fn lookup(&self, name: &str) -> Option<&'a Symbol> {
145 let key = (self.scope_id, name.to_lowercase());
146 if let Some(cached) = self.lookup_cache.borrow().get(&key).copied() {
147 return cached;
148 }
149 let resolved = self.st.lookup_in(self.scope_id, name);
150 self.lookup_cache.borrow_mut().insert(key, resolved);
151 resolved
152 }
153
154 fn error(&mut self, span: Span, msg: impl Into<String>) {
155 self.diags.push(Diagnostic {
156 span,
157 kind: DiagKind::Error,
158 msg: msg.into(),
159 });
160 }
161
162 fn warning(&mut self, span: Span, msg: impl Into<String>) {
163 self.diags.push(Diagnostic {
164 span,
165 kind: DiagKind::Warning,
166 msg: msg.into(),
167 });
168 }
169 }
170
171 /// Validate a parsed and resolved file. Returns diagnostics (errors and warnings).
172 pub fn validate_file(units: &[SpannedUnit], st: &SymbolTable) -> Vec<Diagnostic> {
173 validate_file_with_std(units, st, None)
174 }
175
176 /// Validate with a specific standard level for conformance checking.
177 pub fn validate_file_with_std(
178 units: &[SpannedUnit],
179 st: &SymbolTable,
180 std: Option<FortranStandard>,
181 ) -> Vec<Diagnostic> {
182 validate_file_with_warning_groups(units, st, std, false, false)
183 }
184
185 pub fn validate_file_with_warning_groups(
186 units: &[SpannedUnit],
187 st: &SymbolTable,
188 std: Option<FortranStandard>,
189 warn_pedantic: bool,
190 warn_deprecated: bool,
191 ) -> Vec<Diagnostic> {
192 let mut ctx = Ctx::new(st, std, warn_pedantic, warn_deprecated);
193 for unit in units {
194 validate_unit(&mut ctx, unit);
195 }
196 ctx.diags
197 }
198
199 /// Validate with access to derived-type layouts, enabling per-field
200 /// attribute checks on ALLOCATE / pointer-assignment targets that
201 /// select a component (`obj%comp`).
202 pub fn validate_file_with_layouts(
203 units: &[SpannedUnit],
204 st: &SymbolTable,
205 std: Option<FortranStandard>,
206 type_layouts: &crate::sema::type_layout::TypeLayoutRegistry,
207 ) -> Vec<Diagnostic> {
208 validate_file_with_layouts_and_warning_groups(units, st, std, type_layouts, false, false)
209 }
210
211 pub fn validate_file_with_layouts_and_warning_groups(
212 units: &[SpannedUnit],
213 st: &SymbolTable,
214 std: Option<FortranStandard>,
215 type_layouts: &crate::sema::type_layout::TypeLayoutRegistry,
216 warn_pedantic: bool,
217 warn_deprecated: bool,
218 ) -> Vec<Diagnostic> {
219 let mut ctx = Ctx::new_with_layouts(st, std, type_layouts, warn_pedantic, warn_deprecated);
220 for unit in units {
221 validate_unit(&mut ctx, unit);
222 }
223 ctx.diags
224 }
225
226 fn warn_legacy_feature(ctx: &mut Ctx<'_>, span: Span, feature: &str) {
227 if ctx.warn_pedantic || ctx.warn_deprecated {
228 ctx.warning(span, format!("{} is an obsolescent feature", feature));
229 }
230 }
231
232 fn decl_attrs_contain(attrs: &[Attribute], needle: Attribute) -> bool {
233 attrs.iter().any(|attr| *attr == needle)
234 }
235
236 fn is_deferred_char_pointer_component(type_spec: &TypeSpec, attrs: &[Attribute]) -> bool {
237 decl_attrs_contain(attrs, Attribute::Pointer)
238 && matches!(
239 type_spec,
240 TypeSpec::Character(Some(sel))
241 if matches!(&sel.len, Some(crate::ast::decl::LenSpec::Colon))
242 )
243 }
244
245 fn validate_unsupported_component_forms(
246 ctx: &mut Ctx<'_>,
247 components: &[crate::ast::decl::SpannedDecl],
248 ) {
249 let _ = (ctx, components);
250 }
251
252 /// Find the scope ID for a program unit, preferring children of `parent_scope`.
253 /// This resolves ambiguity when multiple scopes share a name (e.g., a module
254 /// subroutine and a CONTAINS subroutine with the same name).
255 fn find_scope_for_unit(
256 st: &SymbolTable,
257 unit: &ProgramUnit,
258 parent_scope: ScopeId,
259 ) -> Option<ScopeId> {
260 #[allow(clippy::type_complexity)]
261 let (kind_matcher, _name): (Box<dyn Fn(&ScopeKind) -> bool>, Option<String>) = match unit {
262 ProgramUnit::Program { name, .. } => {
263 let target = name.clone().unwrap_or_else(|| "<main>".into());
264 (
265 Box::new(move |k| matches!(k, ScopeKind::Program(ref n) if n == &target)),
266 None,
267 )
268 }
269 ProgramUnit::Module { name, .. } => {
270 let n = name.clone();
271 (
272 Box::new(
273 move |k| matches!(k, ScopeKind::Module(ref m) if m.eq_ignore_ascii_case(&n)),
274 ),
275 Some(name.clone()),
276 )
277 }
278 ProgramUnit::Subroutine { name, .. } => {
279 let n = name.clone();
280 (
281 Box::new(
282 move |k| matches!(k, ScopeKind::Subroutine(ref m) if m.eq_ignore_ascii_case(&n)),
283 ),
284 Some(name.clone()),
285 )
286 }
287 ProgramUnit::Function { name, .. } => {
288 let n = name.clone();
289 (
290 Box::new(
291 move |k| matches!(k, ScopeKind::Function(ref m) if m.eq_ignore_ascii_case(&n)),
292 ),
293 Some(name.clone()),
294 )
295 }
296 ProgramUnit::BlockData { name, .. } => {
297 let target = name.clone().unwrap_or_else(|| "<block_data>".into());
298 (
299 Box::new(move |k| matches!(k, ScopeKind::Program(ref n) if n == &target)),
300 None,
301 )
302 }
303 _ => return None,
304 };
305
306 // Prefer a child of the current parent scope.
307 let child = st
308 .scopes
309 .iter()
310 .find(|s| s.parent == Some(parent_scope) && kind_matcher(&s.kind));
311 if let Some(s) = child {
312 return Some(s.id);
313 }
314
315 // Fall back to any matching scope.
316 st.scopes
317 .iter()
318 .find(|s| kind_matcher(&s.kind))
319 .map(|s| s.id)
320 }
321
322 fn validate_unit(ctx: &mut Ctx, unit: &SpannedUnit) {
323 let saved_scope = ctx.scope_id;
324 if let Some(scope_id) = find_scope_for_unit(ctx.st, &unit.node, ctx.scope_id) {
325 ctx.scope_id = scope_id;
326 }
327
328 match &unit.node {
329 ProgramUnit::Program {
330 uses,
331 implicit,
332 decls,
333 body,
334 contains,
335 ..
336 } => {
337 for use_stmt in uses {
338 ctx.require_std(use_stmt.span, FortranStandard::F90, "USE statement");
339 }
340 for implicit_stmt in implicit {
341 if matches!(implicit_stmt.node, Decl::ImplicitNone { .. }) {
342 ctx.require_std(implicit_stmt.span, FortranStandard::F90, "IMPLICIT NONE");
343 }
344 }
345 if !contains.is_empty() {
346 ctx.require_std(
347 unit.span,
348 FortranStandard::F90,
349 "CONTAINS/internal procedures",
350 );
351 }
352 validate_decls(ctx, decls);
353 check_implicit_none(ctx, body, decls);
354 validate_stmts(ctx, body);
355 validate_label_consistency(ctx, unit.span);
356 for sub in contains {
357 validate_unit(ctx, sub);
358 }
359 }
360 ProgramUnit::Module {
361 uses,
362 implicit,
363 decls,
364 contains,
365 ..
366 } => {
367 ctx.require_std(unit.span, FortranStandard::F90, "MODULE");
368 for use_stmt in uses {
369 ctx.require_std(use_stmt.span, FortranStandard::F90, "USE statement");
370 }
371 for implicit_stmt in implicit {
372 if matches!(implicit_stmt.node, Decl::ImplicitNone { .. }) {
373 ctx.require_std(implicit_stmt.span, FortranStandard::F90, "IMPLICIT NONE");
374 }
375 }
376 validate_decls(ctx, decls);
377 for sub in contains {
378 validate_unit(ctx, sub);
379 }
380 }
381 ProgramUnit::Subroutine {
382 prefix,
383 uses,
384 implicit,
385 decls,
386 body,
387 contains,
388 args,
389 ..
390 } => {
391 let saved_pure = ctx.in_pure;
392 let saved_elemental = ctx.in_elemental;
393 ctx.in_pure = prefix.iter().any(|p| matches!(p, Prefix::Pure));
394 ctx.in_elemental = prefix.iter().any(|p| matches!(p, Prefix::Elemental));
395 if ctx.in_elemental {
396 ctx.in_pure = true;
397 }
398
399 if ctx.in_elemental {
400 validate_elemental_args(ctx, args, decls, unit.span);
401 }
402
403 for p in prefix {
404 match p {
405 Prefix::Pure | Prefix::Elemental => {
406 ctx.require_std(unit.span, FortranStandard::F95, "PURE/ELEMENTAL");
407 }
408 Prefix::Impure => {
409 ctx.require_std(unit.span, FortranStandard::F2008, "IMPURE");
410 }
411 Prefix::Recursive => {
412 ctx.require_std(unit.span, FortranStandard::F90, "RECURSIVE");
413 }
414 _ => {}
415 }
416 }
417 for use_stmt in uses {
418 ctx.require_std(use_stmt.span, FortranStandard::F90, "USE statement");
419 }
420 for implicit_stmt in implicit {
421 if matches!(implicit_stmt.node, Decl::ImplicitNone { .. }) {
422 ctx.require_std(implicit_stmt.span, FortranStandard::F90, "IMPLICIT NONE");
423 }
424 }
425 if !contains.is_empty() {
426 ctx.require_std(
427 unit.span,
428 FortranStandard::F90,
429 "CONTAINS/internal procedures",
430 );
431 }
432 validate_decls(ctx, decls);
433 check_implicit_none(ctx, body, decls);
434 validate_stmts(ctx, body);
435 validate_label_consistency(ctx, unit.span);
436 for sub in contains {
437 validate_unit(ctx, sub);
438 }
439 ctx.in_pure = saved_pure;
440 ctx.in_elemental = saved_elemental;
441 }
442 ProgramUnit::Function {
443 prefix,
444 uses,
445 implicit,
446 decls,
447 body,
448 contains,
449 args,
450 ..
451 } => {
452 let saved_pure = ctx.in_pure;
453 let saved_elemental = ctx.in_elemental;
454 ctx.in_pure = prefix.iter().any(|p| matches!(p, Prefix::Pure));
455 ctx.in_elemental = prefix.iter().any(|p| matches!(p, Prefix::Elemental));
456 if ctx.in_elemental {
457 ctx.in_pure = true;
458 }
459
460 if ctx.in_elemental {
461 validate_elemental_args(ctx, args, decls, unit.span);
462 }
463
464 for p in prefix {
465 match p {
466 Prefix::Pure | Prefix::Elemental => {
467 ctx.require_std(unit.span, FortranStandard::F95, "PURE/ELEMENTAL");
468 }
469 Prefix::Impure => {
470 ctx.require_std(unit.span, FortranStandard::F2008, "IMPURE");
471 }
472 Prefix::Recursive => {
473 ctx.require_std(unit.span, FortranStandard::F90, "RECURSIVE");
474 }
475 _ => {}
476 }
477 }
478 for use_stmt in uses {
479 ctx.require_std(use_stmt.span, FortranStandard::F90, "USE statement");
480 }
481 for implicit_stmt in implicit {
482 if matches!(implicit_stmt.node, Decl::ImplicitNone { .. }) {
483 ctx.require_std(implicit_stmt.span, FortranStandard::F90, "IMPLICIT NONE");
484 }
485 }
486 if !contains.is_empty() {
487 ctx.require_std(
488 unit.span,
489 FortranStandard::F90,
490 "CONTAINS/internal procedures",
491 );
492 }
493 validate_decls(ctx, decls);
494 check_implicit_none(ctx, body, decls);
495 validate_stmts(ctx, body);
496 validate_label_consistency(ctx, unit.span);
497 for sub in contains {
498 validate_unit(ctx, sub);
499 }
500 ctx.in_pure = saved_pure;
501 ctx.in_elemental = saved_elemental;
502 }
503 ProgramUnit::Submodule {
504 uses,
505 decls,
506 contains,
507 ..
508 } => {
509 ctx.require_std(unit.span, FortranStandard::F2008, "SUBMODULE");
510 for use_stmt in uses {
511 ctx.require_std(use_stmt.span, FortranStandard::F90, "USE statement");
512 }
513 validate_decls(ctx, decls);
514 for sub in contains {
515 validate_unit(ctx, sub);
516 }
517 }
518 ProgramUnit::BlockData { decls, .. } => {
519 warn_legacy_feature(ctx, unit.span, "BLOCK DATA");
520 validate_decls(ctx, decls);
521 }
522 ProgramUnit::InterfaceBlock {
523 name,
524 is_abstract,
525 bodies,
526 } => {
527 ctx.require_std(unit.span, FortranStandard::F90, "INTERFACE block");
528 // Validate defined operator interfaces.
529 if let Some(ref iface_name) = name {
530 if is_operator_interface(iface_name) {
531 validate_operator_interface(ctx, iface_name, bodies, unit.span);
532 }
533 }
534 // Abstract interfaces cannot have MODULE PROCEDURE.
535 if *is_abstract {
536 ctx.require_std(unit.span, FortranStandard::F2003, "ABSTRACT interface");
537 for body in bodies {
538 if let InterfaceBody::ModuleProcedure(names) = body {
539 if !names.is_empty() {
540 ctx.error(
541 unit.span,
542 "abstract interface cannot contain MODULE PROCEDURE statements",
543 );
544 }
545 }
546 }
547 }
548 for body in bodies {
549 if let InterfaceBody::Subprogram(sub) = body {
550 validate_unit(ctx, sub);
551 }
552 }
553 }
554 }
555
556 ctx.scope_id = saved_scope;
557 }
558
559 // ---- Declaration validation ----
560
561 fn validate_decls(ctx: &mut Ctx, decls: &[crate::ast::decl::SpannedDecl]) {
562 for decl in decls {
563 if let Decl::TypeDecl {
564 attrs,
565 entities,
566 type_spec,
567 ..
568 } = &decl.node
569 {
570 let has_alloc = attrs.iter().any(|a| matches!(a, Attribute::Allocatable));
571 let has_pointer = attrs.iter().any(|a| matches!(a, Attribute::Pointer));
572 let is_scalar_decl = entities.iter().all(|entity| entity.array_spec.is_none());
573
574 // Deferred-length character must be allocatable or pointer.
575 if let crate::ast::decl::TypeSpec::Character(Some(sel)) = type_spec {
576 if let Some(crate::ast::decl::LenSpec::Colon) = &sel.len {
577 ctx.require_std(
578 decl.span,
579 FortranStandard::F2003,
580 "deferred-length character",
581 );
582 if !has_alloc && !has_pointer {
583 ctx.error(decl.span, "deferred-length character (len=:) requires allocatable or pointer attribute");
584 }
585 }
586 }
587
588 match type_spec {
589 TypeSpec::Class(_) => {
590 ctx.require_std(decl.span, FortranStandard::F2003, "CLASS declaration");
591 }
592 TypeSpec::ClassStar | TypeSpec::TypeStar => {
593 ctx.require_std(
594 decl.span,
595 FortranStandard::F2018,
596 "CLASS(*)/TYPE(*) declaration",
597 );
598 }
599 _ => {}
600 }
601
602 if has_alloc && is_scalar_decl {
603 ctx.require_std(
604 decl.span,
605 FortranStandard::F2003,
606 "allocatable scalar variables",
607 );
608 }
609
610 // Allocatable + pointer is forbidden.
611 if has_alloc && has_pointer {
612 ctx.error(
613 decl.span,
614 "a variable cannot be both allocatable and pointer",
615 );
616 }
617
618 // Parameter with allocatable/pointer is forbidden.
619 let has_param = attrs.iter().any(|a| matches!(a, Attribute::Parameter));
620 if has_param && has_alloc {
621 ctx.error(
622 decl.span,
623 "a named constant (parameter) cannot be allocatable",
624 );
625 }
626 if has_param && has_pointer {
627 ctx.error(
628 decl.span,
629 "a named constant (parameter) cannot be a pointer",
630 );
631 }
632
633 // Pure/elemental: SAVE is forbidden.
634 if ctx.in_pure {
635 let has_save = attrs.iter().any(|a| matches!(a, Attribute::Save));
636 if has_save {
637 ctx.error(decl.span, "SAVE attribute not allowed in pure procedure");
638 }
639 }
640
641 let _ = entities; // entities checked individually if needed
642 }
643
644 if matches!(decl.node, Decl::ImplicitNone { .. }) {
645 ctx.require_std(decl.span, FortranStandard::F90, "IMPLICIT NONE");
646 }
647
648 if matches!(decl.node, Decl::UseStmt { .. }) {
649 ctx.require_std(decl.span, FortranStandard::F90, "USE statement");
650 }
651
652 if matches!(decl.node, Decl::CommonBlock { .. }) {
653 warn_legacy_feature(ctx, decl.span, "COMMON block");
654 }
655
656 if matches!(decl.node, Decl::EquivalenceStmt { .. }) {
657 warn_legacy_feature(ctx, decl.span, "EQUIVALENCE");
658 }
659
660 // Derived type definition validation.
661 if let Decl::DerivedTypeDef {
662 name,
663 attrs: type_attrs,
664 type_bound_procs,
665 components,
666 ..
667 } = &decl.node
668 {
669 ctx.require_std(decl.span, FortranStandard::F90, "derived types");
670 if type_attrs
671 .iter()
672 .any(|attr| matches!(attr, TypeAttr::Abstract))
673 {
674 ctx.require_std(decl.span, FortranStandard::F2003, "ABSTRACT type");
675 }
676 validate_unsupported_component_forms(ctx, components);
677 validate_derived_type(
678 ctx,
679 name,
680 type_attrs,
681 type_bound_procs,
682 components,
683 decl.span,
684 );
685 }
686 }
687 }
688
689 // ---- Statement validation ----
690
691 fn validate_stmts(ctx: &mut Ctx, stmts: &[SpannedStmt]) {
692 for stmt in stmts {
693 validate_stmt(ctx, stmt);
694 }
695 }
696
697 fn validate_stmt(ctx: &mut Ctx, stmt: &SpannedStmt) {
698 match &stmt.node {
699 // ---- Assignment ----
700 Stmt::Assignment { target, value } => {
701 validate_assignment_target(ctx, target, stmt.span);
702 reject_pure_nonlocal_definition(ctx, target, stmt.span, "assignment");
703 if ctx.in_pure {
704 check_pure_expr_calls(ctx, value);
705 }
706 }
707 Stmt::PointerAssignment { target, value, .. } => {
708 validate_pointer_assignment(ctx, target, value, stmt.span);
709 reject_pure_nonlocal_definition(ctx, target, stmt.span, "pointer assignment");
710 }
711
712 // ---- Allocate / Deallocate ----
713 Stmt::Allocate { items, opts } => {
714 if opts.iter().any(|opt| {
715 opt.keyword
716 .as_deref()
717 .is_some_and(|kw| kw.eq_ignore_ascii_case("source"))
718 }) {
719 ctx.require_std(stmt.span, FortranStandard::F2003, "ALLOCATE with SOURCE=");
720 }
721 for item in items {
722 validate_allocatable_item(ctx, item, "allocate");
723 }
724 }
725 Stmt::Deallocate { items, .. } => {
726 for item in items {
727 validate_allocatable_item(ctx, item, "deallocate");
728 }
729 }
730
731 // ---- I/O in pure ----
732 Stmt::Write { .. }
733 | Stmt::Read { .. }
734 | Stmt::Print { .. }
735 | Stmt::Open { .. }
736 | Stmt::Close { .. }
737 | Stmt::Inquire { .. }
738 | Stmt::Rewind { .. }
739 | Stmt::Backspace { .. }
740 | Stmt::Endfile { .. }
741 | Stmt::Flush { .. }
742 | Stmt::Wait { .. } => {
743 if ctx.in_pure {
744 ctx.error(stmt.span, "I/O statement not allowed in pure procedure");
745 }
746 }
747
748 // ---- STOP in pure ----
749 Stmt::Stop { .. } => {
750 if ctx.in_pure {
751 ctx.error(stmt.span, "STOP not allowed in pure procedure");
752 }
753 }
754 Stmt::ErrorStop { .. } => {
755 if ctx.in_pure {
756 ctx.error(stmt.span, "ERROR STOP not allowed in pure procedure");
757 }
758 ctx.require_std(stmt.span, FortranStandard::F2008, "ERROR STOP");
759 }
760
761 // ---- GOTO / labels ----
762 Stmt::Goto { label } => {
763 ctx.labels_referenced.push((*label, stmt.span));
764 }
765 Stmt::ComputedGoto { labels, .. } => {
766 warn_legacy_feature(ctx, stmt.span, "computed GOTO");
767 for label in labels {
768 ctx.labels_referenced.push((*label, stmt.span));
769 }
770 }
771 Stmt::ArithmeticIf { neg, zero, pos, .. } => {
772 warn_legacy_feature(ctx, stmt.span, "arithmetic IF");
773 ctx.labels_referenced.push((*neg, stmt.span));
774 ctx.labels_referenced.push((*zero, stmt.span));
775 ctx.labels_referenced.push((*pos, stmt.span));
776 }
777 Stmt::Continue { label: Some(lbl) } => {
778 register_label(ctx, *lbl, stmt.span);
779 }
780 Stmt::Labeled { label, stmt: inner } => {
781 register_label(ctx, *label, stmt.span);
782 validate_stmt(ctx, inner);
783 }
784
785 // ---- Control flow — recurse into bodies ----
786 Stmt::IfConstruct {
787 then_body,
788 else_ifs,
789 else_body,
790 ..
791 } => {
792 validate_stmts(ctx, then_body);
793 for (_, body) in else_ifs {
794 validate_stmts(ctx, body);
795 }
796 if let Some(body) = else_body {
797 validate_stmts(ctx, body);
798 }
799 }
800 Stmt::IfStmt { action, .. } => validate_stmt(ctx, action),
801 Stmt::DoLoop { body, .. } => validate_stmts(ctx, body),
802 Stmt::DoWhile { body, .. } => validate_stmts(ctx, body),
803 Stmt::DoConcurrent { body, .. } => {
804 ctx.require_std(stmt.span, FortranStandard::F2008, "DO CONCURRENT");
805 validate_stmts(ctx, body);
806 }
807 Stmt::SelectCase { cases, .. } => {
808 for case in cases {
809 validate_stmts(ctx, &case.body);
810 }
811 }
812 Stmt::WhereConstruct {
813 body, elsewhere, ..
814 } => {
815 validate_stmts(ctx, body);
816 for (_, ebody) in elsewhere {
817 validate_stmts(ctx, ebody);
818 }
819 }
820 Stmt::WhereStmt { stmt: inner, .. } => validate_stmt(ctx, inner),
821 Stmt::ForallConstruct { body, .. } => {
822 ctx.require_std(stmt.span, FortranStandard::F95, "FORALL construct");
823 validate_stmts(ctx, body);
824 }
825 Stmt::ForallStmt { stmt: inner, .. } => {
826 ctx.require_std(stmt.span, FortranStandard::F95, "FORALL statement");
827 validate_stmt(ctx, inner);
828 }
829 Stmt::Block {
830 uses,
831 ifaces,
832 implicit,
833 decls,
834 body,
835 ..
836 } => {
837 ctx.require_std(stmt.span, FortranStandard::F2008, "BLOCK construct");
838 validate_decls(ctx, uses);
839 validate_decls(ctx, implicit);
840 validate_decls(ctx, decls);
841 for iface in ifaces {
842 validate_unit(ctx, iface);
843 }
844 validate_stmts(ctx, body);
845 }
846 Stmt::Associate { assocs, body, .. } => {
847 ctx.require_std(stmt.span, FortranStandard::F2003, "ASSOCIATE construct");
848 validate_associate(ctx, assocs, body, stmt.span);
849 }
850
851 // Call in pure: callee must be pure (we check if it's known impure).
852 Stmt::Call { callee, args, .. } => {
853 if let Expr::Name { name } = &callee.node {
854 if name.eq_ignore_ascii_case("move_alloc") {
855 ctx.require_std(stmt.span, FortranStandard::F2003, "MOVE_ALLOC");
856 }
857 }
858 if ctx.in_pure {
859 validate_pure_call(ctx, callee, stmt.span);
860 }
861 validate_call_site_intent(ctx, callee, args, stmt.span);
862 }
863
864 // Nullify: items must be pointers.
865 Stmt::Nullify { items } => {
866 for item in items {
867 if let Some(ref name) = extract_base_name(item) {
868 let is_pointer = ctx.lookup(name).map(|s| s.attrs.pointer).unwrap_or(true);
869 if !is_pointer {
870 ctx.error(
871 item.span,
872 format!("NULLIFY target '{}' must have pointer attribute", name),
873 );
874 }
875 }
876 }
877 }
878
879 // Embedded declarations (e.g., inside BLOCK constructs).
880 Stmt::Declaration(decl) => {
881 validate_decls(ctx, std::slice::from_ref(decl));
882 }
883
884 _ => {}
885 }
886 }
887
888 // ---- Specific validation checks ----
889
890 /// Check that an assignment target is modifiable (not intent(in), not parameter).
891 /// Handles component access (x%field) and array elements (a(i)) — the base
892 /// variable's intent/parameter status applies to all parts.
893 fn validate_assignment_target(ctx: &mut Ctx, target: &crate::ast::expr::SpannedExpr, span: Span) {
894 if let Some(name) = extract_base_name(target) {
895 let (is_intent_in, is_parameter, is_pointer) = ctx
896 .lookup(&name)
897 .map(|sym| {
898 (
899 matches!(sym.attrs.intent, Some(Intent::In)),
900 sym.attrs.parameter,
901 sym.attrs.pointer,
902 )
903 })
904 .unwrap_or((false, false, false));
905 let writes_through_pointer_target = is_pointer && !matches!(target.node, Expr::Name { .. });
906 if is_intent_in && !writes_through_pointer_target {
907 ctx.error(
908 span,
909 format!("cannot assign to intent(in) variable '{}'", name),
910 );
911 }
912 if is_parameter {
913 ctx.error(span, format!("cannot assign to named constant '{}'", name));
914 }
915 }
916 }
917
918 /// Validate pointer assignment: LHS must be pointer, RHS must be target/pointer.
919 fn validate_pointer_assignment(
920 ctx: &mut Ctx,
921 target: &crate::ast::expr::SpannedExpr,
922 value: &crate::ast::expr::SpannedExpr,
923 span: Span,
924 ) {
925 // Component-access target (`p%ptr_field => x`): check the leaf
926 // component's attributes through the type-layout registry. If
927 // layouts aren't available (older callers) or the chain can't be
928 // resolved, skip the check rather than flag the base variable.
929 if expr_selects_component(target) {
930 if let Some(leaf) = leaf_field_layout(ctx, target) {
931 if !leaf.field.pointer {
932 ctx.error(
933 span,
934 format!(
935 "pointer assignment target component '{}' must have pointer attribute",
936 leaf.field.name
937 ),
938 );
939 }
940 }
941 } else if let Some(name) = extract_base_name(target) {
942 let is_pointer = ctx.lookup(&name).map(|s| s.attrs.pointer).unwrap_or(true);
943 if !is_pointer {
944 ctx.error(
945 span,
946 format!(
947 "pointer assignment target '{}' must have pointer attribute",
948 name
949 ),
950 );
951 }
952 }
953
954 // RHS must have target attribute or be a pointer (or null()/function call).
955 if expr_selects_component(value) {
956 // Look up the leaf component's attributes. F2018 §8.5.14
957 // says a subobject of a TARGET base (or an allocated
958 // ALLOCATABLE) is itself a valid target, so accept when any
959 // ancestor on the path carries one of those attributes.
960 if let Some(leaf) = leaf_field_layout(ctx, value) {
961 let ok = leaf.field.pointer
962 || leaf.field.target
963 || leaf.ancestor_is_target
964 || leaf.ancestor_is_allocatable;
965 if !ok {
966 ctx.error(span, format!(
967 "pointer assignment source component '{}' must have target or pointer attribute",
968 leaf.field.name
969 ));
970 }
971 }
972 return;
973 }
974 if let Some(name) = extract_base_name(value) {
975 // Skip if the value is a function call — could be null() or pointer-valued function.
976 if matches!(value.node, Expr::FunctionCall { .. }) {
977 return;
978 }
979 // Dummy procedure arguments are valid RHS targets per F2003
980 // (their addresses are implicitly available). The generic
981 // target/pointer check below doesn't see the "dummy
982 // procedure" attribute directly; accept any Function/
983 // Subroutine symbol and any variable declared via
984 // `procedure(iface)` (parsed with Attribute::External).
985 if let Some(sym) = ctx.lookup(&name) {
986 use crate::sema::symtab::SymbolKind;
987 if matches!(sym.kind, SymbolKind::Function | SymbolKind::Subroutine) {
988 return;
989 }
990 if sym.attrs.external {
991 return;
992 }
993 }
994 let ok = ctx
995 .lookup(&name)
996 .map(|s| s.attrs.target || s.attrs.pointer)
997 .unwrap_or(true);
998 if !ok {
999 ctx.error(
1000 span,
1001 format!(
1002 "pointer assignment source '{}' must have target or pointer attribute",
1003 name
1004 ),
1005 );
1006 }
1007 }
1008 }
1009
1010 /// Validate that an ALLOCATE/DEALLOCATE item is allocatable or pointer.
1011 ///
1012 /// For a component access like `pools(i)%tokens(n)`, the target is
1013 /// the `tokens` field — not the `pools` base. Resolve the leaf
1014 /// component through the type-layout registry and check its own
1015 /// attributes. Bare-name targets still get the symbol attribute
1016 /// check. If the chain can't be resolved (registry missing, cross-
1017 /// TU stale .amod, etc.) we skip rather than produce a misleading
1018 /// error.
1019 fn validate_allocatable_item(ctx: &mut Ctx, item: &crate::ast::expr::SpannedExpr, stmt_name: &str) {
1020 if expr_selects_component(item) {
1021 if let Some(leaf) = leaf_field_layout(ctx, item) {
1022 if !leaf.field.allocatable && !leaf.field.pointer {
1023 ctx.error(
1024 item.span,
1025 format!(
1026 "only allocatable or pointer components can appear in {}, but '{}' is neither",
1027 stmt_name.to_uppercase(), leaf.field.name
1028 ),
1029 );
1030 }
1031 }
1032 return;
1033 }
1034 let base_name = extract_base_name(item);
1035 if let Some(ref name) = base_name {
1036 let ok = ctx
1037 .lookup(name)
1038 .map(|s| s.attrs.allocatable || s.attrs.pointer)
1039 .unwrap_or(true); // unknown symbol — skip
1040 if !ok {
1041 ctx.error(
1042 item.span,
1043 format!(
1044 "only allocatable or pointer variables can appear in {}, but '{}' is neither",
1045 stmt_name.to_uppercase(),
1046 name
1047 ),
1048 );
1049 }
1050 }
1051 }
1052
1053 /// Does this expression select into a derived-type component
1054 /// anywhere in its path? e.g. `pools(i)%tokens(n)` → true,
1055 /// `pools(i)` → false, `pools` → false.
1056 fn expr_selects_component(expr: &crate::ast::expr::SpannedExpr) -> bool {
1057 match &expr.node {
1058 Expr::ComponentAccess { .. } => true,
1059 Expr::FunctionCall { callee, .. } => expr_selects_component(callee),
1060 _ => false,
1061 }
1062 }
1063
1064 /// Resolved metadata for the leaf of a component access.
1065 struct LeafComponent<'a> {
1066 field: &'a crate::sema::type_layout::FieldLayout,
1067 /// Any ancestor on the path (including the base variable or any
1068 /// intermediate component) has the TARGET attribute. F2018
1069 /// §8.5.14: a subobject of a TARGET is itself a valid target.
1070 ancestor_is_target: bool,
1071 /// Any ancestor is ALLOCATABLE — per §8.5.14, an allocated
1072 /// subobject of an allocatable is also a valid target.
1073 ancestor_is_allocatable: bool,
1074 }
1075
1076 /// Walk an expression down to its leaf component access and return
1077 /// that component's FieldLayout (with attribute metadata). Returns
1078 /// `None` if the expression has no component access, or if the
1079 /// chain's derived-type path can't be resolved through the symbol
1080 /// table + layout registry (for example, a field whose type is a
1081 /// derived type that wasn't in the registry — uncommon but possible
1082 /// when a cross-TU .amod is stale).
1083 fn leaf_field_layout<'a>(
1084 ctx: &'a Ctx,
1085 expr: &crate::ast::expr::SpannedExpr,
1086 ) -> Option<LeafComponent<'a>> {
1087 let layouts = ctx.type_layouts?;
1088 // Collect the component chain from outermost to innermost.
1089 let mut chain: Vec<&str> = Vec::new();
1090 let mut cur = expr;
1091 let base_name = loop {
1092 match &cur.node {
1093 Expr::ComponentAccess { base, component } => {
1094 chain.push(component.as_str());
1095 cur = base;
1096 }
1097 Expr::FunctionCall { callee, .. } => {
1098 cur = callee;
1099 }
1100 Expr::Name { name } => break name.as_str(),
1101 _ => return None,
1102 }
1103 };
1104 chain.reverse();
1105 if chain.is_empty() {
1106 return None;
1107 }
1108 // Resolve the base variable's derived type via the symbol table.
1109 let sym = ctx.lookup(base_name)?;
1110 let base_type = match sym.type_info.as_ref()? {
1111 crate::sema::symtab::TypeInfo::Derived(name) => name.clone(),
1112 _ => return None,
1113 };
1114 // Seed ancestor flags from the base variable's own attributes.
1115 let mut ancestor_is_target = sym.attrs.target;
1116 let mut ancestor_is_allocatable = sym.attrs.allocatable;
1117 let mut current_type = base_type;
1118 let mut leaf: Option<&crate::sema::type_layout::FieldLayout> = None;
1119 for (i, comp) in chain.iter().enumerate() {
1120 let layout = layouts.get(&current_type)?;
1121 let field = layout.field(comp)?;
1122 // On non-terminal components, accumulate TARGET / ALLOCATABLE
1123 // so the leaf check can honour inherited target-ness.
1124 let is_terminal = i + 1 == chain.len();
1125 if !is_terminal {
1126 if field.target {
1127 ancestor_is_target = true;
1128 }
1129 if field.allocatable {
1130 ancestor_is_allocatable = true;
1131 }
1132 }
1133 leaf = Some(field);
1134 match &field.type_info {
1135 crate::sema::symtab::TypeInfo::Derived(name) => {
1136 current_type = name.clone();
1137 }
1138 _ => {
1139 // Scalar / intrinsic-typed leaf — no further resolution.
1140 }
1141 }
1142 }
1143 leaf.map(|field| LeafComponent {
1144 field,
1145 ancestor_is_target,
1146 ancestor_is_allocatable,
1147 })
1148 }
1149
1150 /// Check if a call in a pure procedure is to a known impure procedure.
1151 /// Symbol-level pure tracking isn't yet wired into the symbol table,
1152 /// so this is conservative: we warn if the callee resolves to an
1153 /// external procedure (whose body we cannot inspect). I/O, STOP,
1154 /// and SAVE violations are caught statement-level in validate_stmt.
1155 /// Walk an expression tree and check any function calls against the
1156 /// pure-call constraint. Catches `r = impure_fn()` which is an
1157 /// expression-level call, not a `Stmt::Call`.
1158 fn check_pure_expr_calls(ctx: &mut Ctx, expr: &crate::ast::expr::SpannedExpr) {
1159 match &expr.node {
1160 Expr::FunctionCall { callee, args } => {
1161 validate_pure_call(ctx, callee, expr.span);
1162 for arg in args {
1163 if let crate::ast::expr::SectionSubscript::Element(e) = &arg.value {
1164 check_pure_expr_calls(ctx, e);
1165 }
1166 }
1167 }
1168 Expr::BinaryOp { left, right, .. } => {
1169 check_pure_expr_calls(ctx, left);
1170 check_pure_expr_calls(ctx, right);
1171 }
1172 Expr::UnaryOp { operand, .. } => check_pure_expr_calls(ctx, operand),
1173 Expr::ParenExpr { inner } => check_pure_expr_calls(ctx, inner),
1174 _ => {}
1175 }
1176 }
1177
1178 fn validate_pure_call(ctx: &mut Ctx, callee: &crate::ast::expr::SpannedExpr, span: Span) {
1179 // F2018 15.7: a PURE procedure may only call PURE, ELEMENTAL,
1180 // or intrinsic procedures. If the callee resolves to a known
1181 // symbol that is NOT marked pure/elemental/intrinsic, reject.
1182 // Unknown callees (external without an interface) are left
1183 // alone — the programmer's responsibility per F2018 §15.4.
1184 let Some(name) = extract_base_name(callee) else {
1185 return;
1186 };
1187 let Some(sym) = ctx.lookup(&name) else {
1188 return;
1189 };
1190 match sym.kind {
1191 SymbolKind::Function | SymbolKind::Subroutine => {
1192 if !sym.attrs.pure && !sym.attrs.elemental && !sym.attrs.intrinsic {
1193 ctx.error(
1194 span,
1195 format!(
1196 "call to '{}' inside a pure procedure: callee is not pure, elemental, or intrinsic (F2018 15.7)",
1197 sym.name
1198 ),
1199 );
1200 }
1201 }
1202 SymbolKind::IntrinsicProc => {} // always OK
1203 _ => {} // external / unknown — can't check
1204 }
1205 }
1206
1207 /// True if `sym` is declared outside the procedure rooted at
1208 /// `procedure_scope` — i.e. it comes from host association, USE
1209 /// association, or a COMMON block in an enclosing unit. This is
1210 /// the F2018 15.7 "accessed by host or use association, or in
1211 /// common" predicate that makes a variable off-limits for
1212 /// definition inside a PURE procedure body.
1213 fn symbol_is_non_local_to_procedure(
1214 st: &SymbolTable,
1215 sym: &Symbol,
1216 procedure_scope: ScopeId,
1217 ) -> bool {
1218 // Walk from `sym.scope` up the parent chain. If we reach
1219 // `procedure_scope` (or a descendant we started from), the
1220 // symbol lives inside the current procedure — that's OK.
1221 // If we reach the top (Global) without crossing the procedure
1222 // boundary, the symbol is in an enclosing scope (module,
1223 // parent program, parent subroutine).
1224 let mut cur = Some(sym.scope);
1225 while let Some(sid) = cur {
1226 if sid == procedure_scope {
1227 return false;
1228 }
1229 cur = st.scope(sid).parent;
1230 }
1231 true
1232 }
1233
1234 /// Reject a PURE-procedure statement that would define a variable
1235 /// visible via host/use association or a common block. The
1236 /// caller supplies the designator's root name; we look it up in
1237 /// the current scope and check whether its home scope lies
1238 /// outside the enclosing procedure. F2018 15.7, C1598.
1239 fn reject_pure_nonlocal_definition(
1240 ctx: &mut Ctx,
1241 target: &crate::ast::expr::SpannedExpr,
1242 span: Span,
1243 stmt_label: &str,
1244 ) {
1245 if !ctx.in_pure {
1246 return;
1247 }
1248 let Some(name) = extract_base_name(target) else {
1249 return;
1250 };
1251 let Some(sym) = ctx.lookup(&name) else {
1252 return;
1253 };
1254 // Only variables and COMMON blocks can be "defined"; function
1255 // names get definition semantics too but those are the pure
1256 // function's own result variable (always local).
1257 if !matches!(
1258 sym.kind,
1259 SymbolKind::Variable | SymbolKind::Parameter | SymbolKind::CommonBlock
1260 ) {
1261 return;
1262 }
1263 if symbol_is_non_local_to_procedure(ctx.st, sym, ctx.scope_id) {
1264 let sym_name = sym.name.clone();
1265 ctx.error(
1266 span,
1267 format!(
1268 "{} target '{}' is accessed by host or use association and cannot be defined inside a pure procedure (F2018 15.7)",
1269 stmt_label, sym_name
1270 ),
1271 );
1272 }
1273 }
1274
1275 /// Validate call-site argument intent constraints.
1276 /// Can't pass a literal, parameter, or expression to intent(out/inout).
1277 fn validate_call_site_intent(
1278 ctx: &mut Ctx,
1279 callee: &crate::ast::expr::SpannedExpr,
1280 args: &[crate::ast::expr::Argument],
1281 span: Span,
1282 ) {
1283 // Look up the callee to find its dummy argument intents.
1284 let callee_name = if let Expr::Name { name } = &callee.node {
1285 name.clone()
1286 } else {
1287 return;
1288 };
1289
1290 // For each actual argument, check if it's an lvalue when the dummy requires out/inout.
1291 // We can only check this if the callee's dummy arg info is in the symbol table.
1292 // For now, check the simpler case: passing a literal or parameter to ANY subroutine arg.
1293 for arg in args {
1294 let actual = match &arg.value {
1295 crate::ast::expr::SectionSubscript::Element(e) => e,
1296 _ => continue,
1297 };
1298 // Check if actual is a literal (not an lvalue).
1299 let is_literal = matches!(
1300 actual.node,
1301 Expr::IntegerLiteral { .. }
1302 | Expr::RealLiteral { .. }
1303 | Expr::StringLiteral { .. }
1304 | Expr::LogicalLiteral { .. }
1305 | Expr::ComplexLiteral { .. }
1306 );
1307 // Check if actual is a named constant (parameter).
1308 let is_parameter = if let Some(name) = extract_base_name(actual) {
1309 ctx.lookup(&name)
1310 .map(|s| s.attrs.parameter)
1311 .unwrap_or(false)
1312 } else {
1313 false
1314 };
1315
1316 if is_literal || is_parameter {
1317 // We can't tell without the callee's interface whether this arg is
1318 // intent(out/inout). But if the callee IS known and has dummy arg info,
1319 // we could check. For now, this infrastructure is in place for when
1320 // we have full interface resolution.
1321 // Full check deferred until interfaces are tracked in symbol table.
1322 }
1323 }
1324 let _ = callee_name;
1325 let _ = span;
1326 }
1327
1328 /// Validate elemental procedure arguments are scalar.
1329 fn validate_elemental_args(
1330 ctx: &mut Ctx,
1331 args: &[DummyArg],
1332 decls: &[crate::ast::decl::SpannedDecl],
1333 span: Span,
1334 ) {
1335 // Elemental: all dummy arguments must be scalar (no dimension attribute).
1336 for arg in args {
1337 if let DummyArg::Name(arg_name) = arg {
1338 for decl in decls {
1339 if let Decl::TypeDecl {
1340 attrs, entities, ..
1341 } = &decl.node
1342 {
1343 for entity in entities {
1344 if entity.name.eq_ignore_ascii_case(arg_name) {
1345 // Check for dimension attribute or explicit array spec on entity.
1346 let has_dimension =
1347 attrs.iter().any(|a| matches!(a, Attribute::Dimension(_)));
1348 let has_entity_dims = entity.array_spec.is_some();
1349 if has_dimension || has_entity_dims {
1350 ctx.error(
1351 span,
1352 format!(
1353 "elemental procedure argument '{}' must be scalar",
1354 arg_name
1355 ),
1356 );
1357 }
1358 }
1359 }
1360 }
1361 }
1362 }
1363 }
1364 }
1365
1366 /// Register a label as defined.
1367 fn register_label(ctx: &mut Ctx, label: u64, span: Span) {
1368 if ctx.labels_defined.contains(&label) {
1369 ctx.error(span, format!("duplicate label {}", label));
1370 } else {
1371 ctx.labels_defined.push(label);
1372 }
1373 }
1374
1375 /// At the end of a scope, verify all GOTO labels have targets.
1376 fn validate_label_consistency(ctx: &mut Ctx, _scope_span: Span) {
1377 // Collect errors first to avoid borrow conflict.
1378 let errors: Vec<(Span, String)> = ctx
1379 .labels_referenced
1380 .iter()
1381 .filter(|(label, _)| !ctx.labels_defined.contains(label))
1382 .map(|(label, span)| {
1383 (
1384 *span,
1385 format!("GOTO target label {} not defined in this scope", label),
1386 )
1387 })
1388 .collect();
1389 for (span, msg) in errors {
1390 ctx.error(span, msg);
1391 }
1392 ctx.labels_defined.clear();
1393 ctx.labels_referenced.clear();
1394 }
1395
1396 /// Check if an interface name represents an operator interface.
1397 fn is_operator_interface(name: &str) -> bool {
1398 let lower = name.to_lowercase();
1399 lower.starts_with("operator(") || lower.starts_with("assignment(")
1400 }
1401
1402 /// Validate a defined operator interface.
1403 fn validate_operator_interface(
1404 ctx: &mut Ctx,
1405 iface_name: &str,
1406 bodies: &[InterfaceBody],
1407 span: Span,
1408 ) {
1409 let lower = iface_name.to_lowercase();
1410 let is_assignment = lower.starts_with("assignment(");
1411
1412 for body in bodies {
1413 match body {
1414 InterfaceBody::Subprogram(sub) => {
1415 match &sub.node {
1416 ProgramUnit::Function { args, .. } => {
1417 if is_assignment {
1418 ctx.error(
1419 sub.span,
1420 format!(
1421 "ASSIGNMENT({}) interface must contain subroutines, not functions",
1422 "="
1423 ),
1424 );
1425 continue;
1426 }
1427 // Operator functions: unary = 1 arg, binary = 2 args.
1428 let nargs = args.len();
1429 if !(1..=2).contains(&nargs) {
1430 ctx.error(
1431 sub.span,
1432 format!(
1433 "operator interface function must have 1 or 2 arguments, got {}",
1434 nargs
1435 ),
1436 );
1437 }
1438 // All arguments must be intent(in) — checked by looking at decls.
1439 // Deferred: would need to walk the function's decls to check intent.
1440 }
1441 ProgramUnit::Subroutine { args, .. } => {
1442 if !is_assignment {
1443 ctx.error(
1444 sub.span,
1445 "operator interface must contain functions, not subroutines",
1446 );
1447 continue;
1448 }
1449 // Assignment subroutines must have exactly 2 arguments.
1450 if args.len() != 2 {
1451 ctx.error(
1452 sub.span,
1453 format!(
1454 "ASSIGNMENT(=) interface subroutine must have 2 arguments, got {}",
1455 args.len()
1456 ),
1457 );
1458 }
1459 }
1460 _ => {
1461 ctx.error(sub.span, "unexpected program unit in operator interface");
1462 }
1463 }
1464 }
1465 InterfaceBody::ModuleProcedure(_) => {
1466 // Module procedures in operator interface — valid, can't check further
1467 // without resolving the procedure.
1468 }
1469 }
1470 }
1471 let _ = span;
1472 }
1473
1474 /// Validate a derived type definition.
1475 fn validate_derived_type(
1476 ctx: &mut Ctx,
1477 name: &str,
1478 type_attrs: &[TypeAttr],
1479 type_bound_procs: &[crate::ast::decl::TypeBoundProc],
1480 _components: &[crate::ast::decl::SpannedDecl],
1481 span: Span,
1482 ) {
1483 let is_abstract = type_attrs.iter().any(|a| matches!(a, TypeAttr::Abstract));
1484
1485 for tbp in type_bound_procs {
1486 // Deferred procedures only allowed in abstract types.
1487 let is_deferred = tbp.attrs.iter().any(|a| a.eq_ignore_ascii_case("deferred"));
1488 if is_deferred && !is_abstract {
1489 ctx.error(
1490 span,
1491 format!(
1492 "type-bound procedure '{}' is DEFERRED but type '{}' is not ABSTRACT",
1493 tbp.name, name
1494 ),
1495 );
1496 }
1497
1498 // PASS and NOPASS are mutually exclusive.
1499 let has_pass = tbp.attrs.iter().any(|a| {
1500 let lower = a.to_lowercase();
1501 lower == "pass" || lower.starts_with("pass(")
1502 });
1503 let has_nopass = tbp.attrs.iter().any(|a| a.eq_ignore_ascii_case("nopass"));
1504 if has_pass && has_nopass {
1505 ctx.error(
1506 span,
1507 format!(
1508 "type-bound procedure '{}' cannot have both PASS and NOPASS",
1509 tbp.name
1510 ),
1511 );
1512 }
1513
1514 // Deferred procedures must have an interface (binding).
1515 if is_deferred && tbp.binding.is_none() {
1516 ctx.error(
1517 span,
1518 format!(
1519 "DEFERRED type-bound procedure '{}' must specify an interface",
1520 tbp.name
1521 ),
1522 );
1523 }
1524 }
1525 }
1526
1527 /// Validate ASSOCIATE construct — check that associate names are not empty.
1528 fn validate_associate(
1529 ctx: &mut Ctx,
1530 assocs: &[(String, crate::ast::expr::SpannedExpr)],
1531 body: &[SpannedStmt],
1532 span: Span,
1533 ) {
1534 for (name, _expr) in assocs {
1535 if name.is_empty() {
1536 ctx.error(span, "ASSOCIATE name cannot be empty");
1537 }
1538 }
1539 validate_stmts(ctx, body);
1540 }
1541
1542 /// Extract the base variable name from an expression (handling subscripts and components).
1543 fn extract_base_name(expr: &crate::ast::expr::SpannedExpr) -> Option<String> {
1544 match &expr.node {
1545 Expr::Name { name } => Some(name.clone()),
1546 Expr::FunctionCall { callee, .. } => extract_base_name(callee),
1547 Expr::ComponentAccess { base, .. } => extract_base_name(base),
1548 _ => None,
1549 }
1550 }
1551
1552 // ---- IMPLICIT NONE enforcement ----
1553
1554 /// Check that all variable references in a statement list are declared
1555 /// when IMPLICIT NONE is active in the current scope.
1556 fn check_implicit_none(
1557 ctx: &mut Ctx,
1558 stmts: &[SpannedStmt],
1559 decls: &[crate::ast::decl::SpannedDecl],
1560 ) {
1561 if !ctx.st.is_implicit_none(ctx.scope_id) {
1562 return;
1563 }
1564
1565 // Collect declared names in this scope (from declarations).
1566 let mut declared: std::collections::HashSet<String> = std::collections::HashSet::new();
1567 extend_declared_names_from_decls(&mut declared, decls);
1568 // Also scan for INTERFACE blocks — function/subroutine names
1569 // declared in interfaces are valid in the current scope.
1570 // The interface bodies are stored as program units in the
1571 // ifaces/contains lists, not in decls. But the symbol table
1572 // should have them via resolve. We also check decls for
1573 // EXTERNAL statements.
1574 for decl in decls {
1575 if let Decl::TypeDecl {
1576 attrs, entities, ..
1577 } = &decl.node
1578 {
1579 if attrs.iter().any(|a| matches!(a, Attribute::External)) {
1580 for e in entities {
1581 declared.insert(e.name.to_lowercase());
1582 }
1583 }
1584 }
1585 }
1586
1587 let mut undeclared = Vec::new();
1588 let mut resolution_cache: std::collections::HashMap<String, bool> =
1589 std::collections::HashMap::new();
1590 let outer_implicit_letters: std::collections::HashSet<char> = std::collections::HashSet::new();
1591 for stmt in stmts {
1592 walk_stmt_for_undeclared(
1593 ctx.st,
1594 ctx.scope_id,
1595 stmt,
1596 &declared,
1597 &outer_implicit_letters,
1598 &mut resolution_cache,
1599 &mut undeclared,
1600 );
1601 }
1602
1603 // Deduplicate by name (only report each undeclared name once).
1604 let mut reported: std::collections::HashSet<String> = std::collections::HashSet::new();
1605 for (name, span) in &undeclared {
1606 let key = name.to_lowercase();
1607 if reported.insert(key) {
1608 ctx.error(
1609 *span,
1610 format!(
1611 "variable '{}' used but not declared (IMPLICIT NONE is active)",
1612 name
1613 ),
1614 );
1615 }
1616 }
1617 }
1618
1619 fn extend_declared_names_from_decls(
1620 declared: &mut std::collections::HashSet<String>,
1621 decls: &[crate::ast::decl::SpannedDecl],
1622 ) {
1623 for decl in decls {
1624 match &decl.node {
1625 Decl::TypeDecl { entities, .. } => {
1626 for e in entities {
1627 declared.insert(e.name.to_lowercase());
1628 }
1629 }
1630 // COMMON block variables are also declared.
1631 Decl::CommonBlock { vars, .. } => {
1632 for v in vars {
1633 declared.insert(v.to_lowercase());
1634 }
1635 }
1636 _ => {}
1637 }
1638 }
1639 }
1640
1641 fn extend_declared_names_from_ifaces(
1642 declared: &mut std::collections::HashSet<String>,
1643 ifaces: &[crate::ast::unit::SpannedUnit],
1644 ) {
1645 use crate::ast::unit::{InterfaceBody, ProgramUnit};
1646
1647 for iface in ifaces {
1648 let ProgramUnit::InterfaceBlock { bodies, .. } = &iface.node else {
1649 continue;
1650 };
1651 for body in bodies {
1652 match body {
1653 InterfaceBody::Subprogram(sub) => match &sub.node {
1654 ProgramUnit::Function { name, .. } | ProgramUnit::Subroutine { name, .. } => {
1655 declared.insert(name.to_lowercase());
1656 }
1657 _ => {}
1658 },
1659 InterfaceBody::ModuleProcedure(names) => {
1660 for name in names {
1661 declared.insert(name.to_lowercase());
1662 }
1663 }
1664 }
1665 }
1666 }
1667 }
1668
1669 fn walk_stmt_for_undeclared(
1670 st: &SymbolTable,
1671 scope_id: ScopeId,
1672 stmt: &SpannedStmt,
1673 declared: &std::collections::HashSet<String>,
1674 implicit_letters: &std::collections::HashSet<char>,
1675 resolution_cache: &mut std::collections::HashMap<String, bool>,
1676 undeclared: &mut Vec<(String, Span)>,
1677 ) {
1678 macro_rules! chk {
1679 ($e:expr) => {
1680 check_expr_names(
1681 st,
1682 scope_id,
1683 $e,
1684 declared,
1685 implicit_letters,
1686 resolution_cache,
1687 undeclared,
1688 )
1689 };
1690 }
1691 macro_rules! recurse {
1692 ($s:expr) => {
1693 walk_stmt_for_undeclared(
1694 st,
1695 scope_id,
1696 $s,
1697 declared,
1698 implicit_letters,
1699 resolution_cache,
1700 undeclared,
1701 )
1702 };
1703 }
1704 match &stmt.node {
1705 Stmt::Assignment { target, value } => {
1706 chk!(target);
1707 chk!(value);
1708 }
1709 Stmt::PointerAssignment { target, value, .. } => {
1710 chk!(target);
1711 chk!(value);
1712 }
1713 Stmt::Print { items, .. } => {
1714 for item in items {
1715 chk!(item);
1716 }
1717 }
1718 Stmt::Write {
1719 items, controls, ..
1720 } => {
1721 for item in items {
1722 chk!(item);
1723 }
1724 for ctrl in controls {
1725 chk!(&ctrl.value);
1726 }
1727 }
1728 Stmt::Read {
1729 items, controls, ..
1730 } => {
1731 for item in items {
1732 chk!(item);
1733 }
1734 for ctrl in controls {
1735 chk!(&ctrl.value);
1736 }
1737 }
1738 Stmt::IfConstruct {
1739 condition,
1740 then_body,
1741 else_ifs,
1742 else_body,
1743 ..
1744 } => {
1745 chk!(condition);
1746 for s in then_body {
1747 recurse!(s);
1748 }
1749 for (cond, body) in else_ifs {
1750 chk!(cond);
1751 for s in body {
1752 recurse!(s);
1753 }
1754 }
1755 if let Some(body) = else_body {
1756 for s in body {
1757 recurse!(s);
1758 }
1759 }
1760 }
1761 Stmt::IfStmt { condition, action } => {
1762 chk!(condition);
1763 recurse!(action);
1764 }
1765 Stmt::DoLoop { body, .. }
1766 | Stmt::DoWhile { body, .. }
1767 | Stmt::DoConcurrent { body, .. } => {
1768 for s in body {
1769 recurse!(s);
1770 }
1771 }
1772 Stmt::Block {
1773 uses,
1774 ifaces,
1775 implicit,
1776 decls,
1777 body,
1778 ..
1779 } => {
1780 // F2018 §11.1.4: a BLOCK construct establishes its own
1781 // scope with an independent implicit-typing environment.
1782 // Layer the block's declared names AND any IMPLICIT
1783 // statements over the inherited rules; the local set
1784 // does not leak back out.
1785 let mut block_declared = declared.clone();
1786 block_declared.extend(block_use_imported_names(st, uses));
1787 extend_declared_names_from_decls(&mut block_declared, decls);
1788 extend_declared_names_from_ifaces(&mut block_declared, ifaces);
1789 let mut block_implicit = implicit_letters.clone();
1790 let mut block_implicit_none = false;
1791 for d in implicit {
1792 match &d.node {
1793 crate::ast::decl::Decl::ImplicitNone { .. } => {
1794 block_implicit_none = true;
1795 }
1796 crate::ast::decl::Decl::ImplicitStmt { specs } => {
1797 for spec in specs {
1798 for &(start, end) in &spec.ranges {
1799 for letter_byte in start as u8..=end as u8 {
1800 let letter = (letter_byte as char).to_ascii_lowercase();
1801 block_implicit.insert(letter);
1802 }
1803 }
1804 }
1805 }
1806 _ => {}
1807 }
1808 }
1809 // An IMPLICIT NONE inside the block clears the inherited
1810 // letter set rather than augmenting it. Subsequent
1811 // IMPLICIT statements in the same block (rare but legal)
1812 // re-establish a covering range from scratch.
1813 if block_implicit_none {
1814 block_implicit.clear();
1815 for d in implicit {
1816 if let crate::ast::decl::Decl::ImplicitStmt { specs } = &d.node {
1817 for spec in specs {
1818 for &(start, end) in &spec.ranges {
1819 for letter_byte in start as u8..=end as u8 {
1820 let letter = (letter_byte as char).to_ascii_lowercase();
1821 block_implicit.insert(letter);
1822 }
1823 }
1824 }
1825 }
1826 }
1827 }
1828 for s in body {
1829 walk_stmt_for_undeclared(
1830 st,
1831 scope_id,
1832 s,
1833 &block_declared,
1834 &block_implicit,
1835 resolution_cache,
1836 undeclared,
1837 );
1838 }
1839 }
1840 Stmt::SelectCase {
1841 selector, cases, ..
1842 } => {
1843 chk!(selector);
1844 for case in cases {
1845 for s in &case.body {
1846 recurse!(s);
1847 }
1848 }
1849 }
1850 Stmt::Call { args, .. } => {
1851 for arg in args {
1852 if let crate::ast::expr::SectionSubscript::Element(e) = &arg.value {
1853 chk!(e);
1854 }
1855 }
1856 }
1857 Stmt::Labeled { stmt: inner, .. } => {
1858 recurse!(inner);
1859 }
1860 Stmt::WhereConstruct {
1861 mask,
1862 body,
1863 elsewhere,
1864 ..
1865 } => {
1866 chk!(mask);
1867 for s in body {
1868 recurse!(s);
1869 }
1870 for (m, b) in elsewhere {
1871 if let Some(m) = m {
1872 chk!(m);
1873 }
1874 for s in b {
1875 recurse!(s);
1876 }
1877 }
1878 }
1879 _ => {}
1880 }
1881 }
1882
1883 fn block_use_imported_names(
1884 st: &SymbolTable,
1885 uses: &[crate::ast::decl::SpannedDecl],
1886 ) -> std::collections::HashSet<String> {
1887 use crate::ast::decl::OnlyItem;
1888 use crate::sema::symtab::Access;
1889
1890 let mut imported = std::collections::HashSet::new();
1891 for use_decl in uses {
1892 let crate::ast::decl::Decl::UseStmt {
1893 module,
1894 renames,
1895 only,
1896 ..
1897 } = &use_decl.node
1898 else {
1899 continue;
1900 };
1901 if let Some(only_items) = only {
1902 for item in only_items {
1903 match item {
1904 OnlyItem::Name(name) => {
1905 imported.insert(name.to_lowercase());
1906 }
1907 OnlyItem::Rename(rename) => {
1908 imported.insert(rename.local.to_lowercase());
1909 }
1910 }
1911 }
1912 continue;
1913 }
1914
1915 if let Some(scope_id) = st.find_module_scope(module) {
1916 for sym in st.scope(scope_id).symbols.values() {
1917 if sym.attrs.access != Access::Private {
1918 imported.insert(sym.name.to_lowercase());
1919 }
1920 }
1921 }
1922 for rename in renames {
1923 imported.insert(rename.local.to_lowercase());
1924 }
1925 }
1926 imported
1927 }
1928
1929 /// Walk an expression and collect undeclared Name references.
1930 fn check_expr_names(
1931 st: &SymbolTable,
1932 scope_id: ScopeId,
1933 expr: &crate::ast::expr::SpannedExpr,
1934 declared: &std::collections::HashSet<String>,
1935 implicit_letters: &std::collections::HashSet<char>,
1936 resolution_cache: &mut std::collections::HashMap<String, bool>,
1937 undeclared: &mut Vec<(String, Span)>,
1938 ) {
1939 match &expr.node {
1940 Expr::Name { name } => {
1941 let key = name.to_lowercase();
1942 // Skip format specifier * (appears in WRITE(*, *) / READ(*, *)).
1943 if key == "*" {
1944 return;
1945 }
1946 if declared.contains(&key) {
1947 return;
1948 }
1949 if is_intrinsic_name(&key) {
1950 return;
1951 }
1952 // F2018 §11.1.4: a BLOCK-scoped IMPLICIT statement gives
1953 // names whose first letter is in the covered range an
1954 // implicit type, even if the enclosing scope is
1955 // IMPLICIT NONE.
1956 if let Some(first) = key.chars().next() {
1957 if implicit_letters.contains(&first.to_ascii_lowercase()) {
1958 return;
1959 }
1960 }
1961 if *resolution_cache
1962 .entry(key.clone())
1963 .or_insert_with(|| st.lookup_in(scope_id, &key).is_some())
1964 {
1965 return;
1966 }
1967 undeclared.push((name.clone(), expr.span));
1968 }
1969 Expr::BinaryOp { left, right, .. } => {
1970 check_expr_names(
1971 st,
1972 scope_id,
1973 left,
1974 declared,
1975 implicit_letters,
1976 resolution_cache,
1977 undeclared,
1978 );
1979 check_expr_names(
1980 st,
1981 scope_id,
1982 right,
1983 declared,
1984 implicit_letters,
1985 resolution_cache,
1986 undeclared,
1987 );
1988 }
1989 Expr::UnaryOp { operand, .. } => {
1990 check_expr_names(
1991 st,
1992 scope_id,
1993 operand,
1994 declared,
1995 implicit_letters,
1996 resolution_cache,
1997 undeclared,
1998 );
1999 }
2000 Expr::FunctionCall { callee, args } => {
2001 // Under IMPLICIT NONE the callee name must resolve to a
2002 // declared identifier: a host/module procedure visible
2003 // via `lookup_in`, an EXTERNAL dummy (already in
2004 // `declared`), or an intrinsic. The `declared` set and
2005 // lookup path in the bare-Name arm handle all three;
2006 // reuse it so `foo(3)` with no declaration of `foo` is
2007 // rejected at compile time instead of falling through to
2008 // a link error.
2009 check_expr_names(
2010 st,
2011 scope_id,
2012 callee,
2013 declared,
2014 implicit_letters,
2015 resolution_cache,
2016 undeclared,
2017 );
2018 for arg in args {
2019 if let crate::ast::expr::SectionSubscript::Element(e) = &arg.value {
2020 check_expr_names(
2021 st,
2022 scope_id,
2023 e,
2024 declared,
2025 implicit_letters,
2026 resolution_cache,
2027 undeclared,
2028 );
2029 }
2030 }
2031 }
2032 Expr::ComponentAccess { base, .. } => {
2033 check_expr_names(
2034 st,
2035 scope_id,
2036 base,
2037 declared,
2038 implicit_letters,
2039 resolution_cache,
2040 undeclared,
2041 );
2042 }
2043 Expr::ParenExpr { inner } => {
2044 check_expr_names(
2045 st,
2046 scope_id,
2047 inner,
2048 declared,
2049 implicit_letters,
2050 resolution_cache,
2051 undeclared,
2052 );
2053 }
2054 _ => {}
2055 }
2056 }
2057
2058 pub fn is_intrinsic_name(name: &str) -> bool {
2059 matches!(
2060 name,
2061 "abs" | "iabs" | "dabs" | "cabs" | "acos" | "asin" | "atan" | "atan2" |
2062 "cos" | "sin" | "tan" | "exp" | "log" | "log10" | "sqrt" | "dsqrt" |
2063 "mod" | "modulo" | "max" | "min" | "sign" | "dim" |
2064 "int" | "nint" | "real" | "dble" | "cmplx" | "conjg" |
2065 "aimag" | "dimag" | "char" | "ichar" | "achar" | "iachar" |
2066 "len" | "len_trim" | "trim" | "adjustl" | "adjustr" |
2067 "index" | "scan" | "verify" | "repeat" | "lge" | "lgt" | "lle" | "llt" |
2068 "kind" | "selected_int_kind" | "selected_real_kind" |
2069 "size" | "shape" | "lbound" | "ubound" | "allocated" | "associated" |
2070 "present" | "merge" | "pack" | "unpack" | "spread" | "reshape" |
2071 "sum" | "product" | "maxval" | "minval" | "count" | "any" | "all" |
2072 "matmul" | "dot_product" | "transpose" |
2073 "huge" | "tiny" | "epsilon" | "precision" | "range" | "radix" |
2074 "maxexponent" | "minexponent" | "digits" | "bit_size" |
2075 "floor" | "ceiling" | "fraction" | "exponent" | "scale" |
2076 "ibset" | "ibclr" | "ibits" | "btest" | "iand" | "ior" | "ieor" | "not" |
2077 "ishft" | "ishftc" | "mvbits" | "transfer" |
2078 "new_line" | "null" | "move_alloc" |
2079 "system_clock" | "date_and_time" | "cpu_time" | "random_number" | "random_seed" |
2080 "command_argument_count" | "get_command_argument" | "get_environment_variable" |
2081 "execute_command_line" | "compiler_version" | "compiler_options" |
2082 "c_loc" | "c_funloc" | "c_f_pointer" | "c_associated" | "c_sizeof" |
2083 "ieee_is_nan" | "ieee_is_finite" | "ieee_value" |
2084 "ieee_support_datatype" | "ieee_support_denormal" |
2085 "ieee_selected_real_kind" |
2086 // Statement-like names that can appear in expression context
2087 "float" | "dfloat" | "sngl" | "idint" | "ifix" | "idnint" |
2088 "dprod" | "dmax1" | "dmin1" | "max0" | "min0" | "max1" | "min1" |
2089 "amax0" | "amin0" | "amax1" | "amin1"
2090 )
2091 }
2092
2093 #[cfg(test)]
2094 mod tests {
2095 use super::*;
2096 use crate::lexer::Lexer;
2097 use crate::parser::Parser;
2098 use crate::sema::resolve;
2099
2100 fn validate_source(src: &str) -> Vec<Diagnostic> {
2101 let tokens = Lexer::tokenize(src, 0).unwrap();
2102 let mut parser = Parser::new(&tokens);
2103 let units = parser.parse_file().unwrap();
2104 let rr = resolve::resolve_file(&units, &[]).unwrap();
2105 let st = rr.st;
2106 validate_file(&units, &st)
2107 }
2108
2109 fn errors_from(src: &str) -> Vec<String> {
2110 validate_source(src)
2111 .iter()
2112 .filter(|d| d.kind == DiagKind::Error)
2113 .map(|d| d.msg.clone())
2114 .collect()
2115 }
2116
2117 fn errors_with_std(src: &str, std: FortranStandard) -> Vec<String> {
2118 let tokens = Lexer::tokenize(src, 0).unwrap();
2119 let mut parser = Parser::new(&tokens);
2120 let units = parser.parse_file().unwrap();
2121 let rr = resolve::resolve_file(&units, &[]).unwrap();
2122 let st = rr.st;
2123 validate_file_with_std(&units, &st, Some(std))
2124 .iter()
2125 .filter(|d| d.kind == DiagKind::Error)
2126 .map(|d| d.msg.clone())
2127 .collect()
2128 }
2129
2130 // ---- Intent enforcement ----
2131
2132 #[test]
2133 fn assign_to_intent_in_errors() {
2134 let errs = errors_from(
2135 "\
2136 subroutine foo(x)
2137 real, intent(in) :: x
2138 x = 1.0
2139 end subroutine
2140 ",
2141 );
2142 assert!(errs.iter().any(|e| e.contains("intent(in)")));
2143 }
2144
2145 #[test]
2146 fn assign_to_intent_inout_ok() {
2147 let errs = errors_from(
2148 "\
2149 subroutine foo(x)
2150 real, intent(inout) :: x
2151 x = 1.0
2152 end subroutine
2153 ",
2154 );
2155 assert!(errs.is_empty());
2156 }
2157
2158 #[test]
2159 fn assign_through_intent_in_pointer_target_ok() {
2160 let errs = errors_from(
2161 "\
2162 module m
2163 type :: t
2164 integer :: x
2165 end type
2166 contains
2167 subroutine foo(p)
2168 type(t), pointer, intent(in) :: p
2169 p%x = 1
2170 end subroutine
2171 end module
2172 ",
2173 );
2174 assert!(!errs.iter().any(|e| e.contains("intent(in)")), "{:?}", errs);
2175 }
2176
2177 #[test]
2178 fn assign_to_parameter_errors() {
2179 let errs = errors_from(
2180 "\
2181 program test
2182 implicit none
2183 integer, parameter :: n = 10
2184 n = 20
2185 end program
2186 ",
2187 );
2188 assert!(errs.iter().any(|e| e.contains("named constant")));
2189 }
2190
2191 // ---- Allocatable / pointer ----
2192
2193 #[test]
2194 fn allocate_non_allocatable_errors() {
2195 let errs = errors_from(
2196 "\
2197 program test
2198 implicit none
2199 real :: x(10)
2200 allocate(x(20))
2201 end program
2202 ",
2203 );
2204 assert!(errs.iter().any(|e| e.contains("allocatable or pointer")));
2205 }
2206
2207 #[test]
2208 fn allocate_allocatable_ok() {
2209 let errs = errors_from(
2210 "\
2211 program test
2212 implicit none
2213 real, allocatable :: x(:)
2214 allocate(x(10))
2215 end program
2216 ",
2217 );
2218 assert!(errs.is_empty());
2219 }
2220
2221 #[test]
2222 fn allocatable_and_pointer_forbidden() {
2223 let errs = errors_from(
2224 "\
2225 program test
2226 implicit none
2227 real, allocatable, pointer :: x
2228 end program
2229 ",
2230 );
2231 assert!(errs
2232 .iter()
2233 .any(|e| e.contains("both allocatable and pointer")));
2234 }
2235
2236 #[test]
2237 fn parameter_allocatable_forbidden() {
2238 let errs = errors_from(
2239 "\
2240 program test
2241 implicit none
2242 integer, parameter, allocatable :: x = 10
2243 end program
2244 ",
2245 );
2246 assert!(errs
2247 .iter()
2248 .any(|e| e.contains("parameter") && e.contains("allocatable")));
2249 }
2250
2251 // ---- Pointer assignment ----
2252
2253 #[test]
2254 fn pointer_assignment_non_pointer_errors() {
2255 let errs = errors_from(
2256 "\
2257 program test
2258 implicit none
2259 real :: x
2260 real, target :: y
2261 x => y
2262 end program
2263 ",
2264 );
2265 assert!(errs.iter().any(|e| e.contains("pointer attribute")));
2266 }
2267
2268 #[test]
2269 fn pointer_assignment_non_target_errors() {
2270 let errs = errors_from(
2271 "\
2272 program test
2273 implicit none
2274 real, pointer :: p
2275 real :: x
2276 p => x
2277 end program
2278 ",
2279 );
2280 assert!(errs.iter().any(|e| e.contains("target or pointer")));
2281 }
2282
2283 #[test]
2284 fn pointer_assignment_ok() {
2285 let errs = errors_from(
2286 "\
2287 program test
2288 implicit none
2289 real, pointer :: p
2290 real, target :: x
2291 p => x
2292 end program
2293 ",
2294 );
2295 assert!(errs.is_empty());
2296 }
2297
2298 // ---- Pure constraints ----
2299
2300 #[test]
2301 fn io_in_pure_errors() {
2302 let errs = errors_from(
2303 "\
2304 pure subroutine foo(x)
2305 real, intent(in) :: x
2306 print *, x
2307 end subroutine
2308 ",
2309 );
2310 assert!(errs.iter().any(|e| e.contains("I/O") && e.contains("pure")));
2311 }
2312
2313 #[test]
2314 fn stop_in_pure_errors() {
2315 let errs = errors_from(
2316 "\
2317 pure function bar(x) result(y)
2318 real, intent(in) :: x
2319 real :: y
2320 y = x
2321 stop
2322 end function
2323 ",
2324 );
2325 assert!(errs
2326 .iter()
2327 .any(|e| e.contains("STOP") && e.contains("pure")));
2328 }
2329
2330 #[test]
2331 fn save_in_pure_errors() {
2332 let errs = errors_from(
2333 "\
2334 pure subroutine foo(x)
2335 real, intent(in) :: x
2336 real, save :: counter
2337 end subroutine
2338 ",
2339 );
2340 assert!(errs
2341 .iter()
2342 .any(|e| e.contains("SAVE") && e.contains("pure")));
2343 }
2344
2345 #[test]
2346 fn pure_without_violations_ok() {
2347 let errs = errors_from(
2348 "\
2349 pure function square(x) result(y)
2350 real, intent(in) :: x
2351 real :: y
2352 y = x * x
2353 end function
2354 ",
2355 );
2356 assert!(errs.is_empty());
2357 }
2358
2359 #[test]
2360 fn pure_write_to_module_variable_errors() {
2361 let errs = errors_from(
2362 "\
2363 module m
2364 integer :: counter = 0
2365 contains
2366 pure integer function writes_counter() result(r)
2367 counter = 99
2368 r = counter
2369 end function
2370 end module
2371 ",
2372 );
2373 assert!(
2374 errs.iter().any(|e| e.contains("counter")
2375 && e.contains("pure")
2376 && e.contains("host or use association")),
2377 "expected pure+module-write error, got {:?}",
2378 errs,
2379 );
2380 }
2381
2382 #[test]
2383 fn pure_read_of_module_variable_ok() {
2384 // F2018 15.7 permits a pure procedure to *reference* a
2385 // variable accessed by use association; only definition
2386 // is forbidden. reads_counter is a legal pure function.
2387 let errs = errors_from(
2388 "\
2389 module m
2390 integer :: counter = 0
2391 contains
2392 pure integer function reads_counter() result(r)
2393 r = counter
2394 end function
2395 end module
2396 ",
2397 );
2398 assert!(
2399 errs.is_empty(),
2400 "pure read of module variable should be legal, got {:?}",
2401 errs
2402 );
2403 }
2404
2405 #[test]
2406 fn pure_write_to_host_variable_errors() {
2407 let errs = errors_from(
2408 "\
2409 program p
2410 integer :: host_var
2411 host_var = 0
2412 call helper()
2413 contains
2414 pure subroutine helper()
2415 host_var = 42
2416 end subroutine
2417 end program
2418 ",
2419 );
2420 assert!(
2421 errs.iter().any(|e| e.contains("host_var")
2422 && e.contains("pure")
2423 && e.contains("host or use association")),
2424 "expected pure+host-write error, got {:?}",
2425 errs,
2426 );
2427 }
2428
2429 #[test]
2430 fn pure_pointer_reassoc_of_module_pointer_errors() {
2431 let errs = errors_from(
2432 "\
2433 module m
2434 integer, pointer :: module_p
2435 contains
2436 pure subroutine reassoc(t)
2437 integer, target, intent(in) :: t
2438 module_p => t
2439 end subroutine
2440 end module
2441 ",
2442 );
2443 assert!(
2444 errs.iter().any(|e| e.contains("module_p")
2445 && e.contains("pure")
2446 && e.contains("pointer assignment")),
2447 "expected pure+module-pointer error, got {:?}",
2448 errs,
2449 );
2450 }
2451
2452 #[test]
2453 fn pure_local_pointer_reassoc_ok() {
2454 // Associating a LOCAL pointer with a module TARGET is
2455 // legal — `q => counter` does not modify `counter`.
2456 let errs = errors_from(
2457 "\
2458 module m
2459 integer, target :: counter = 0
2460 contains
2461 pure integer function associates_counter() result(r)
2462 integer, pointer :: q
2463 q => counter
2464 r = 0
2465 end function
2466 end module
2467 ",
2468 );
2469 assert!(
2470 errs.is_empty(),
2471 "pure local pointer reassoc should be legal, got {:?}",
2472 errs
2473 );
2474 }
2475
2476 #[test]
2477 fn pure_intent_out_dummy_ok() {
2478 let errs = errors_from(
2479 "\
2480 pure subroutine zero_it(x)
2481 integer, intent(out) :: x
2482 x = 0
2483 end subroutine
2484 ",
2485 );
2486 assert!(
2487 errs.is_empty(),
2488 "pure write to intent(out) dummy should be legal, got {:?}",
2489 errs
2490 );
2491 }
2492
2493 // ---- Deferred length character ----
2494
2495 #[test]
2496 fn deferred_len_without_allocatable_errors() {
2497 let errs = errors_from(
2498 "\
2499 program test
2500 implicit none
2501 character(len=:) :: s
2502 end program
2503 ",
2504 );
2505 assert!(errs.iter().any(|e| e.contains("deferred-length")));
2506 }
2507
2508 #[test]
2509 fn deferred_len_with_allocatable_ok() {
2510 let errs = errors_from(
2511 "\
2512 program test
2513 implicit none
2514 character(len=:), allocatable :: s
2515 end program
2516 ",
2517 );
2518 assert!(errs.is_empty());
2519 }
2520
2521 // ---- Label validation ----
2522 // Note: the parser does not yet assign labels to statements (labels are
2523 // separate tokens consumed but not attached). Full GOTO-target validation
2524 // requires a parser enhancement to track statement labels. The validation
2525 // infrastructure is in place; these tests verify the diagnostic machinery
2526 // using the programmatic API directly.
2527
2528 #[test]
2529 fn goto_undefined_label_detected() {
2530 // Test the label validation infrastructure directly.
2531 use crate::lexer::{Position, Span};
2532 let st = SymbolTable::new();
2533 let mut ctx = Ctx::new(&st, None, false, false);
2534 let span = Span {
2535 file_id: 0,
2536 start: Position { line: 1, col: 1 },
2537 end: Position { line: 1, col: 1 },
2538 };
2539
2540 // Reference label 999 but don't define it.
2541 ctx.labels_referenced.push((999, span));
2542 validate_label_consistency(&mut ctx, span);
2543 assert!(ctx.diags.iter().any(|d| d.msg.contains("label 999")));
2544 }
2545
2546 #[test]
2547 fn goto_defined_label_no_error() {
2548 use crate::lexer::{Position, Span};
2549 let st = SymbolTable::new();
2550 let mut ctx = Ctx::new(&st, None, false, false);
2551 let span = Span {
2552 file_id: 0,
2553 start: Position { line: 1, col: 1 },
2554 end: Position { line: 1, col: 1 },
2555 };
2556
2557 ctx.labels_defined.push(10);
2558 ctx.labels_referenced.push((10, span));
2559 validate_label_consistency(&mut ctx, span);
2560 assert!(ctx.diags.is_empty());
2561 }
2562
2563 #[test]
2564 fn duplicate_label_detected() {
2565 use crate::lexer::{Position, Span};
2566 let st = SymbolTable::new();
2567 let mut ctx = Ctx::new(&st, None, false, false);
2568 let span = Span {
2569 file_id: 0,
2570 start: Position { line: 1, col: 1 },
2571 end: Position { line: 1, col: 1 },
2572 };
2573
2574 register_label(&mut ctx, 10, span);
2575 register_label(&mut ctx, 10, span); // duplicate
2576 assert!(ctx.diags.iter().any(|d| d.msg.contains("duplicate label")));
2577 }
2578
2579 // ---- Valid code produces no errors ----
2580
2581 #[test]
2582 fn clean_program_no_errors() {
2583 let errs = errors_from(
2584 "\
2585 program test
2586 implicit none
2587 integer :: i, n
2588 real :: x
2589 n = 10
2590 do i = 1, n
2591 x = real(i) * 2.0
2592 end do
2593 end program
2594 ",
2595 );
2596 assert!(errs.is_empty(), "unexpected errors: {:?}", errs);
2597 }
2598
2599 #[test]
2600 fn module_with_subroutine_no_errors() {
2601 let errs = errors_from(
2602 "\
2603 module mymod
2604 implicit none
2605 integer :: shared
2606 contains
2607 subroutine update(val)
2608 integer, intent(in) :: val
2609 shared = val
2610 end subroutine
2611 end module
2612 ",
2613 );
2614 assert!(errs.is_empty(), "unexpected errors: {:?}", errs);
2615 }
2616
2617 #[test]
2618 fn module_parameter_visible_in_contained_subroutine() {
2619 let errs = errors_from(
2620 "\
2621 module m
2622 use iso_c_binding, only: c_int
2623 implicit none
2624 private
2625 public :: s
2626 integer, parameter :: color_red = 31
2627 contains
2628 subroutine s()
2629 use iso_c_binding, only: c_int
2630 print *, color_red
2631 end subroutine
2632 end module
2633 ",
2634 );
2635 assert!(errs.is_empty(), "unexpected errors: {:?}", errs);
2636 }
2637
2638 // ---- Defined operator validation ----
2639 // Note: the parser doesn't yet support interface blocks in the module
2640 // specification section (they must appear as top-level units or in
2641 // CONTAINS). These tests use the validation API directly.
2642
2643 #[test]
2644 fn operator_interface_subroutine_errors() {
2645 // Parse a top-level interface block with operator name.
2646 let errs = errors_from(
2647 "\
2648 interface operator(+)
2649 subroutine bad_add(a, b)
2650 integer, intent(in) :: a, b
2651 end subroutine
2652 end interface
2653 ",
2654 );
2655 assert!(errs
2656 .iter()
2657 .any(|e| e.contains("functions, not subroutines")));
2658 }
2659
2660 #[test]
2661 fn operator_interface_wrong_arg_count() {
2662 let errs = errors_from(
2663 "\
2664 interface operator(+)
2665 function add3(a, b, c) result(r)
2666 integer, intent(in) :: a, b, c
2667 integer :: r
2668 end function
2669 end interface
2670 ",
2671 );
2672 assert!(errs.iter().any(|e| e.contains("1 or 2 arguments")));
2673 }
2674
2675 #[test]
2676 fn operator_interface_valid_binary() {
2677 let errs = errors_from(
2678 "\
2679 interface operator(+)
2680 function add_vec(a, b) result(c)
2681 integer, intent(in) :: a, b
2682 integer :: c
2683 end function
2684 end interface
2685 ",
2686 );
2687 assert!(errs.is_empty(), "unexpected errors: {:?}", errs);
2688 }
2689
2690 #[test]
2691 fn assignment_interface_function_errors() {
2692 let errs = errors_from(
2693 "\
2694 interface assignment(=)
2695 function bad_assign(a, b) result(c)
2696 integer, intent(in) :: a, b
2697 integer :: c
2698 end function
2699 end interface
2700 ",
2701 );
2702 assert!(errs
2703 .iter()
2704 .any(|e| e.contains("subroutines, not functions")));
2705 }
2706
2707 #[test]
2708 fn assignment_interface_wrong_arg_count() {
2709 let errs = errors_from(
2710 "\
2711 interface assignment(=)
2712 subroutine bad_assign(a, b, c)
2713 integer, intent(inout) :: a
2714 integer, intent(in) :: b, c
2715 end subroutine
2716 end interface
2717 ",
2718 );
2719 assert!(errs.iter().any(|e| e.contains("2 arguments")));
2720 }
2721
2722 // ---- Derived type validation ----
2723
2724 #[test]
2725 fn deferred_in_non_abstract_errors() {
2726 let errs = errors_from(
2727 "\
2728 module m
2729 implicit none
2730 type :: shape
2731 contains
2732 procedure, deferred :: area
2733 end type
2734 end module
2735 ",
2736 );
2737 assert!(errs
2738 .iter()
2739 .any(|e| e.contains("DEFERRED") && e.contains("not ABSTRACT")));
2740 }
2741
2742 #[test]
2743 fn deferred_in_abstract_ok() {
2744 let errs = errors_from(
2745 "\
2746 module m
2747 implicit none
2748 type, abstract :: shape
2749 contains
2750 procedure, deferred :: area
2751 end type
2752 end module
2753 ",
2754 );
2755 // No error for deferred in abstract type (the "must specify interface"
2756 // error is expected since our parser stores binding as None for simple
2757 // deferred procedures — that's a parser representation issue).
2758 assert!(!errs.iter().any(|e| e.contains("not ABSTRACT")));
2759 }
2760
2761 #[test]
2762 fn pass_and_nopass_together_errors() {
2763 let errs = errors_from(
2764 "\
2765 module m
2766 implicit none
2767 type :: thing
2768 contains
2769 procedure, pass, nopass :: method
2770 end type
2771 end module
2772 ",
2773 );
2774 assert!(errs.iter().any(|e| e.contains("both PASS and NOPASS")));
2775 }
2776
2777 // ---- Standard conformance (--std=) ----
2778
2779 #[test]
2780 fn do_concurrent_requires_f2008() {
2781 let errs = errors_with_std(
2782 "\
2783 program test
2784 implicit none
2785 integer :: i
2786 do concurrent (i = 1:10)
2787 end do
2788 end program
2789 ",
2790 FortranStandard::F95,
2791 );
2792 assert!(errs
2793 .iter()
2794 .any(|e| e.contains("DO CONCURRENT") && e.contains("F2008")));
2795 }
2796
2797 #[test]
2798 fn do_concurrent_ok_with_f2008() {
2799 let errs = errors_with_std(
2800 "\
2801 program test
2802 implicit none
2803 integer :: i
2804 do concurrent (i = 1:10)
2805 end do
2806 end program
2807 ",
2808 FortranStandard::F2008,
2809 );
2810 assert!(!errs.iter().any(|e| e.contains("DO CONCURRENT")));
2811 }
2812
2813 #[test]
2814 fn error_stop_requires_f2008() {
2815 let errs = errors_with_std(
2816 "\
2817 program test
2818 implicit none
2819 error stop
2820 end program
2821 ",
2822 FortranStandard::F95,
2823 );
2824 assert!(errs
2825 .iter()
2826 .any(|e| e.contains("ERROR STOP") && e.contains("F2008")));
2827 }
2828
2829 #[test]
2830 fn block_construct_requires_f2008() {
2831 let errs = errors_with_std(
2832 "\
2833 program test
2834 implicit none
2835 block
2836 x = 1
2837 end block
2838 end program
2839 ",
2840 FortranStandard::F95,
2841 );
2842 assert!(errs
2843 .iter()
2844 .any(|e| e.contains("BLOCK") && e.contains("F2008")));
2845 }
2846
2847 #[test]
2848 fn associate_requires_f2003() {
2849 let errs = errors_with_std(
2850 "\
2851 program test
2852 implicit none
2853 integer :: n
2854 n = 10
2855 associate (m => n)
2856 end associate
2857 end program
2858 ",
2859 FortranStandard::F95,
2860 );
2861 assert!(errs
2862 .iter()
2863 .any(|e| e.contains("ASSOCIATE") && e.contains("F2003")));
2864 }
2865
2866 #[test]
2867 fn no_std_violations_when_unset() {
2868 // With no --std= set, everything is allowed.
2869 let errs = errors_from(
2870 "\
2871 program test
2872 implicit none
2873 integer :: i
2874 do concurrent (i = 1:10)
2875 end do
2876 block
2877 x = 1
2878 end block
2879 end program
2880 ",
2881 );
2882 assert!(!errs.iter().any(|e| e.contains("requires")));
2883 }
2884
2885 #[test]
2886 fn impure_requires_f2008() {
2887 let errs = errors_with_std(
2888 "\
2889 impure subroutine s()
2890 end subroutine
2891 ",
2892 FortranStandard::F95,
2893 );
2894 assert!(errs
2895 .iter()
2896 .any(|e| e.contains("IMPURE") && e.contains("F2008")));
2897 }
2898
2899 #[test]
2900 fn submodule_requires_f2008() {
2901 use crate::lexer::{Position, Span};
2902
2903 let span = Span {
2904 file_id: 0,
2905 start: Position { line: 1, col: 1 },
2906 end: Position { line: 1, col: 1 },
2907 };
2908 let unit = crate::ast::Spanned::new(
2909 ProgramUnit::Submodule {
2910 parent: "parent_mod".into(),
2911 ancestor: None,
2912 name: "child_mod".into(),
2913 uses: vec![],
2914 decls: vec![],
2915 contains: vec![],
2916 },
2917 span,
2918 );
2919 let diags =
2920 validate_file_with_std(&[unit], &SymbolTable::new(), Some(FortranStandard::F95));
2921 let errs: Vec<_> = diags
2922 .into_iter()
2923 .filter(|d| d.kind == DiagKind::Error)
2924 .map(|d| d.msg)
2925 .collect();
2926 assert!(errs
2927 .iter()
2928 .any(|e| e.contains("SUBMODULE") && e.contains("F2008")));
2929 }
2930
2931 #[test]
2932 fn abstract_type_requires_f2003() {
2933 let errs = errors_with_std(
2934 "\
2935 module m
2936 type, abstract :: shape
2937 end type shape
2938 end module
2939 ",
2940 FortranStandard::F95,
2941 );
2942 assert!(errs
2943 .iter()
2944 .any(|e| e.contains("ABSTRACT type") && e.contains("F2003")));
2945 }
2946
2947 #[test]
2948 fn class_star_requires_f2018() {
2949 let errs = errors_with_std(
2950 "\
2951 subroutine s(x)
2952 class(*) :: x
2953 end subroutine
2954 ",
2955 FortranStandard::F2008,
2956 );
2957 assert!(errs
2958 .iter()
2959 .any(|e| e.contains("CLASS(*)/TYPE(*) declaration") && e.contains("F2018")));
2960 }
2961
2962 #[test]
2963 fn type_star_requires_f2018() {
2964 let errs = errors_with_std(
2965 "\
2966 subroutine s(x)
2967 type(*) :: x
2968 end subroutine
2969 ",
2970 FortranStandard::F2008,
2971 );
2972 assert!(errs
2973 .iter()
2974 .any(|e| e.contains("CLASS(*)/TYPE(*) declaration") && e.contains("F2018")));
2975 }
2976
2977 #[test]
2978 fn deferred_length_character_requires_f2003() {
2979 let errs = errors_with_std(
2980 "\
2981 program p
2982 character(len=:), allocatable :: s
2983 end program
2984 ",
2985 FortranStandard::F95,
2986 );
2987 assert!(errs
2988 .iter()
2989 .any(|e| e.contains("deferred-length character") && e.contains("F2003")));
2990 }
2991
2992 #[test]
2993 fn allocatable_scalar_requires_f2003() {
2994 let errs = errors_with_std(
2995 "\
2996 program p
2997 integer, allocatable :: x
2998 end program
2999 ",
3000 FortranStandard::F95,
3001 );
3002 assert!(errs
3003 .iter()
3004 .any(|e| e.contains("allocatable scalar variables") && e.contains("F2003")));
3005 }
3006
3007 #[test]
3008 fn allocate_source_requires_f2003() {
3009 let errs = errors_with_std(
3010 "\
3011 program p
3012 integer, allocatable :: x
3013 integer :: y
3014 allocate(x, source=y)
3015 end program
3016 ",
3017 FortranStandard::F95,
3018 );
3019 assert!(errs
3020 .iter()
3021 .any(|e| e.contains("ALLOCATE with SOURCE=") && e.contains("F2003")));
3022 }
3023
3024 #[test]
3025 fn move_alloc_requires_f2003() {
3026 let errs = errors_with_std(
3027 "\
3028 program p
3029 integer, allocatable :: x, y
3030 call move_alloc(x, y)
3031 end program
3032 ",
3033 FortranStandard::F95,
3034 );
3035 assert!(errs
3036 .iter()
3037 .any(|e| e.contains("MOVE_ALLOC") && e.contains("F2003")));
3038 }
3039
3040 // ---- Elemental ----
3041
3042 #[test]
3043 fn elemental_io_errors() {
3044 let errs = errors_from(
3045 "\
3046 elemental subroutine foo(x)
3047 real, intent(in) :: x
3048 print *, x
3049 end subroutine
3050 ",
3051 );
3052 // Elemental implies pure, so I/O is forbidden.
3053 assert!(errs.iter().any(|e| e.contains("I/O") && e.contains("pure")));
3054 }
3055 }
3056