Skip to content

Commit e3421a2

Browse files
committed
Support user iteration protocol, frozenset, and attribute builtins
1 parent 2001538 commit e3421a2

6 files changed

Lines changed: 642 additions & 72 deletions

File tree

‎src/DotPython.Runtime.Managed/Execution/ManagedObjectProtocols.cs‎

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

‎src/DotPython.Runtime.Managed/Execution/PythonBuiltinMethods.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,18 @@ out var item
553553
["copy"] = Set("copy", 0, 0, (set, _) => new PythonSetValue([.. set.Elements])),
554554
};
555555

556+
private static readonly Dictionary<string, PythonProtocolFunctionValue> FrozenSetMethods = new(
557+
StringComparer.Ordinal
558+
)
559+
{
560+
["copy"] = Set(
561+
"copy",
562+
0,
563+
0,
564+
(set, _) => new PythonSetValue([.. set.Elements]) { IsFrozen = true }
565+
),
566+
};
567+
556568
internal static bool TryGet(
557569
PythonValue target,
558570
string name,
@@ -565,6 +577,7 @@ out PythonProtocolFunctionValue method
565577
PythonListValue => ListMethods,
566578
PythonDictionaryValue => DictionaryMethods,
567579
PythonTupleValue => TupleMethods,
580+
PythonSetValue { IsFrozen: true } => FrozenSetMethods,
568581
PythonSetValue => SetMethods,
569582
_ => null,
570583
};

‎src/DotPython.Runtime.Managed/Execution/PythonBuiltinTypes.cs‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ internal static class PythonBuiltinTypes
1717
internal static readonly PythonBuiltinTypeValue Int = new("int", ConstructInt);
1818
internal static readonly PythonBuiltinTypeValue List = new("list", ConstructList);
1919
internal static readonly PythonBuiltinTypeValue Set = new("set", ConstructSet);
20+
internal static readonly PythonBuiltinTypeValue Frozenset = new(
21+
"frozenset",
22+
ConstructFrozenset
23+
);
2024
internal static readonly PythonBuiltinTypeValue Str = new("str", ConstructStr);
2125
internal static readonly PythonBuiltinTypeValue Tuple = new("tuple", ConstructTuple);
2226

2327
internal static IEnumerable<PythonBuiltinTypeValue> All =>
24-
[Bool, Dict, Float, Int, List, Set, Str, Tuple];
28+
[Bool, Dict, Float, Frozenset, Int, List, Set, Str, Tuple];
2529

2630
internal static PythonBuiltinTypeValue CreateOpaque(string name) =>
2731
new(
@@ -45,7 +49,8 @@ internal static bool IsInstance(PythonValue value, PythonBuiltinTypeValue type)
4549
"list" => value is PythonListValue,
4650
"tuple" => value is PythonTupleValue,
4751
"dict" => value is PythonDictionaryValue,
48-
"set" => value is PythonSetValue,
52+
"set" => value is PythonSetValue { IsFrozen: false },
53+
"frozenset" => value is PythonSetValue { IsFrozen: true },
4954
_ => false,
5055
};
5156

@@ -200,6 +205,22 @@ private static PythonSetValue ConstructSet(IReadOnlyList<PythonValue> arguments,
200205
);
201206
}
202207

208+
private static PythonSetValue ConstructFrozenset(
209+
IReadOnlyList<PythonValue> arguments,
210+
TextSpan span
211+
)
212+
{
213+
RequireArguments("frozenset", arguments, 0, 1, span);
214+
var deduplicated =
215+
arguments.Count == 0
216+
? new PythonSetValue([])
217+
: ManagedObjectProtocols.CreateSet(
218+
ManagedObjectProtocols.MaterializeValues(arguments[0], span),
219+
span
220+
);
221+
return new PythonSetValue(deduplicated.Elements) { IsFrozen = true };
222+
}
223+
203224
private static PythonTextValue ConstructStr(IReadOnlyList<PythonValue> arguments, TextSpan span)
204225
{
205226
RequireArguments("str", arguments, 0, 1, span);

‎src/DotPython.Runtime.Managed/Execution/PythonValue.cs‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -597,23 +597,28 @@ PythonIteratorValue Inner
597597

598598
internal sealed record PythonSetValue(List<PythonValue> Elements) : PythonValue
599599
{
600+
/// <summary>Whether this value is a `frozenset` (immutable, hashable) rather than a `set`.</summary>
601+
internal bool IsFrozen { get; init; }
602+
600603
internal override string ToDisplayString()
601604
{
602605
if (Elements.Count == 0)
603606
{
604-
return "set()";
607+
return IsFrozen ? "frozenset()" : "set()";
605608
}
606609

607610
if (!PythonRepresentationGuard.TryEnter(this))
608611
{
609-
return "{...}";
612+
return IsFrozen ? "frozenset({...})" : "{...}";
610613
}
611614

612615
try
613616
{
614-
return "{"
617+
var elements =
618+
"{"
615619
+ string.Join(", ", Elements.Select(element => element.ToRepresentationString()))
616620
+ "}";
621+
return IsFrozen ? $"frozenset({elements})" : elements;
617622
}
618623
finally
619624
{
@@ -622,6 +627,17 @@ internal override string ToDisplayString()
622627
}
623628
}
624629

630+
/// <summary>
631+
/// A lazy iteration source over a user-defined iterator: `MoveNext` captures the VM
632+
/// and the bound `__next__`, converting a raised StopIteration into exhaustion.
633+
/// </summary>
634+
internal sealed record PythonUserIteratorSourceValue(
635+
Func<(bool HasValue, PythonValue Value)> MoveNext
636+
) : PythonValue
637+
{
638+
internal override string ToDisplayString() => "<iterator>";
639+
}
640+
625641
internal sealed record PythonSliceValue(PythonValue Start, PythonValue Stop, PythonValue Step)
626642
: PythonValue
627643
{

0 commit comments

Comments
 (0)