Skip to content

Commit cd609e4

Browse files
committed
Compiled understands hasattr on classes.
1 parent 642ebc8 commit cd609e4

15 files changed

+339
-11
lines changed

typed_python/HeldClassType.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ RefTo* HeldClass::getRefToType() {
243243

244244
void HeldClass::delAttribute(instance_ptr self, int memberIndex) const {
245245
Type* member_t = m_members[memberIndex].getType();
246+
246247
if (checkInitializationFlag(self, memberIndex)) {
247248
member_t->destroy(eltPtr(self, memberIndex));
248249
clearInitializationFlag(self, memberIndex);
@@ -337,6 +338,8 @@ void HeldClass::copy_constructor(instance_ptr self, instance_ptr other) {
337338
if (checkInitializationFlag(other, k)) {
338339
member_t->copy_constructor(self+m_byte_offsets[k], other+m_byte_offsets[k]);
339340
setInitializationFlag(self, k);
341+
} else {
342+
clearInitializationFlag(self, k);
340343
}
341344
}
342345
}

typed_python/PyRefToInstance.cpp

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,11 @@ int PyRefToInstance::tp_setattr_concrete(PyObject* attrName, PyObject* attrVal)
129129
}
130130

131131
HeldClass* clsType = (HeldClass*)type()->getEltType();
132+
instance_ptr heldClassBody = *(instance_ptr*)dataPtr();
132133

133-
int i = clsType->getMemberIndex(PyUnicode_AsUTF8(attrName));
134+
int memberIndex = clsType->getMemberIndex(PyUnicode_AsUTF8(attrName));
134135

135-
if (i < 0) {
136+
if (memberIndex < 0) {
136137
auto it = clsType->getClassMembers().find(PyUnicode_AsUTF8(attrName));
137138
if (it == clsType->getClassMembers().end()) {
138139
PyErr_Format(
@@ -150,23 +151,44 @@ int PyRefToInstance::tp_setattr_concrete(PyObject* attrName, PyObject* attrVal)
150151
return -1;
151152
}
152153

153-
Type* eltType = clsType->getMemberType(i);
154+
if (!attrVal) {
155+
if (clsType->getMemberIsNonempty(memberIndex)) {
156+
PyErr_Format(
157+
PyExc_AttributeError,
158+
"Attribute '%S' cannot be deleted",
159+
attrName
160+
);
161+
return -1;
162+
}
154163

155-
Type* attrType = extractTypeFrom(attrVal->ob_type);
164+
if (!clsType->checkInitializationFlag(heldClassBody, memberIndex)) {
165+
PyErr_Format(
166+
PyExc_AttributeError,
167+
"Attribute '%S' is not initialized",
168+
attrName
169+
);
170+
return -1;
171+
}
156172

157-
instance_ptr heldClassBody = *(instance_ptr*)dataPtr();
173+
clsType->delAttribute(heldClassBody, memberIndex);
174+
return 0;
175+
}
176+
177+
Type* eltType = clsType->getMemberType(memberIndex);
178+
179+
Type* attrType = extractTypeFrom(attrVal->ob_type);
158180

159181
if (Type::typesEquivalent(eltType, attrType)) {
160182
PyInstance* item_w = (PyInstance*)attrVal;
161183

162-
clsType->setAttribute(heldClassBody, i, item_w->dataPtr());
184+
clsType->setAttribute(heldClassBody, memberIndex, item_w->dataPtr());
163185

164186
return 0;
165187
} else if (attrType && attrType->getTypeCategory() == Type::TypeCategory::catRefTo &&
166188
((RefTo*)attrType)->getEltType() == eltType) {
167189
PyInstance* item_w = (PyInstance*)attrVal;
168190

169-
clsType->setAttribute(heldClassBody, i, *(instance_ptr*)item_w->dataPtr());
191+
clsType->setAttribute(heldClassBody, memberIndex, *(instance_ptr*)item_w->dataPtr());
170192

171193
return 0;
172194
} else {
@@ -182,7 +204,7 @@ int PyRefToInstance::tp_setattr_concrete(PyObject* attrName, PyObject* attrVal)
182204
return -1;
183205
}
184206

185-
clsType->setAttribute(heldClassBody, i, tempObj);
207+
clsType->setAttribute(heldClassBody, memberIndex, tempObj);
186208

187209
eltType->destroy(tempObj);
188210
tp_free(tempObj);

typed_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,5 @@
9595
# when we're acquiring and releasing frequently.
9696
gilReleaseThreadLoop = threading.Thread(target=_types.gilReleaseThreadLoop, daemon=True)
9797
gilReleaseThreadLoop.start()
98+
99+
_types.setGilReleaseThreadLoopSleepMicroseconds(500)

typed_python/compiler/function_conversion_context.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2212,6 +2212,25 @@ def convert_iteration_expression(self, to_iterate, ast, variableSuffix, controlF
22122212

22132213
self.assignToLocalVariable(iter_varname, iterator_object, variableStates)
22142214

2215+
if iterator_object.expr_type.has_intiter():
2216+
# execute the 'intiter' fastpath for iterators. Classes where iteration
2217+
# is simply enumerating a list of values indexed by an integer can
2218+
# be replaced with a simple loop, which downstream compilation steps
2219+
# can see through better. Eventually, we'd like to be able to pull apart
2220+
# everything we're doing with classes for optimization purposes,
2221+
# but at the moment, this is more expedient.
2222+
iter_varname = ".iterate_over." + str(ast.line_number) + variableSuffix
2223+
2224+
self.assignToLocalVariable(iter_varname, iterator_object, variableStates)
2225+
2226+
inner, innerReturns = self.convert_statement_list_ast(
2227+
list(rewriteIntiterForLoop(iter_varname, ast.target, ast.body, ast.orelse)),
2228+
variableStates,
2229+
controlFlowBlocks,
2230+
)
2231+
2232+
return context.finalize(inner, exceptionsTakeFrom=ast), innerReturns
2233+
22152234
while True:
22162235
# track the initial variable states
22172236
initVariableStates = variableStates.clone()

typed_python/compiler/python_object_representation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from typed_python.compiler.type_wrappers.deserialize_wrapper import DeserializeWrapper
5858
from typed_python.compiler.type_wrappers.time_wrapper import TimeWrapper
5959
from typed_python.compiler.type_wrappers.super_wrapper import SuperWrapper
60+
from typed_python.compiler.type_wrappers.hasattr_wrapper import HasattrWrapper
6061
from typed_python.compiler.type_wrappers.compiler_introspection_wrappers import (
6162
IsCompiledWrapper,
6263
TypeKnownToCompiler,
@@ -240,6 +241,9 @@ def pythonObjectRepresentation(context, f, owningGlobalScopeAndName=None):
240241
if isinstance(f, CompilableBuiltin):
241242
return TypedExpression(context, native_ast.nullExpr, f, False)
242243

244+
if f is hasattr:
245+
return TypedExpression(context, native_ast.nullExpr, HasattrWrapper(), False)
246+
243247
if f is len:
244248
return TypedExpression(context, native_ast.nullExpr, LenWrapper(), False)
245249

typed_python/compiler/tests/class_compilation_test.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,3 +2932,72 @@ def doIt():
29322932

29332933
doIt()
29342934
Entrypoint(doIt)()
2935+
2936+
def test_class_hasattr(self):
2937+
class C(Class):
2938+
x = Member(int)
2939+
2940+
def __init__(self):
2941+
del self.x
2942+
2943+
def __init__(self, x): # noqa
2944+
self.x = x
2945+
2946+
@Entrypoint
2947+
def compiledHasattr(c):
2948+
return hasattr(c, 'x')
2949+
2950+
@Entrypoint
2951+
def compiledGenericHasattr(c, attr):
2952+
return hasattr(c, attr)
2953+
2954+
assert compiledGenericHasattr.resultTypeFor(C, str).typeRepresentation is bool
2955+
2956+
assert hasattr(C(x=1), 'x')
2957+
assert compiledHasattr(C(x=1))
2958+
assert compiledGenericHasattr(C(x=1), 'x')
2959+
2960+
assert not hasattr(C(), 'x')
2961+
assert not compiledHasattr(C())
2962+
assert not compiledGenericHasattr(C(), 'x')
2963+
2964+
@Entrypoint
2965+
def compiledDel():
2966+
c = C(x=1)
2967+
assert hasattr(c, 'x')
2968+
del c.x
2969+
assert not hasattr(c, 'x')
2970+
2971+
compiledDel()
2972+
2973+
def test_class_hasattr_perf(self):
2974+
class C(Class):
2975+
x = Member(float)
2976+
2977+
@Entrypoint
2978+
def loopWithoutHasattr(c):
2979+
res = 0.0
2980+
for _ in range(10000000):
2981+
res += c.x
2982+
return res
2983+
2984+
@Entrypoint
2985+
def loopWithHasattr(c):
2986+
res = 0.0
2987+
for _ in range(10000000):
2988+
if hasattr(c, 'x'):
2989+
res += c.x
2990+
return res
2991+
2992+
loopWithHasattr(C())
2993+
loopWithoutHasattr(C())
2994+
2995+
t0 = time.time()
2996+
loopWithHasattr(C())
2997+
elapsedWith = time.time() - t0
2998+
2999+
t0 = time.time()
3000+
loopWithoutHasattr(C())
3001+
elapsedWithout = time.time() - t0
3002+
3003+
assert .6 < elapsedWithout / elapsedWith < 1.4

typed_python/compiler/tests/generators_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,14 @@ def listCompSumUntyped(x):
165165

166166
@flaky(max_runs=3, min_passes=1)
167167
def test_untyped_tuple_from_listcomp_perf(self):
168+
import time
169+
168170
def sum(iterable):
171+
t0 = time.time()
169172
res = 0
170173
for s in iterable:
171174
res += s
175+
print("took ", time.time() - t0)
172176
return res
173177

174178
@Entrypoint

typed_python/compiler/tests/held_class_compilation_test.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,68 @@ def doIt():
432432

433433
assert len(delType) >= 1
434434
assert delType[0] == C
435+
436+
def test_class_hasattr(self):
437+
@Held
438+
class C(Class):
439+
x = Member(int)
440+
441+
def __init__(self):
442+
assert hasattr(self, 'x')
443+
del self.x
444+
assert not hasattr(self, 'x')
445+
446+
def __init__(self, x): # noqa
447+
self.x = x
448+
449+
@Entrypoint
450+
def compiledHasattr(c):
451+
return hasattr(c, 'x')
452+
453+
assert hasattr(C(x=1), 'x')
454+
assert compiledHasattr(C(x=1))
455+
456+
assert not hasattr(C(), 'x')
457+
assert not compiledHasattr(C())
458+
459+
@Entrypoint
460+
def compiledDel():
461+
c = C(x=1)
462+
assert hasattr(c, 'x')
463+
del c.x
464+
assert not hasattr(c, 'x')
465+
466+
compiledDel()
467+
468+
def test_class_hasattr_perf(self):
469+
@Held
470+
class C(Class):
471+
x = Member(float)
472+
473+
@Entrypoint
474+
def loopWithoutHasattr(c):
475+
res = 0.0
476+
for _ in range(10000000):
477+
res += c.x
478+
return res
479+
480+
@Entrypoint
481+
def loopWithHasattr(c):
482+
res = 0.0
483+
for _ in range(10000000):
484+
if hasattr(c, 'x'):
485+
res += c.x
486+
return res
487+
488+
loopWithHasattr(C())
489+
loopWithoutHasattr(C())
490+
491+
t0 = time.time()
492+
loopWithHasattr(C())
493+
elapsedWith = time.time() - t0
494+
495+
t0 = time.time()
496+
loopWithoutHasattr(C())
497+
elapsedWithout = time.time() - t0
498+
499+
assert .6 < elapsedWithout / elapsedWith < 1.4

typed_python/compiler/type_wrappers/class_wrapper.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,20 @@ def __init__(self, t):
196196
)
197197
).load()
198198

199+
def convert_hasattr(self, context, instance, attribute):
200+
if attribute.constantValue is not None and attribute.constantValue in self.nameToIndex:
201+
ix = self.nameToIndex[attribute.constantValue]
202+
203+
if self.fieldGuaranteedInitialized(ix):
204+
return context.constant(True)
205+
206+
return context.pushPod(
207+
bool,
208+
self.isInitializedNativeExpr(instance, ix)
209+
)
210+
211+
return super().convert_hasattr(context, instance, attribute)
212+
199213
def fieldGuaranteedInitialized(self, ix):
200214
if self.classType.MemberNames[ix] not in self.typeRepresentation.ClassMembers:
201215
raise Exception(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright 2017-2023 typed_python Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import time
15+
16+
17+
from typed_python.compiler.type_wrappers.wrapper import Wrapper
18+
import typed_python.compiler.native_ast as native_ast
19+
20+
21+
class HasattrWrapper(Wrapper):
22+
is_pod = True
23+
is_empty = False
24+
is_pass_by_ref = False
25+
26+
def __init__(self):
27+
super().__init__(time.time)
28+
29+
def getNativeLayoutType(self):
30+
return native_ast.Type.Void()
31+
32+
def convert_call(self, context, expr, args, kwargs):
33+
if len(args) == 2 and len(kwargs) == 0:
34+
return args[0].convert_hasattr(args[1])
35+
36+
return expr.toPyObj().convert_call(args, kwargs).toBool()

0 commit comments

Comments
 (0)