This one was what we call a 3-fer. Next in line on the PR125515 list.

See attached patch.

Regression tested on x86-64.

OK for mainline and then backport to 16?

Regards,

Jerry

---

fortran: ICE or wrong-code for ASSOCIATE selector that is a
 type-bound user-defined operator

Three related bugs prevented ASSOCIATE selectors that are type-bound
user-defined operator expressions from compiling correctly.

Bug 1 (class.cc): find_typebound_proc_uop returned NULL immediately when
the derived type has no f2k_derived namespace, bypassing the parent-type
inheritance walk.  This caused inherited UDOs to be silently not found.
Fix: set root = NULL and let the loop reach the parent type instead.

Bug 2 (resolve.cc): resolve_typebound_procedures called resolve_symbol on
the parent type only after an early return that fires when the derived type
has no direct type-bound bindings.  This left parent-type bindings
unresolved when searched via gfc_find_typebound_user_op.
Fix: move resolve_symbol(super_type) before the early return.

Bug 3 (match.cc): match_association_list did not handle ASSOCIATE selectors
of the form .uop. expr or the nested case .uop2. (.uop1. expr).  When the
selector's type was BT_UNKNOWN at parse time the name of the associate
variable was left untyped, producing a "Syntax error in expression" ICE in
the body of the ASSOCIATE construct.
Fix: add three helpers before match_association_list:
  - resolve_assoc_operand: attempts gfc_resolve_expr on EXPR_FUNCTION
    operands and falls back to gfc_find_dt_in_generic for constructor calls
    whose argument types are not yet known.
  - infer_typebound_uop_type: reads the return type of a type-bound UDO
    directly from specific_st->n.tb->u.specific->n.sym without calling
    gfc_resolve_symbol, avoiding a resolve_symbol_called race condition.
  - extend_assoc_op: walks the expression tree bottom-up, propagating
    types through INTRINSIC_PARENTHESES wrappers before calling the two
    helpers above on each INTRINSIC_USER node.
When the selector is an INTRINSIC_USER EXPR_OP with BT_UNKNOWN type,
call extend_assoc_op on the operands, then gfc_extend_expr (errors
suppressed).  Accept the result when gfc_extend_expr returns MATCH_YES or
when it returns MATCH_ERROR but has already converted the node to
EXPR_COMPCALL with a known type (the full resolution pass finishes it).

PR fortran/125528

Assisted by: Claude Sonnet 4.6

gcc/fortran/ChangeLog:

        PR fortran/125528
        * class.cc (find_typebound_proc_uop): Set root = NULL instead of
        returning NULL when derived type lacks f2k_derived, so parent-type
        type-bound procedures and operators are still found via inheritance.
        * match.cc (resolve_assoc_operand): New helper.
        (infer_typebound_uop_type): New helper.
        (extend_assoc_op): New helper.
        (match_association_list): Handle ASSOCIATE selectors that are
        type-bound user-defined operator expressions, including nested cases.
        * resolve.cc (resolve_typebound_procedures): Move resolve_symbol
        call for the parent type before the early return so inherited
        type-bound bindings are resolved even when the child type has none
        of its own.

gcc/testsuite/ChangeLog:

        PR fortran/125528
        * gfortran.dg/associate_80.f90: New test.
---
From 9bab576f32c66218f9b8e7f3c95d9e12988579ea Mon Sep 17 00:00:00 2001
From: Jerry DeLisle <[email protected]>
Date: Wed, 27 May 2026 15:10:07 -0700
Subject: [PATCH] fortran: ICE or wrong-code for ASSOCIATE selector that is a
 type-bound user-defined operator

Three related bugs prevented ASSOCIATE selectors that are type-bound
user-defined operator expressions from compiling correctly.

Bug 1 (class.cc): find_typebound_proc_uop returned NULL immediately when
the derived type has no f2k_derived namespace, bypassing the parent-type
inheritance walk.  This caused inherited UDOs to be silently not found.
Fix: set root = NULL and let the loop reach the parent type instead.

Bug 2 (resolve.cc): resolve_typebound_procedures called resolve_symbol on
the parent type only after an early return that fires when the derived type
has no direct type-bound bindings.  This left parent-type bindings
unresolved when searched via gfc_find_typebound_user_op.
Fix: move resolve_symbol(super_type) before the early return.

Bug 3 (match.cc): match_association_list did not handle ASSOCIATE selectors
of the form .uop. expr or the nested case .uop2. (.uop1. expr).  When the
selector's type was BT_UNKNOWN at parse time the name of the associate
variable was left untyped, producing a "Syntax error in expression" ICE in
the body of the ASSOCIATE construct.
Fix: add three helpers before match_association_list:
  - resolve_assoc_operand: attempts gfc_resolve_expr on EXPR_FUNCTION
    operands and falls back to gfc_find_dt_in_generic for constructor calls
    whose argument types are not yet known.
  - infer_typebound_uop_type: reads the return type of a type-bound UDO
    directly from specific_st->n.tb->u.specific->n.sym without calling
    gfc_resolve_symbol, avoiding a resolve_symbol_called race condition.
  - extend_assoc_op: walks the expression tree bottom-up, propagating
    types through INTRINSIC_PARENTHESES wrappers before calling the two
    helpers above on each INTRINSIC_USER node.
When the selector is an INTRINSIC_USER EXPR_OP with BT_UNKNOWN type,
call extend_assoc_op on the operands, then gfc_extend_expr (errors
suppressed).  Accept the result when gfc_extend_expr returns MATCH_YES or
when it returns MATCH_ERROR but has already converted the node to
EXPR_COMPCALL with a known type (the full resolution pass finishes it).

PR fortran/125528

Assisted by: Claude Sonnet 4.6

gcc/fortran/ChangeLog:

	PR fortran/125528
	* class.cc (find_typebound_proc_uop): Set root = NULL instead of
	returning NULL when derived type lacks f2k_derived, so parent-type
	type-bound procedures and operators are still found via inheritance.
	* match.cc (resolve_assoc_operand): New helper.
	(infer_typebound_uop_type): New helper.
	(extend_assoc_op): New helper.
	(match_association_list): Handle ASSOCIATE selectors that are
	type-bound user-defined operator expressions, including nested cases.
	* resolve.cc (resolve_typebound_procedures): Move resolve_symbol
	call for the parent type before the early return so inherited
	type-bound bindings are resolved even when the child type has none
	of its own.

gcc/testsuite/ChangeLog:

	PR fortran/125528
	* gfortran.dg/associate_80.f90: New test.
---
 gcc/fortran/class.cc                       |   4 +-
 gcc/fortran/match.cc                       | 186 ++++++++++++++++++++-
 gcc/fortran/resolve.cc                     |  10 +-
 gcc/testsuite/gfortran.dg/associate_80.f90 |  83 +++++++++
 4 files changed, 273 insertions(+), 10 deletions(-)
 create mode 100644 gcc/testsuite/gfortran.dg/associate_80.f90

diff --git a/gcc/fortran/class.cc b/gcc/fortran/class.cc
index a4c3c37104a..d811ee2929c 100644
--- a/gcc/fortran/class.cc
+++ b/gcc/fortran/class.cc
@@ -3136,7 +3136,9 @@ find_typebound_proc_uop (gfc_symbol* derived, bool* t,
     root = (uop ? derived->f2k_derived->tb_uop_root
 		: derived->f2k_derived->tb_sym_root);
   else
-    return NULL;
+    /* No f2k_derived namespace; allow the extension check below to proceed
+       so inherited type-bound procedures/operators are still found.  */
+    root = NULL;
 
   /* Try to find it in the current type's namespace.  */
   res = gfc_find_symtree (root, name);
diff --git a/gcc/fortran/match.cc b/gcc/fortran/match.cc
index d892a4588b2..9172f46ed7c 100644
--- a/gcc/fortran/match.cc
+++ b/gcc/fortran/match.cc
@@ -1980,6 +1980,155 @@ check_coarray_assoc (const char *name, gfc_association_list *assoc)
   return true;
 }
 
+/* Try to resolve an EXPR_FUNCTION operand so its return type is known.
+   Called during ASSOCIATE selector parsing, before type-bound operator
+   extension, when the operand is an unresolved generic constructor call
+   such as `scalar_1D_t(initializer, order=2, ...)`.  Errors are suppressed
+   since we are still in the parsing phase.  */
+
+static void
+resolve_assoc_operand (gfc_expr *e)
+{
+  if (!e || e->ts.type != BT_UNKNOWN || e->expr_type != EXPR_FUNCTION)
+    return;
+
+  /* First, try full expression resolution (works when argument types are
+     already known at parse time).  */
+  gfc_push_suppress_errors ();
+  gfc_resolve_expr (e);
+  gfc_pop_suppress_errors ();
+
+  if (e->ts.type != BT_UNKNOWN)
+    return;
+
+  /* Fallback for generic constructor interfaces such as
+       scalar_1D_t(initializer, order=2, cells=16, x_min=0D0, x_max=5D0)
+     where full argument resolution is not possible at parse time.
+     If the function name resolves to a generic interface that wraps a
+     derived type (a constructor interface), infer the return type as
+     that derived type.  */
+  if (!e->symtree || !e->symtree->n.sym)
+    return;
+
+  gfc_symbol *dt_sym = gfc_find_dt_in_generic (e->symtree->n.sym);
+  if (dt_sym && gfc_fl_struct (dt_sym->attr.flavor))
+    {
+      e->ts.type = BT_DERIVED;
+      e->ts.u.derived = dt_sym;
+    }
+}
+
+/* Infer the return type of a type-bound user-defined operator without
+   converting the expression node or triggering gfc_resolve_symbol on the
+   return type.  This is used during ASSOCIATE selector parsing to propagate
+   type information bottom-up through nested UDO expressions such as
+   (.div. (.grad. x)), so that the outer gfc_extend_expr can locate the
+   type-bound .div. once the type of (.grad. x) is known.
+
+   Calling gfc_extend_expr for this purpose would partially resolve the
+   return type's derived-type symbol (setting resolve_symbol_called before
+   resolve_typebound_procedures has run), which prevents the subsequent
+   outer gfc_extend_expr from properly resolving the type-bound operator
+   on the return type.  We avoid that by reading the return type directly
+   from the procedure's result variable without triggering resolution.  */
+
+static void
+infer_typebound_uop_type (gfc_expr *e)
+{
+  if (!e || e->expr_type != EXPR_OP || e->value.op.op != INTRINSIC_USER
+      || e->ts.type != BT_UNKNOWN)
+    return;
+
+  /* Find the operand and strip parentheses.  */
+  gfc_expr *operand = e->value.op.op1;
+  while (operand && operand->expr_type == EXPR_OP
+	 && operand->value.op.op == INTRINSIC_PARENTHESES)
+    operand = operand->value.op.op1;
+
+  if (!operand || operand->ts.type != BT_DERIVED || !operand->ts.u.derived)
+    return;
+
+  /* Look up the UDO binding in the derived type's namespace (and its
+     parent types, via the recursion in find_typebound_proc_uop).  This
+     does not call resolve_symbol, so it leaves resolve_symbol_called
+     untouched for all types involved.  */
+  bool ok = true;
+  gfc_symtree *tb_uop
+    = gfc_find_typebound_user_op (operand->ts.u.derived, &ok,
+				  e->value.op.uop->name, false, NULL);
+  if (!tb_uop || !tb_uop->n.tb)
+    return;
+
+  gfc_typebound_proc *tb = tb_uop->n.tb;
+  if (!tb->is_generic || !tb->u.generic)
+    return;
+
+  /* Take the first specific binding.  specific_st is set from module reading;
+     its n.tb is the gfc_typebound_proc for that specific binding (same as
+     what resolve_typebound_procedures later stores in g->specific).  Follow
+     the chain specific_st->n.tb->u.specific->n.sym to reach the actual
+     implementing function symbol, whose ts holds the return type.
+     This mirrors what build_compcall_for_operator does via
+     g->specific->u.specific->n.sym->ts after resolution.  */
+  gfc_tbp_generic *g = tb->u.generic;
+  if (!g->specific_st || !g->specific_st->n.tb)
+    return;
+
+  gfc_typebound_proc *specific_tb = g->specific_st->n.tb;
+  if (specific_tb->is_generic || !specific_tb->u.specific
+      || !specific_tb->u.specific->n.sym)
+    return;
+
+  gfc_symbol *proc = specific_tb->u.specific->n.sym;
+  if (proc->ts.type != BT_UNKNOWN)
+    e->ts = proc->ts;
+}
+
+/* Recursively propagate type information bottom-up through a nested UDO
+   expression tree so that when gfc_extend_expr is called on the outermost
+   operator during ASSOCIATE selector parsing, the inner operands already have
+   their types set and the type-bound lookup can succeed.  Uses
+   infer_typebound_uop_type rather than gfc_extend_expr to avoid triggering
+   resolve_symbol on the return types, which would prevent the outer
+   gfc_extend_expr from working correctly.  */
+
+static void
+extend_assoc_op (gfc_expr *e)
+{
+  if (!e || e->expr_type != EXPR_OP)
+    return;
+
+  /* Bottom-up: process children first.  */
+  extend_assoc_op (e->value.op.op1);
+  extend_assoc_op (e->value.op.op2);
+
+  /* Propagate the child's type upward through parentheses nodes.
+     gfc_extend_expr's matching_typebound_op checks ts.type BEFORE stripping
+     INTRINSIC_PARENTHESES wrappers, so an untyped parentheses node prevents
+     the outer operator from being found.  */
+  if (e->value.op.op == INTRINSIC_PARENTHESES
+      && e->ts.type == BT_UNKNOWN
+      && e->value.op.op1
+      && e->value.op.op1->ts.type != BT_UNKNOWN)
+    {
+      e->ts = e->value.op.op1->ts;
+      return;
+    }
+
+  /* Only handle unresolved user-defined operators.  */
+  if (e->value.op.op != INTRINSIC_USER || e->ts.type != BT_UNKNOWN)
+    return;
+
+  /* Try to infer the type of each operand if it is an unresolved constructor
+     call (EXPR_FUNCTION whose return type is still BT_UNKNOWN).  */
+  resolve_assoc_operand (e->value.op.op1);
+  resolve_assoc_operand (e->value.op.op2);
+
+  /* Infer this operator's return type from the type-bound procedure's result
+     variable, without calling gfc_resolve_symbol on the return type.  */
+  infer_typebound_uop_type (e);
+}
+
 match
 match_association_list (bool for_change_team = false)
 {
@@ -2142,12 +2291,37 @@ match_association_list (bool for_change_team = false)
 	    }
 	}
       else if (newAssoc->target->ts.type == BT_UNKNOWN
-	       && newAssoc->target->expr_type == EXPR_OP)
-	{
-	  /* This will work for sure if the operator is type bound to a use
-	     associated derived type.  */
-	  gfc_expr *tmp =gfc_copy_expr (newAssoc->target);
-	  if (gfc_extend_expr (tmp) == MATCH_YES)
+	       && newAssoc->target->expr_type == EXPR_OP
+	       && newAssoc->target->value.op.op == INTRINSIC_USER)
+	{
+	  /* If the selector is an unresolved type-bound user-defined operator
+	     expression, try to extend it now so the associate name gets a usable
+	     type.  For nested operators such as
+	       (.div. (.grad. x))
+	     first propagate types bottom-up through the inner operands
+	     (extend_assoc_op).  For a direct operator applied to a constructor
+	     call such as
+	       (.div. vector_t(init_fn, n=8))
+	     additionally resolve the direct operands as constructor calls
+	     (resolve_assoc_operand).  Then call gfc_extend_expr on the
+	     outermost operator.  Only handle INTRINSIC_USER here; arithmetic
+	     operators are left to the normal resolution pass.  */
+	  gfc_expr *tmp = gfc_copy_expr (newAssoc->target);
+	  extend_assoc_op (tmp->value.op.op1);
+	  extend_assoc_op (tmp->value.op.op2);
+	  resolve_assoc_operand (tmp->value.op.op1);
+	  resolve_assoc_operand (tmp->value.op.op2);
+	  /* Suppress errors from gfc_extend_expr: during parsing the full
+	     resolution has not run yet, so gfc_resolve_expr(COMPCALL) may
+	     fail even when the type-bound operator was found and the node
+	     was correctly converted to EXPR_COMPCALL.  Accept the conversion
+	     in that case and let the normal resolution pass finish it.  */
+	  gfc_push_suppress_errors ();
+	  match ext_m = gfc_extend_expr (tmp);
+	  gfc_pop_suppress_errors ();
+	  if (ext_m == MATCH_YES
+	      || (tmp->expr_type == EXPR_COMPCALL
+		  && tmp->ts.type != BT_UNKNOWN))
 	    gfc_replace_expr (newAssoc->target, tmp);
 	  else
 	    gfc_free_expr (tmp);
diff --git a/gcc/fortran/resolve.cc b/gcc/fortran/resolve.cc
index a31f395709d..df91671e171 100644
--- a/gcc/fortran/resolve.cc
+++ b/gcc/fortran/resolve.cc
@@ -17218,13 +17218,17 @@ resolve_typebound_procedures (gfc_symbol* derived)
   int op;
   gfc_symbol* super_type;
 
-  if (!derived->f2k_derived || !derived->f2k_derived->tb_sym_root)
-    return true;
-
+  /* Resolve the super-type first so that inherited bindings (including
+     user operators) are fully resolved before we look them up via
+     gfc_find_typebound_user_op.  This must happen even when 'derived'
+     has no direct type-bound bindings of its own.  */
   super_type = gfc_get_derived_super_type (derived);
   if (super_type)
     resolve_symbol (super_type);
 
+  if (!derived->f2k_derived || !derived->f2k_derived->tb_sym_root)
+    return true;
+
   resolve_bindings_derived = derived;
   resolve_bindings_result = true;
 
diff --git a/gcc/testsuite/gfortran.dg/associate_80.f90 b/gcc/testsuite/gfortran.dg/associate_80.f90
new file mode 100644
index 00000000000..14c405c8f22
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/associate_80.f90
@@ -0,0 +1,83 @@
+! { dg-do run }
+!
+! Test ASSOCIATE selectors that are type-bound user-defined operators (UDOs),
+! including inherited operators and nested expressions such as
+! .div. (.grad. x).  PR fortran/125515
+!
+module associate_80_m
+  implicit none
+
+  type :: scalar_t
+    real :: val
+  contains
+    generic :: operator(.grad.) => do_grad
+    procedure, private :: do_grad
+    generic :: operator(.sq.)   => do_sq
+    procedure, private :: do_sq
+    generic :: get => get_val
+    procedure :: get_val
+  end type
+
+  type, extends(scalar_t) :: vector_t
+  contains
+    generic :: operator(.div.) => do_div
+    procedure, private :: do_div
+  end type
+
+contains
+
+  ! .grad. on scalar_t -> vector_t
+  pure function do_grad (self) result (r)
+    class(scalar_t), intent(in) :: self
+    type(vector_t) :: r
+    r%val = self%val * 2.0
+  end function
+
+  ! .sq. on scalar_t -> scalar_t
+  pure function do_sq (self) result (r)
+    class(scalar_t), intent(in) :: self
+    type(scalar_t) :: r
+    r%val = self%val * self%val
+  end function
+
+  ! .div. on vector_t -> scalar_t
+  pure function do_div (self) result (r)
+    class(vector_t), intent(in) :: self
+    type(scalar_t) :: r
+    r%val = self%val / 2.0
+  end function
+
+  pure function get_val (self) result (r)
+    class(scalar_t), intent(in) :: self
+    real :: r
+    r = self%val
+  end function
+
+end module associate_80_m
+
+program associate_80
+  use associate_80_m
+  implicit none
+
+  type(scalar_t) :: s
+  s%val = 3.0
+
+  ! Case 1: direct type-bound UDO as ASSOCIATE selector
+  associate (g => .grad. s)
+    if (abs (g%val - 6.0) > 1.0e-6) stop 1
+    if (abs (g%get () - 6.0) > 1.0e-6) stop 2
+  end associate
+
+  ! Case 2: inherited UDO (.sq. defined on scalar_t, used via scalar_t)
+  associate (q => .sq. s)
+    if (abs (q%val - 9.0) > 1.0e-6) stop 3
+  end associate
+
+  ! Case 3: nested UDOs — .div. (.grad. s)
+  ! .grad. s gives vector_t with val=6, .div. gives scalar_t with val=3
+  associate (r => .div. (.grad. s))
+    if (abs (r%val - 3.0) > 1.0e-6) stop 4
+    if (abs (r%get () - 3.0) > 1.0e-6) stop 5
+  end associate
+
+end program associate_80
-- 
2.54.0

Reply via email to