@@ -23,6 +23,15 @@ internal enum PythonRichComparison
2323 GreaterThanOrEqual ,
2424}
2525
26+ /// <summary>
27+ /// Resolves a user-defined instance into an iterator by running its `__iter__` on the
28+ /// owning VM; the static protocol layer cannot execute frames itself.
29+ /// </summary>
30+ internal delegate PythonIteratorValue UserIterationDispatcher (
31+ PythonManagedObjectValue instance ,
32+ TextSpan span
33+ ) ;
34+
2635/// <summary>
2736/// Central protocol dispatch for managed values exposed through a native-compatibility boundary.
2837/// </summary>
@@ -139,7 +148,7 @@ .. template.Interpolations.Select(interpolation => interpolation.Value),
139148 case PythonTemplateValue :
140149 throw Fault (
141150 "DPY4023" ,
142- $ "'Template' object has no attribute '{ name } '. ",
151+ $ "'Template' object has no attribute '{ name } '",
143152 span ,
144153 "AttributeError"
145154 ) ;
@@ -172,7 +181,7 @@ .. template.Interpolations.Select(interpolation => interpolation.Value),
172181 ) ,
173182 _ => throw Fault (
174183 "DPY4023" ,
175- $ "'{ generator . TypeName } ' object has no attribute '{ name } '. ",
184+ $ "'{ generator . TypeName } ' object has no attribute '{ name } '",
176185 span ,
177186 "AttributeError"
178187 ) ,
@@ -188,7 +197,7 @@ .. template.Interpolations.Select(interpolation => interpolation.Value),
188197 "format_spec" => new PythonTextValue ( interpolation . FormatSpecification ) ,
189198 _ => throw Fault (
190199 "DPY4023" ,
191- $ "'Interpolation' object has no attribute '{ name } '. ",
200+ $ "'Interpolation' object has no attribute '{ name } '",
192201 span ,
193202 "AttributeError"
194203 ) ,
@@ -201,7 +210,7 @@ .. template.Interpolations.Select(interpolation => interpolation.Value),
201210
202211 throw Fault (
203212 "DPY4023" ,
204- $ "'{ GetTypeName ( builtin ) } ' object has no attribute '{ name } '. ",
213+ $ "'{ GetTypeName ( builtin ) } ' object has no attribute '{ name } '",
205214 span ,
206215 "AttributeError"
207216 ) ;
@@ -470,13 +479,22 @@ private static PythonNoneValue CloseGenerator(PythonGeneratorValue generator, Te
470479 }
471480 }
472481
473- internal static PythonIteratorValue GetIterator ( PythonValue value , TextSpan span = default )
482+ internal static PythonIteratorValue GetIterator (
483+ PythonValue value ,
484+ TextSpan span = default ,
485+ UserIterationDispatcher ? userIteration = null
486+ )
474487 {
475488 if ( value is PythonIteratorValue iterator )
476489 {
477490 return iterator ;
478491 }
479492
493+ if ( value is PythonManagedObjectValue instance && userIteration is not null )
494+ {
495+ return userIteration ( instance , span ) ;
496+ }
497+
480498 if ( value is PythonGeneratorValue generatorValue )
481499 {
482500 if ( generatorValue . IsCoroutine )
@@ -525,7 +543,12 @@ or PythonSetValue
525543 )
526544 )
527545 {
528- throw Fault ( "DPY4015" , "This value is not iterable." , span , "TypeError" ) ;
546+ throw Fault (
547+ "DPY4015" ,
548+ $ "'{ GetTypeName ( value ) } ' object is not iterable",
549+ span ,
550+ "TypeError"
551+ ) ;
529552 }
530553
531554 return new PythonIteratorValue (
@@ -672,6 +695,17 @@ internal static bool TryGetNext(
672695 }
673696
674697 break ;
698+ case PythonUserIteratorSourceValue userSource :
699+ {
700+ var step = userSource . MoveNext ( ) ;
701+ if ( step . HasValue )
702+ {
703+ value = step . Value ;
704+ return true ;
705+ }
706+
707+ break ;
708+ }
675709 }
676710
677711 value = PythonNoneValue . Instance ;
@@ -857,7 +891,12 @@ internal static void SetTupleItem(
857891 tuple . Elements [ index ] = value ;
858892 }
859893
860- internal static bool Contains ( PythonValue container , PythonValue item , TextSpan span = default )
894+ internal static bool Contains (
895+ PythonValue container ,
896+ PythonValue item ,
897+ TextSpan span = default ,
898+ UserIterationDispatcher ? userIteration = null
899+ )
861900 {
862901 ArgumentNullException . ThrowIfNull ( container ) ;
863902 ArgumentNullException . ThrowIfNull ( item ) ;
@@ -881,7 +920,7 @@ internal static bool Contains(PythonValue container, PythonValue item, TextSpan
881920 return TryFindDictionaryItem ( dictionary , item , out _ ) ;
882921 }
883922
884- var iterator = GetIterator ( container , span ) ;
923+ var iterator = GetIterator ( container , span , userIteration ) ;
885924 while ( TryGetNext ( iterator , out var candidate , span ) )
886925 {
887926 if ( AreEqual ( candidate , item ) )
@@ -1058,9 +1097,14 @@ when TryFindDictionaryItem(dictionary, index, out var item):
10581097 }
10591098 }
10601099
1061- internal static void ExtendList ( PythonListValue list , PythonValue iterable , TextSpan span )
1100+ internal static void ExtendList (
1101+ PythonListValue list ,
1102+ PythonValue iterable ,
1103+ TextSpan span ,
1104+ UserIterationDispatcher ? userIteration = null
1105+ )
10621106 {
1063- var values = MaterializeValues ( iterable , span ) ;
1107+ var values = MaterializeValues ( iterable , span , userIteration ) ;
10641108 list . Elements . AddRange ( values ) ;
10651109 }
10661110
@@ -1094,7 +1138,12 @@ internal static void AddToSet(PythonSetValue set, PythonValue value, TextSpan sp
10941138 {
10951139 if ( ! IsHashable ( value ) )
10961140 {
1097- throw Fault ( "DPY4014" , $ "Unhashable type: '{ GetTypeName ( value ) } '.", span , "TypeError" ) ;
1141+ throw Fault (
1142+ "DPY4014" ,
1143+ $ "cannot use '{ GetTypeName ( value ) } ' as a set element (unhashable type: '{ GetTypeName ( value ) } ')",
1144+ span ,
1145+ "TypeError"
1146+ ) ;
10981147 }
10991148
11001149 foreach ( var element in set . Elements )
@@ -1119,10 +1168,14 @@ internal static PythonSetValue CreateSet(IReadOnlyList<PythonValue> values, Text
11191168 return set ;
11201169 }
11211170
1122- internal static List < PythonValue > MaterializeValues ( PythonValue iterable , TextSpan span )
1171+ internal static List < PythonValue > MaterializeValues (
1172+ PythonValue iterable ,
1173+ TextSpan span ,
1174+ UserIterationDispatcher ? userIteration = null
1175+ )
11231176 {
11241177 var values = new List < PythonValue > ( ) ;
1125- var iterator = GetIterator ( iterable , span ) ;
1178+ var iterator = GetIterator ( iterable , span , userIteration ) ;
11261179 while ( TryGetNext ( iterator , out var value , span ) )
11271180 {
11281181 values . Add ( value ) ;
@@ -1250,16 +1303,29 @@ internal static int GetPythonHash(PythonValue value, TextSpan span = default)
12501303 RuntimeHelpers . GetHashCode ( method . Function )
12511304 ) ,
12521305 PythonModuleValue module => RuntimeHelpers . GetHashCode ( module ) ,
1253- PythonListValue or PythonDictionaryValue => throw Fault (
1306+ PythonSetValue { IsFrozen : true } frozen => GetFrozenSetHash ( frozen , span ) ,
1307+ PythonListValue or PythonDictionaryValue or PythonSetValue => throw Fault (
12541308 "DPY4014" ,
1255- "This managed value is not hashable. ",
1309+ $ "unhashable type: ' { GetTypeName ( value ) } ' ",
12561310 span ,
12571311 "TypeError"
12581312 ) ,
12591313 _ => RuntimeHelpers . GetHashCode ( value ) ,
12601314 } ;
12611315 }
12621316
1317+ private static int GetFrozenSetHash ( PythonSetValue frozen , TextSpan span )
1318+ {
1319+ // Order-insensitive combination so equal frozensets hash equally.
1320+ var hash = 0 ;
1321+ foreach ( var element in frozen . Elements )
1322+ {
1323+ hash ^= GetPythonHash ( element , span ) ;
1324+ }
1325+
1326+ return HashCode . Combine ( hash , frozen . Elements . Count ) ;
1327+ }
1328+
12631329 internal static byte [ ] GetBytes ( PythonByteSequenceValue value )
12641330 {
12651331 ArgumentNullException . ThrowIfNull ( value ) ;
@@ -1286,7 +1352,7 @@ internal static string GetTypeName(PythonValue value) =>
12861352 PythonTupleValue => "tuple" ,
12871353 PythonDictionaryValue => "dict" ,
12881354 PythonSliceValue => "slice" ,
1289- PythonSetValue => "set" ,
1355+ PythonSetValue set => set . IsFrozen ? "frozenset" : "set" ,
12901356 PythonDictionaryViewValue view => view . Kind ,
12911357 PythonRangeValue => "range" ,
12921358 PythonEnumerateSourceValue => "enumerate" ,
@@ -1426,7 +1492,12 @@ TextSpan span
14261492 {
14271493 if ( ! IsHashable ( key ) )
14281494 {
1429- throw Fault ( "DPY4014" , "The dictionary key is not hashable." , span , "TypeError" ) ;
1495+ throw Fault (
1496+ "DPY4014" ,
1497+ $ "cannot use '{ GetTypeName ( key ) } ' as a dict key (unhashable type: '{ GetTypeName ( key ) } ')",
1498+ span ,
1499+ "TypeError"
1500+ ) ;
14301501 }
14311502
14321503 if ( TryFindDictionaryItem ( dictionary , key , out var item ) )
@@ -1461,7 +1532,8 @@ out PythonDictionaryItemValue item
14611532 internal static bool IsHashable ( PythonValue value ) =>
14621533 value switch
14631534 {
1464- PythonListValue or PythonDictionaryValue or PythonSetValue => false ,
1535+ PythonSetValue set => set . IsFrozen ,
1536+ PythonListValue or PythonDictionaryValue => false ,
14651537 PythonTupleValue tuple => tuple . Elements . All ( IsHashable ) ,
14661538 _ => true ,
14671539 } ;
@@ -1747,11 +1819,11 @@ private static Complex ToComplex(PythonValue value) =>
17471819 _ => throw new ArgumentOutOfRangeException ( nameof ( value ) ) ,
17481820 } ;
17491821
1750- private static PythonRuntimeException MissingAttribute (
1822+ internal static PythonRuntimeException MissingAttribute (
17511823 string typeName ,
17521824 string name ,
17531825 TextSpan span
1754- ) => Fault ( "DPY4022" , $ "'{ typeName } ' has no attribute '{ name } '. ", span , "AttributeError" ) ;
1826+ ) => Fault ( "DPY4022" , $ "'{ typeName } ' object has no attribute '{ name } '", span , "AttributeError" ) ;
17551827
17561828 internal static PythonRuntimeException Fault (
17571829 string code ,
0 commit comments