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