Skip to content

Commit b8eb7e0

Browse files
committed
Give builtin, constructor, and exception types canonical identity with a three-argument type() and writable class metadata
1 parent 1e83263 commit b8eb7e0

8 files changed

Lines changed: 656 additions & 95 deletions

File tree

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

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,25 @@ private static PythonValue GetAttributeCore(
126126
ArgumentNullException.ThrowIfNull(target);
127127
ArgumentException.ThrowIfNullOrWhiteSpace(name);
128128

129+
if (name == "__class__" && target is not PythonManagedObjectValue)
130+
{
131+
return UserObjectProtocols.Dispatcher is null
132+
? PythonBuiltinTypes.GetRuntimeType(target)
133+
: PythonBuiltinTypes.Type.Construct([target], span);
134+
}
135+
if (target is PythonBuiltinTypeValue or PythonManagedTypeValue or PythonExceptionTypeValue)
136+
{
137+
if (name == "__mro__")
138+
return PythonBuiltinTypes.GetMro(target);
139+
if (name == "__bases__")
140+
return PythonBuiltinTypes.GetBases(target);
141+
if (name == "__base__")
142+
{
143+
var bases = PythonBuiltinTypes.GetBases(target).Elements;
144+
return bases.Length == 0 ? PythonNoneValue.Instance : bases[0];
145+
}
146+
}
147+
129148
switch (target)
130149
{
131150
case PythonModuleValue module when name == "__dict__":
@@ -207,18 +226,16 @@ proxy.Instance is PythonManagedObjectValue objectInstance
207226
return new PythonMappingProxyValue(type.Attributes.Dictionary);
208227
case PythonMappingProxyValue proxy:
209228
return PythonMappingProxies.GetAttribute(proxy, name, span);
229+
case PythonManagedTypeValue type when name == "__name__":
230+
return new PythonTextValue(type.Name);
231+
case PythonManagedTypeValue type when name == "__qualname__":
232+
return new PythonTextValue(type.QualName ?? type.Name);
210233
case PythonManagedTypeValue type when TryGetTypeAttribute(type, name, out var value):
211234
return BindDescriptor(value, null, type, span, name);
212-
case PythonManagedTypeValue type when name == "__name__" || name == "__qualname__":
213-
return new PythonTextValue(type.Name);
214235
case PythonManagedTypeValue type when name == "__module__":
215236
return type.Module is null
216237
? new PythonTextValue("builtins")
217238
: new PythonTextValue(type.Module);
218-
case PythonManagedTypeValue type when name == "__mro__":
219-
return new PythonTupleValue([.. type.Mro.Cast<PythonValue>()]);
220-
case PythonManagedTypeValue type when name == "__bases__":
221-
return new PythonTupleValue([.. type.Bases.Cast<PythonValue>()]);
222239
case PythonManagedTypeValue type:
223240
throw MissingAttribute(type.Name, name, span);
224241
case PythonExternalObjectValue external:
@@ -796,8 +813,40 @@ when TryGetTypeAttribute(exceptionClass, name, out var descriptor)
796813

797814
SetInstanceAttribute(instance, name, value, span);
798815
return;
816+
case PythonManagedTypeValue type when name is "__name__" or "__qualname__":
817+
if (value is not PythonTextValue text)
818+
{
819+
throw Fault(
820+
"DPY4023",
821+
$"can only assign string to {type.Name}.{name}, not '{GetTypeName(value)}'",
822+
span,
823+
"TypeError"
824+
);
825+
}
826+
if (name == "__name__")
827+
{
828+
if (text.Value.Contains('\0', StringComparison.Ordinal))
829+
{
830+
throw Fault(
831+
"DPY4023",
832+
"type name must not contain null characters",
833+
span,
834+
"ValueError"
835+
);
836+
}
837+
type.Name = text.Value;
838+
}
839+
else
840+
{
841+
type.QualName = text.Value;
842+
}
843+
return;
799844
case PythonManagedTypeValue type:
800845
type.Attributes[name] = value;
846+
if (name == "__module__")
847+
{
848+
type.Module = value is PythonTextValue moduleText ? moduleText.Value : null;
849+
}
801850
return;
802851
default:
803852
throw Fault(
@@ -1065,6 +1114,14 @@ when TryGetTypeAttribute(exceptionClass, name, out var descriptor)
10651114

10661115
DeleteInstanceAttribute(instance, name, span);
10671116
return;
1117+
case PythonManagedTypeValue type
1118+
when name is "__name__" or "__qualname__" or "__module__":
1119+
throw Fault(
1120+
"DPY4023",
1121+
$"cannot delete '{name}' attribute of immutable type '{type.Name}'",
1122+
span,
1123+
"TypeError"
1124+
);
10681125
case PythonManagedTypeValue type when type.Attributes.Remove(name):
10691126
return;
10701127
case PythonManagedTypeValue type:
@@ -2498,7 +2555,8 @@ internal static string GetTypeName(PythonValue value) =>
24982555
PythonInterpolationValue => "Interpolation",
24992556
PythonIteratorValue => "iterator",
25002557
PythonModuleValue => "module",
2501-
PythonManagedTypeValue => "type",
2558+
PythonManagedTypeValue or PythonBuiltinTypeValue => "type",
2559+
PythonSuperProxyValue => "super",
25022560
PythonPropertyValue => "property",
25032561
PythonStaticMethodValue => "staticmethod",
25042562
PythonClassMethodValue => "classmethod",

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

Lines changed: 157 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// comparison key derived from user identity.
33
#pragma warning disable CA1308
44

5+
using System.Collections.Concurrent;
56
using System.Globalization;
67
using System.Numerics;
78
using System.Text;
@@ -12,6 +13,147 @@ namespace DotPython.Runtime.Managed.Execution;
1213
/// <summary>Built-in type objects usable as constructors and isinstance class info.</summary>
1314
internal static class PythonBuiltinTypes
1415
{
16+
private static readonly ConcurrentDictionary<string, PythonBuiltinTypeValue> OpaqueTypes = new(
17+
StringComparer.Ordinal
18+
);
19+
private static readonly ConcurrentDictionary<string, PythonBuiltinTypeValue> ConstructorTypes =
20+
new(StringComparer.Ordinal);
21+
private static readonly ConcurrentDictionary<string, PythonExceptionTypeValue> ExceptionTypes =
22+
new(StringComparer.Ordinal);
23+
24+
internal static readonly PythonBuiltinTypeValue Type = new(
25+
"type",
26+
(arguments, span) => UserObjectProtocols.Dispatcher!.ConstructType(arguments, span)
27+
);
28+
29+
internal static PythonBuiltinTypeValue GetConstructorType(
30+
PythonBuiltinFunctionValue function
31+
) =>
32+
ConstructorTypes.GetOrAdd(
33+
function.Name,
34+
static (name, hasKeywords) =>
35+
new PythonBuiltinTypeValue(
36+
name,
37+
(arguments, span) =>
38+
UserObjectProtocols
39+
.Dispatcher!.GetBuiltinConstructor(name)
40+
.Invoke(arguments, span),
41+
hasKeywords
42+
? (arguments, names, values, span) =>
43+
UserObjectProtocols
44+
.Dispatcher!.GetBuiltinConstructor(name)
45+
.InvokeWithKeywords!(arguments, names, values, span)
46+
: null
47+
),
48+
function.InvokeWithKeywords is not null
49+
);
50+
51+
internal static PythonExceptionTypeValue GetExceptionType(string name) =>
52+
ExceptionTypes.GetOrAdd(name, static key => new PythonExceptionTypeValue(key));
53+
54+
internal static PythonTupleValue GetBases(PythonValue type) =>
55+
type switch
56+
{
57+
PythonBuiltinTypeValue builtin
58+
when ReferenceEquals(builtin, PythonBuiltinFunctions.Object) => new([]),
59+
PythonBuiltinTypeValue builtin when ReferenceEquals(builtin, Bool) => new([Int]),
60+
PythonBuiltinTypeValue => new([PythonBuiltinFunctions.Object]),
61+
PythonManagedTypeValue managed
62+
when ReferenceEquals(managed, PythonBuiltinFunctions.ObjectType) => new([]),
63+
PythonManagedTypeValue { DeclaredBases: { Count: > 0 } declared } => new([.. declared]),
64+
PythonManagedTypeValue { Bases.Count: > 0 } managed => new([.. managed.Bases]),
65+
PythonManagedTypeValue { ExceptionBaseName: { } baseName } => new([
66+
GetExceptionType(baseName),
67+
]),
68+
PythonManagedTypeValue => new([PythonBuiltinFunctions.Object]),
69+
PythonExceptionTypeValue exception
70+
when PythonVirtualMachine.GetBuiltinExceptionBase(exception.Name) is { } baseName =>
71+
new([GetExceptionType(baseName)]),
72+
PythonExceptionTypeValue => new([PythonBuiltinFunctions.Object]),
73+
_ => throw new ArgumentException("Expected a Python type.", nameof(type)),
74+
};
75+
76+
internal static PythonTupleValue GetMro(PythonValue type)
77+
{
78+
if (
79+
type is PythonManagedTypeValue managed
80+
&& !ReferenceEquals(managed, PythonBuiltinFunctions.ObjectType)
81+
)
82+
{
83+
var entries = managed.Mro.Cast<PythonValue>().ToList();
84+
var oldest = managed.Mro[^1];
85+
if (oldest.ExceptionBaseName is { } exceptionBase)
86+
{
87+
entries.AddRange(GetMro(GetExceptionType(exceptionBase)).Elements);
88+
}
89+
else
90+
{
91+
entries.Add(PythonBuiltinFunctions.Object);
92+
}
93+
return new([.. entries]);
94+
}
95+
if (ReferenceEquals(type, PythonBuiltinFunctions.ObjectType))
96+
{
97+
type = PythonBuiltinFunctions.Object;
98+
}
99+
var bases = GetBases(type).Elements;
100+
return bases.Length == 0 ? new([type]) : new([type, .. GetMro(bases[0]).Elements]);
101+
}
102+
103+
internal static PythonValue GetRuntimeType(PythonValue value) =>
104+
value switch
105+
{
106+
PythonBuiltinTypeValue or PythonManagedTypeValue or PythonExceptionTypeValue => Type,
107+
PythonTruthValue => Bool,
108+
PythonWholeNumberValue => Int,
109+
PythonFloatingPointValue => Float,
110+
PythonComplexValue => PythonBuiltinFunctions.Complex,
111+
PythonTextValue => Str,
112+
PythonByteSequenceValue => Bytes,
113+
PythonTemplateValue => PythonStandardModules.TemplateType,
114+
PythonInterpolationValue => PythonStandardModules.InterpolationType,
115+
PythonListValue => List,
116+
PythonTupleValue => Tuple,
117+
PythonDictionaryValue => Dict,
118+
PythonMappingProxyValue => PythonMappingProxies.Type,
119+
PythonSetValue { IsFrozen: true } => Frozenset,
120+
PythonSetValue => Set,
121+
PythonManagedObjectValue instance
122+
when ReferenceEquals(instance.Type, PythonBuiltinFunctions.ObjectType) =>
123+
PythonBuiltinFunctions.Object,
124+
PythonManagedObjectValue instance => instance.Type,
125+
PythonExceptionValue { ManagedType: { } managedType } => managedType,
126+
PythonExceptionValue exception => GetExceptionType(exception.TypeName),
127+
_ => ConstructorTypes.TryGetValue(GetRuntimeTypeName(value), out var constructorType)
128+
? constructorType
129+
: CreateOpaque(GetRuntimeTypeName(value)),
130+
};
131+
132+
internal static string GetRuntimeTypeName(PythonValue value) =>
133+
value switch
134+
{
135+
PythonBuiltinTypeValue => "type",
136+
PythonSuperProxyValue => "super",
137+
PythonIteratorValue { Iterable: PythonEnumerateSourceValue } => "enumerate",
138+
PythonIteratorValue { Iterable: PythonZipSourceValue } => "zip",
139+
PythonIteratorValue { Iterable: PythonMapSourceValue } => "map",
140+
PythonIteratorValue { Iterable: PythonFilterSourceValue } => "filter",
141+
PythonIteratorValue { Iterable: PythonListValue } => "list_iterator",
142+
PythonIteratorValue { Iterable: PythonTupleValue } => "tuple_iterator",
143+
PythonIteratorValue { Iterable: PythonTextValue text }
144+
when text.Value.All(character => character <= 127) => "str_ascii_iterator",
145+
PythonIteratorValue { Iterable: PythonTextValue } => "str_iterator",
146+
PythonIteratorValue { Iterable: PythonRangeValue } => "range_iterator",
147+
PythonIteratorValue { Iterable: PythonDictionaryValue } => "dict_keyiterator",
148+
PythonIteratorValue { Iterable: PythonDictionaryViewValue { Kind: "dict_keys" } } =>
149+
"dict_keyiterator",
150+
PythonIteratorValue { Iterable: PythonDictionaryViewValue { Kind: "dict_values" } } =>
151+
"dict_valueiterator",
152+
PythonIteratorValue { Iterable: PythonDictionaryViewValue { Kind: "dict_items" } } =>
153+
"dict_itemiterator",
154+
_ => ManagedObjectProtocols.GetTypeName(value),
155+
};
156+
15157
internal static readonly PythonBuiltinTypeValue Bool = new("bool", ConstructBool);
16158
internal static readonly PythonBuiltinTypeValue Bytes = new(
17159
"bytes",
@@ -62,14 +204,18 @@ [new PythonTextValue(string.Empty), null, null],
62204
[Bool, Bytes, Dict, Float, Frozenset, Int, List, Set, Str, Tuple];
63205

64206
internal static PythonBuiltinTypeValue CreateOpaque(string name) =>
65-
new(
207+
OpaqueTypes.GetOrAdd(
66208
name,
67-
(_, span) =>
68-
throw ManagedObjectProtocols.Fault(
69-
"DPY4009",
70-
$"The type '{name}' is not constructible in this runtime slice.",
71-
span,
72-
"TypeError"
209+
static key =>
210+
new(
211+
key,
212+
(_, span) =>
213+
throw ManagedObjectProtocols.Fault(
214+
"DPY4009",
215+
$"The type '{key}' is not constructible in this runtime slice.",
216+
span,
217+
"TypeError"
218+
)
73219
)
74220
);
75221

@@ -90,6 +236,10 @@ internal static bool IsInstance(PythonValue value, PythonBuiltinTypeValue type)
90236
"Template" => value is PythonTemplateValue,
91237
"Interpolation" => value is PythonInterpolationValue,
92238
"object" => true,
239+
"type" => value
240+
is PythonBuiltinTypeValue
241+
or PythonManagedTypeValue
242+
or PythonExceptionTypeValue,
93243
_ => false,
94244
};
95245

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,9 @@ internal PythonManagedTypeValue(
458458
string? exceptionBaseName = null
459459
)
460460
{
461-
ArgumentException.ThrowIfNullOrWhiteSpace(name);
461+
ArgumentNullException.ThrowIfNull(name);
462462
Name = name;
463+
QualName = name;
463464
Bases = bases;
464465
Mro = [this, .. linearizedBases ?? []];
465466
Construct = construct;
@@ -471,6 +472,8 @@ internal PythonManagedTypeValue(
471472
/// <summary>The declared base classes, in source order.</summary>
472473
internal IReadOnlyList<PythonManagedTypeValue> Bases { get; }
473474

475+
internal IReadOnlyList<PythonValue>? DeclaredBases { get; set; }
476+
474477
/// <summary>The C3 method resolution order, starting with this type (no `object` terminus).</summary>
475478
internal IReadOnlyList<PythonManagedTypeValue> Mro { get; }
476479

@@ -481,13 +484,16 @@ internal PythonManagedTypeValue(
481484

482485
internal Func<IReadOnlyList<PythonValue>, PythonValue>? Construct { get; }
483486

484-
internal string Name { get; }
487+
internal string Name { get; set; }
488+
489+
internal string? QualName { get; set; }
485490

486491
/// <summary>The `__name__` of the defining module (`__module__`); null for runtime-internal types.</summary>
487492
internal string? Module { get; set; }
488493

489494
/// <summary>`__module__.__qualname__`, the form CPython prints in reprs.</summary>
490-
internal string QualifiedDisplayName => Module is null ? Name : $"{Module}.{Name}";
495+
internal string QualifiedDisplayName =>
496+
Module is null ? QualName ?? Name : $"{Module}.{QualName ?? Name}";
491497

492498
public bool Equals(PythonManagedTypeValue? other) => ReferenceEquals(this, other);
493499

0 commit comments

Comments
 (0)