Prototype for None-checking. *Very* unstable.
authorDag Sverre Seljebotn <dagss@student.matnat.uio.no>
Thu, 21 Aug 2008 13:24:38 +0000 (15:24 +0200)
committerDag Sverre Seljebotn <dagss@student.matnat.uio.no>
Thu, 21 Aug 2008 13:24:38 +0000 (15:24 +0200)
Cython/Compiler/ExprNodes.py
Cython/Compiler/Main.py
Cython/Compiler/Optimize.py
tests/run/noneattributeacc.pyx [new file with mode: 0644]

index 253ff749333d3db4989415352b1aa9d617ddb56c..960db51653f4d0a1c10418904ffc82314d0b2bd0 100644 (file)
@@ -830,10 +830,12 @@ class NameNode(AtomicExprNode):
     #
     #  entry           Entry     Symbol table entry
     #  interned_cname  string
+    #  possible_var_values object See Optimize.FindPossibleVariableValues
     
     is_name = 1
     skip_assignment_decref = False
     entry = None
+    possible_var_values = None
 
     def create_analysed_rvalue(pos, env, entry):
         node = NameNode(pos)
@@ -2073,6 +2075,7 @@ class AttributeNode(ExprNode):
     #
     #  obj          ExprNode
     #  attribute    string
+    #  needs_none_check boolean        Used if obj is an extension type.
     #
     #  Used internally:
     #
@@ -2089,6 +2092,7 @@ class AttributeNode(ExprNode):
     result = "<error>"
     entry = None
     is_called = 0
+    needs_none_check = True
 
     def coerce_to(self, dst_type, env):
         #  If coercing to a generic pyobject and this is a cpdef function
@@ -2317,6 +2321,15 @@ class AttributeNode(ExprNode):
                     self.obj.py_result(),
                     self.interned_attr_cname,
                     code.error_goto_if_null(self.result_code, self.pos)))
+        else:
+            # result_code contains what is needed, but we may need to insert
+            # a check and raise an exception
+            if self.obj.type.is_extension_type and self.needs_none_check:
+                code.globalstate.use_utility_code(raise_noneattr_error_utility_code)
+                code.putln("if (%s) {" % code.unlikely("%s == Py_None") % self.obj.result_as(PyrexTypes.py_object_type))
+                code.putln("__Pyx_RaiseNoneAttributeError(\"%s\");" % self.attribute.encode("UTF-8")) # todo: fix encoding
+                code.putln(code.error_goto(self.pos))
+                code.putln("}")
     
     def generate_assignment_code(self, rhs, code):
         self.obj.generate_evaluation_code(code)
@@ -4551,3 +4564,13 @@ static INLINE int __Pyx_SetItemInt(PyObject *o, Py_ssize_t i, PyObject *v, int i
 """,
 """
 """]
+#------------------------------------------------------------------------------------
+
+raise_noneattr_error_utility_code = [
+"""
+static INLINE void __Pyx_RaiseNoneAttributeError(char* attrname);
+""", """
+static INLINE void __Pyx_RaiseNoneAttributeError(char* attrname) {
+    PyErr_Format(PyExc_AttributeError, "'NoneType' object has no attribute '%s'", attrname);
+}
+"""]
index 8a9139d0296d4cecbf13d645c890ff9755ccb94f..67216abd34c6718902f13f1f49fc4075e0a4dad5 100644 (file)
@@ -81,6 +81,7 @@ class Context:
         from ParseTreeTransforms import CreateClosureClasses, MarkClosureVisitor, DecoratorTransform
         from ParseTreeTransforms import ResolveOptions
         from Optimize import FlattenInListTransform, SwitchTransform, OptimizeRefcounting
+        from Optimize import OptimizeNoneChecking, FindPossibleVariableValues
         from Buffer import IntroduceBufferAuxiliaryVars
         from ModuleNode import check_c_classes
 
@@ -104,7 +105,9 @@ class Context:
             _check_c_classes,
             AnalyseExpressionsTransform(self),
             SwitchTransform(),
+            FindPossibleVariableValues(self),
             OptimizeRefcounting(self),
+            OptimizeNoneChecking(self),
 #            SpecialFunctions(self),
             #        CreateClosureClasses(context),
             ]
index 6036a5ade29364a62d1e3c75c6204979dd5c3196..8c19c126b3157623347ae9c4f0095a3fe7abfdc9 100644 (file)
@@ -149,3 +149,119 @@ class OptimizeRefcounting(Visitor.CythonTransform):
                 # Set a flag in NameNode to skip the decref
                 lhs.skip_assignment_decref = True
         return node
+
+
+class ExtTypePossibleValues:
+    can_be_none = True
+    def copy_with(self, can_be_none=None):
+        result = ExtTypePossibleValues()
+        if can_be_none is not None:
+            result.can_be_none = can_be_none
+        return result
+    def new(self):
+        "Polymorphic constructor"
+        return ExtTypePossibleValues()
+
+class FindPossibleVariableValues(Visitor.CythonTransform):
+    """
+    Annotates NameNodes with information about the possible values
+    the variable referred to can take, *at that point* in the execution.
+
+    This is done on a best effort basis, so we can be as smart or dumb
+    as we want. A do-nothing-op should always be valid.
+
+    Each type of variable keeps a different type of "variable range"
+    information.
+
+    This information is invalid if the tree is reorganized (read:
+    keep this transform late in the pipeline).
+
+    Currently this is done:
+    - Extension types gets flagged 
+    """
+
+    #
+    # Manage info stack
+    #
+    def create_empty_knowledge(self, scope):
+        knowledge = {}
+        for entry in scope.entries.values():
+            if entry.type.is_extension_type:
+                knowledge[entry] = ExtTypePossibleValues()
+        return knowledge
+    
+    def visit_ModuleNode(self, node):
+        self.knowledge = self.create_empty_knowledge(node.scope)
+        self.visitchildren(node)
+        return node
+    
+    def visit_FuncDefNode(self, node):
+        oldknow = self.knowledge
+        self.knowledge = self.create_empty_knowledge(node.local_scope)
+        self.visitchildren(node)
+        self.knowledge = oldknow
+        return node
+
+    def visit_NameNode(self, node):
+        node.possible_var_values = self.knowledge.get(node.entry, None)
+        return node
+    
+    #
+    # Conditions which restrict possible variable values
+    #
+    def visit_IfClauseNode(self, clause):
+        def process():
+            self.visitchildren(clause)
+            return clause
+
+        # we're lazy and only check in one specific easy case: single comparison with None
+        # the code is a bit nasty but handling the proper cases will force through better code
+        # anyway
+        cond = clause.condition
+        if not isinstance(cond, ExprNodes.PrimaryCmpNode): return process()
+        if clause.condition.cascade is not None: return process()
+        if isinstance(cond.operand1, ExprNodes.NoneNode):
+            operand_checked = cond.operand2
+        elif isinstance(cond.operand2, ExprNodes.NoneNode):
+            operand_checked = cond.operand1
+        else:
+            return process()
+        if not isinstance(operand_checked, ExprNodes.NameNode):
+            return process()
+        entry = operand_checked.entry
+        if entry not in self.knowledge:
+            # Not tracking this variable
+            return process()
+        # Finally!
+        if cond.operator == 'is_not':
+            # Within this block we can assume the variable is not None
+            # (until it is reassigned)
+            self.visitchildren(clause, attrs=["condition"])
+            oldvalues = self.knowledge[entry]
+            self.knowledge[entry] = oldvalues.copy_with(can_be_none=False)
+            self.visitchildren(clause, attrs=["body"])
+            self.knowledge[entry] = oldvalues
+            return clause
+        else:
+            return process()
+            
+
+    # Assignments which reset possible variable values
+    def visit_SingleAssignmentNode(self, node):
+        if isinstance(node.lhs, ExprNodes.NameNode):
+            entry = node.lhs.entry
+            if entry in self.knowledge:
+                self.knowledge[entry] = self.knowledge[entry].new()
+        self.visitchildren(node)
+        return node
+
+class OptimizeNoneChecking(Visitor.CythonTransform):
+    def visit_AttributeNode(self, node):
+        if isinstance(node.obj, ExprNodes.NameNode):
+            obj = node.obj
+            if obj.type.is_extension_type and not obj.possible_var_values.can_be_none:
+                node.needs_none_check = False
+        self.visitchildren(node)
+        return node
+
+                
diff --git a/tests/run/noneattributeacc.pyx b/tests/run/noneattributeacc.pyx
new file mode 100644 (file)
index 0000000..4a885d6
--- /dev/null
@@ -0,0 +1,52 @@
+"""
+Tests accessing attributes of extension type variables
+set to None
+
+>>> obj = MyClass(2, 3)
+>>> func(obj)
+2
+>>> func(None)
+Traceback (most recent call last):
+   ...
+AttributeError: 'NoneType' object has no attribute 'a'
+
+>>> checking(obj)
+2
+2
+>>> checking(None)
+var is None
+
+>>> check_and_assign(obj)
+Traceback (most recent call last):
+   ...
+AttributeError: 'NoneType' object has no attribute 'a'
+
+"""
+
+cdef class MyClass:
+    cdef int a, b
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+def func(MyClass var):
+    print var.a
+
+def some():
+    return MyClass(4, 5)
+
+def checking(MyClass var):
+    state = (var is None)
+    if not state:
+        print var.a
+    if var is not None:
+        print var.a
+    else:
+        print "var is None"
+
+def check_and_assign(MyClass var):
+    if var is not None:
+        print var.a
+        var = None
+        print var.a
+