@@ -1042,6 +1042,16 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
10421042 value . Append ( ". " ) . Append ( overloads ) ;
10431043 }
10441044
1045+ // Point at the argument that failed against the nearest overload so
1046+ // the caller doesn't have to diff the signatures by eye. Appended
1047+ // after the overloads block: consumers that extract the hint from
1048+ // that marker onwards keep this line too.
1049+ var mismatch = DiagnoseClosestOverloadMismatch ( candidates , args , kw ) ;
1050+ if ( mismatch . Length > 0 )
1051+ {
1052+ value . Append ( '\n ' ) . Append ( mismatch ) ;
1053+ }
1054+
10451055 Exceptions . RaiseTypeError ( value . ToString ( ) ) ;
10461056 }
10471057
@@ -1216,6 +1226,260 @@ public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodInforma
12161226 }
12171227 }
12181228
1229+ /// <summary>
1230+ /// Builds a one-line diagnosis of the first argument that fails to match the
1231+ /// nearest candidate overload (the one with the most leading convertible
1232+ /// arguments), e.g. "Argument mismatch: argument 3 ('asynchronous') expected
1233+ /// bool, got str." Returns an empty string when there is nothing conclusive to
1234+ /// report (e.g. a pure arity mismatch). Only runs on the bind-failure path; it
1235+ /// never throws and never leaves a Python error pending.
1236+ /// </summary>
1237+ private static string DiagnoseClosestOverloadMismatch ( IEnumerable < MethodBase > candidates , BorrowedReference args , BorrowedReference kw )
1238+ {
1239+ try
1240+ {
1241+ if ( candidates == null )
1242+ {
1243+ return string . Empty ;
1244+ }
1245+
1246+ var pyArgCount = args == null ? 0 : ( int ) Runtime . PyTuple_Size ( args ) ;
1247+
1248+ // Snapshot the keyword arguments with strong references so they stay
1249+ // valid while candidates are probed.
1250+ List < KeyValuePair < string , PyObject > > kwargs = null ;
1251+ if ( kw != null && Runtime . PyDict_Size ( kw ) > 0 )
1252+ {
1253+ kwargs = new List < KeyValuePair < string , PyObject > > ( ) ;
1254+ using var keyList = Runtime . PyDict_Keys ( kw ) ;
1255+ using var valueList = Runtime . PyDict_Values ( kw ) ;
1256+ var kwCount = ( int ) Runtime . PyList_Size ( keyList . Borrow ( ) ) ;
1257+ for ( var i = 0 ; i < kwCount ; i ++ )
1258+ {
1259+ var name = Runtime . GetManagedString ( Runtime . PyList_GetItem ( keyList . Borrow ( ) , i ) ) ;
1260+ if ( name != null )
1261+ {
1262+ kwargs . Add ( new KeyValuePair < string , PyObject > (
1263+ name , new PyObject ( Runtime . PyList_GetItem ( valueList . Borrow ( ) , i ) ) ) ) ;
1264+ }
1265+ }
1266+ }
1267+
1268+ var bestScore = - 1 ;
1269+ var bestMismatchIndex = - 1 ;
1270+ ParameterInfo bestMismatchParameter = null ;
1271+ string bestKwargName = null ;
1272+ PyObject bestKwargValue = null ;
1273+
1274+ foreach ( var method in candidates )
1275+ {
1276+ if ( method == null || OperatorMethod . IsOperatorMethod ( method ) )
1277+ {
1278+ continue ;
1279+ }
1280+
1281+ var pi = method . GetParameters ( ) ;
1282+ var paramsArrayIndex = pi . Length > 0 && Attribute . IsDefined ( pi [ pi . Length - 1 ] , typeof ( ParamArrayAttribute ) )
1283+ ? pi . Length - 1
1284+ : - 1 ;
1285+
1286+ var score = 0 ;
1287+ var mismatchIndex = - 1 ;
1288+ var limit = Math . Min ( pyArgCount , pi . Length ) ;
1289+ for ( var i = 0 ; i < limit ; i ++ )
1290+ {
1291+ if ( i == paramsArrayIndex )
1292+ {
1293+ // Remaining arguments feed the params array; probing its
1294+ // element conversions here would be guesswork, count them
1295+ // as matched.
1296+ score = limit ;
1297+ break ;
1298+ }
1299+
1300+ var op = Runtime . PyTuple_GetItem ( args , i ) ;
1301+ if ( op == null )
1302+ {
1303+ Exceptions . Clear ( ) ;
1304+ break ;
1305+ }
1306+
1307+ if ( ! ArgumentMatchesParameter ( op , pi [ i ] ) )
1308+ {
1309+ mismatchIndex = i ;
1310+ break ;
1311+ }
1312+ score ++ ;
1313+ }
1314+
1315+ string kwargName = null ;
1316+ PyObject kwargValue = null ;
1317+ ParameterInfo kwargParameter = null ;
1318+ if ( mismatchIndex == - 1 && kwargs != null )
1319+ {
1320+ foreach ( var pair in kwargs )
1321+ {
1322+ var parameter = pi . FirstOrDefault ( p => p . Name == pair . Key || p . Name . ToSnakeCase ( ) == pair . Key ) ;
1323+ if ( parameter == null )
1324+ {
1325+ // Not a parameter of this overload; flagging unknown
1326+ // keyword names is out of scope here.
1327+ continue ;
1328+ }
1329+
1330+ if ( ArgumentMatchesParameter ( pair . Value . Reference , parameter ) )
1331+ {
1332+ score ++ ;
1333+ }
1334+ else
1335+ {
1336+ kwargName = pair . Key ;
1337+ kwargValue = pair . Value ;
1338+ kwargParameter = parameter ;
1339+ break ;
1340+ }
1341+ }
1342+ }
1343+
1344+ if ( mismatchIndex == - 1 && kwargName == null )
1345+ {
1346+ // Everything given matched: the failure was arity or keyword
1347+ // related, nothing conclusive to pinpoint for this candidate.
1348+ continue ;
1349+ }
1350+
1351+ if ( score > bestScore )
1352+ {
1353+ bestScore = score ;
1354+ bestMismatchIndex = mismatchIndex ;
1355+ bestKwargName = kwargName ;
1356+ bestKwargValue = kwargValue ;
1357+ bestMismatchParameter = mismatchIndex != - 1 ? pi [ mismatchIndex ] : kwargParameter ;
1358+ }
1359+ }
1360+
1361+ if ( bestMismatchParameter == null )
1362+ {
1363+ return string . Empty ;
1364+ }
1365+
1366+ var expected = MethodSignatureFormatter . FormatType ( bestMismatchParameter . ParameterType ) ;
1367+ var parameterName = bestMismatchParameter . Name . ToSnakeCase ( ) ;
1368+ if ( bestKwargName != null )
1369+ {
1370+ return $ "Argument mismatch: keyword argument '{ bestKwargName } ' expected { expected } , got { GetPythonTypeName ( bestKwargValue . Reference ) } .";
1371+ }
1372+
1373+ var mismatchedArg = Runtime . PyTuple_GetItem ( args , bestMismatchIndex ) ;
1374+ var got = mismatchedArg == null ? Util . BadStr : GetPythonTypeName ( mismatchedArg ) ;
1375+ return $ "Argument mismatch: argument { bestMismatchIndex + 1 } ('{ parameterName } ') expected { expected } , got { got } .";
1376+ }
1377+ catch
1378+ {
1379+ // Best-effort hint only; never mask the original failure.
1380+ return string . Empty ;
1381+ }
1382+ finally
1383+ {
1384+ // Probing conversions may have left a Python error set; the caller is
1385+ // about to raise the real TypeError.
1386+ Exceptions . Clear ( ) ;
1387+ }
1388+ }
1389+
1390+ /// <summary>
1391+ /// Mirror of the per-argument acceptance rules the binder applies when matching
1392+ /// an overload (type alias equality, matching type codes, lossless numeric
1393+ /// conversions, implicit operators), used to find the first mismatching
1394+ /// argument for the bind-failure diagnosis. Lenient where probing is unreliable
1395+ /// (by-ref, generic and untyped parameters) so it under-reports rather than
1396+ /// blames the wrong argument.
1397+ /// </summary>
1398+ private static bool ArgumentMatchesParameter ( BorrowedReference op , ParameterInfo parameter )
1399+ {
1400+ var parameterType = parameter . ParameterType ;
1401+ if ( parameterType . IsByRef || parameterType . ContainsGenericParameters || parameterType == typeof ( object ) )
1402+ {
1403+ return true ;
1404+ }
1405+
1406+ Type clrtype = null ;
1407+ using ( var pyoptype = Runtime . PyObject_Type ( op ) )
1408+ {
1409+ Exceptions . Clear ( ) ;
1410+ if ( ! pyoptype . IsNull ( ) )
1411+ {
1412+ clrtype = Converter . GetTypeByAlias ( pyoptype . Borrow ( ) ) ;
1413+ }
1414+ }
1415+
1416+ if ( clrtype == null )
1417+ {
1418+ // Not a primitive-aliased Python value (e.g. a wrapped CLR object):
1419+ // probe the conversion itself.
1420+ var converted = Converter . ToManaged ( op , parameterType , out _ , false ) ;
1421+ Exceptions . Clear ( ) ;
1422+ return converted ;
1423+ }
1424+
1425+ if ( parameterType == clrtype )
1426+ {
1427+ return true ;
1428+ }
1429+
1430+ var pytype = Converter . GetPythonTypeByAlias ( parameterType ) ;
1431+ using ( var pyoptype = Runtime . PyObject_Type ( op ) )
1432+ {
1433+ Exceptions . Clear ( ) ;
1434+ if ( ! pyoptype . IsNull ( ) && pytype == pyoptype . Borrow ( ) )
1435+ {
1436+ return true ;
1437+ }
1438+ }
1439+
1440+ var underlyingType = Nullable . GetUnderlyingType ( parameterType ) ?? parameterType ;
1441+ if ( Type . GetTypeCode ( underlyingType ) == Type . GetTypeCode ( clrtype ) )
1442+ {
1443+ return true ;
1444+ }
1445+
1446+ if ( underlyingType == typeof ( decimal ) || underlyingType == typeof ( double )
1447+ || ( Runtime . PyFloat_Check ( op ) && Type . GetTypeCode ( underlyingType ) . IsInteger ( ) && ! underlyingType . IsEnum ) )
1448+ {
1449+ var converted = Converter . ToManaged ( op , parameterType , out _ , false ) ;
1450+ Exceptions . Clear ( ) ;
1451+ if ( converted )
1452+ {
1453+ return true ;
1454+ }
1455+ }
1456+
1457+ var opImplicit = parameterType . GetMethod ( "op_Implicit" , new [ ] { clrtype } ) ;
1458+ return opImplicit != null && opImplicit . ReturnType == parameterType ;
1459+ }
1460+
1461+ /// <summary>
1462+ /// The Python type name of a value (e.g. "str", "float64"), for error messages.
1463+ /// </summary>
1464+ private static string GetPythonTypeName ( BorrowedReference op )
1465+ {
1466+ using var pyType = Runtime . PyObject_Type ( op ) ;
1467+ if ( ! pyType . IsNull ( ) )
1468+ {
1469+ using var name = Runtime . PyObject_GetAttrString ( pyType . Borrow ( ) , "__name__" ) ;
1470+ if ( ! name . IsNull ( ) )
1471+ {
1472+ var managed = Runtime . GetManagedString ( name . Borrow ( ) ) ;
1473+ if ( ! string . IsNullOrEmpty ( managed ) )
1474+ {
1475+ return managed ;
1476+ }
1477+ }
1478+ }
1479+ Exceptions . Clear ( ) ;
1480+ return Util . BadStr ;
1481+ }
1482+
12191483 protected static void AppendArgumentTypes ( StringBuilder to , BorrowedReference args )
12201484 {
12211485 long argCount = Runtime . PyTuple_Size ( args ) ;
0 commit comments