# HG changeset patch # User Paul Boddie # Date 1486507060 -3600 # Node ID 11a673460532bfad67912137e6b312af1033f2b2 # Parent c4203b80000740853a2fa1fd2e668a45dc631880 Enhanced attribute access modifiers to communicate details of invocations, moving detection of unsuitable invocations (due to argument/parameter number mismatches) to the deducer. The result nodes needed changing to accommodate invocation-related information so that the previously-introduced warnings can still be generated. diff -r c4203b800007 -r 11a673460532 common.py --- a/common.py Mon Feb 06 22:30:47 2017 +0100 +++ b/common.py Tue Feb 07 23:37:40 2017 +0100 @@ -114,7 +114,7 @@ # Retain the assignment value expression and track invocations. self.in_assignment = None - self.in_invocation = False + self.in_invocation = None # Attribute chain state management. @@ -725,7 +725,7 @@ self.chain_assignment.append(self.in_assignment) self.chain_invocation.append(self.in_invocation) self.in_assignment = None - self.in_invocation = False + self.in_invocation = None def restore_attribute_chain(self, attrs): diff -r c4203b800007 -r 11a673460532 deducer.py --- a/deducer.py Mon Feb 06 22:30:47 2017 +0100 +++ b/deducer.py Tue Feb 07 23:37:40 2017 +0100 @@ -90,7 +90,8 @@ # Accesses that are assignments or invocations. self.reference_assignments = set() - self.reference_invocations = set() + self.reference_invocations = {} + self.reference_invocations_unsuitable = {} # Map locations to types, constrained indicators and attributes. @@ -104,6 +105,7 @@ self.access_constrained = set() self.referenced_attrs = {} self.referenced_objects = {} + self.accessor_invocation_types = {} # Details of access operations. @@ -139,6 +141,7 @@ self.init_aliases() self.modify_mutated_attributes() self.identify_references() + self.classify_invocations() self.classify_accessors() self.classify_accesses() self.initialise_access_plans() @@ -346,6 +349,7 @@ f_attrs = open(join(self.output, "attributes"), "w") f_tests = open(join(self.output, "tests"), "w") f_warnings = open(join(self.output, "attribute_warnings"), "w") + f_unsuitable = open(join(self.output, "invocation_warnings"), "w") try: locations = self.referenced_attrs.keys() @@ -383,6 +387,15 @@ print >>f_attr_summary, encode_access_location(location), encode_constrained(constrained), \ test_type and "-".join(test_type) or "untested", sorted_output(all_accessed_attrs) + # Write details of potentially unsuitable invocation + # occurrences. + + unsuitable = self.reference_invocations_unsuitable.get(location) + if unsuitable: + unsuitable = map(str, unsuitable) + unsuitable.sort() + print >>f_unsuitable, encode_access_location(location), ", ".join(unsuitable) + else: print >>f_warnings, encode_access_location(location) @@ -391,6 +404,7 @@ f_attrs.close() f_tests.close() f_warnings.close() + f_unsuitable.close() def write_access_plans(self): @@ -430,6 +444,55 @@ finally: f_attrs.close() + def classify_invocations(self): + + """ + Classify invocations, suggesting guard opportunities where accessors + providing attributes involved in invocations can be sufficiently + determined, recording invocation candidates that would not be suitable. + """ + + for access_location, arguments in self.reference_invocations.items(): + + # Obtain the possible attributes. + + referenced_attrs = self.referenced_attrs[access_location] + + if referenced_attrs: + object_types = set() + unsuitable = set() + + for attrtype, object_type, attr in referenced_attrs: + + # Obtain identifiable callables. + + objpath = attr.get_origin() + if objpath: + parameters = self.importer.function_parameters.get(objpath) + if parameters: + defaults = self.importer.function_defaults.get(objpath) + + # Determine whether the specified arguments are + # compatible with the callable signature. + + if arguments >= len(parameters) - len(defaults) and \ + arguments <= len(parameters): + + object_types.add(object_type) + else: + unsuitable.add(attr) + + # Record the unsuitable candidates for the invocation. + + if unsuitable: + self.reference_invocations_unsuitable[access_location] = unsuitable + + # Record the possible accessor types for the invocation. + + for accessor_location in self.get_accessors_for_access(access_location): + init_item(self.accessor_invocation_types, accessor_location, set) + self.accessor_invocation_types[accessor_location].update(object_types) + def classify_accessors(self): "For each program location, classify accessors." @@ -866,7 +929,7 @@ # Now only process assignments and invocations. if invocation: - self.reference_invocations.add(access_location) + self.reference_invocations[access_location] = invocation continue elif not assignment: continue @@ -1576,7 +1639,9 @@ # Obtain attribute references for the access. - attrs = [attr for _attrtype, object_type, attr in self.referenced_attrs[access_location]] + attrs = [] + for _attrtype, object_type, attr in self.referenced_attrs[access_location]: + attrs.append(attr) # Separate the different attribute types. @@ -1635,7 +1700,9 @@ # Alias references an attribute access. if attrnames: - attrs = [attr for attrtype, object_type, attr in self.referenced_attrs[access_location]] + attrs = [] + for attrtype, object_type, attr in self.referenced_attrs[access_location]: + attrs.append(attr) refs.update(attrs) # Alias references a name, not an access. @@ -1739,15 +1806,26 @@ From the given 'class_types', identify methods for the given 'attrnames' that are being invoked, returning a filtered collection of class types. + + This method may be used to remove class types from consideration where + their attributes are methods that are directly invoked: method + invocations must involve instance accessors. """ to_filter = set() for class_type in class_types: for attrname in attrnames: + + # Attempt to obtain a class attribute of the given name. This + # may return an attribute provided by an ancestor class. + ref = self.importer.get_class_attribute(class_type, attrname) parent_class = ref and ref.parent() + # If such an attribute is a method and would be available on + # the given class, record the class for filtering. + if ref and ref.has_kind("") and ( parent_class == class_type or class_type in self.descendants[parent_class]): @@ -1824,6 +1902,8 @@ return attrs + # Attribute access plan formulation. + class_tests = ( ("guarded", "specific", "type"), ("guarded", "common", "type"), diff -r c4203b800007 -r 11a673460532 encoders.py --- a/encoders.py Mon Feb 06 22:30:47 2017 +0100 +++ b/encoders.py Tue Feb 07 23:37:40 2017 +0100 @@ -78,7 +78,7 @@ def encode_modifiers(modifiers): - "Encode assignment details from 'modifiers'." + "Encode assignment and invocation details from 'modifiers'." all_modifiers = [] for t in modifiers: @@ -87,16 +87,38 @@ def encode_modifier_term(t): - "Encode modifier 't' representing assignment status." + "Encode modifier 't' representing an assignment or an invocation." assignment, invocation = t - return assignment and "=" or invocation and "!" or "_" + if assignment: + return "=" + elif invocation is not None: + return "(%d)" % invocation + else: + return "_" -def decode_modifier_term(s): +def decode_modifiers(s): + + "Decode 's' containing modifiers." + + i = 0 + end = len(s) - "Decode modifier term 's' representing assignment status." + modifiers = [] - return (s == "=", s == "!") + while i < end: + if s[i] == "=": + modifiers.append((True, None)) + i += 1 + elif s[i] == "(": + j = s.index(")", i) + modifiers.append((False, int(s[i+1:j]))) + i = j + 1 + else: + modifiers.append((False, None)) + i += 1 + + return modifiers diff -r c4203b800007 -r 11a673460532 inspector.py --- a/inspector.py Mon Feb 06 22:30:47 2017 +0100 +++ b/inspector.py Tue Feb 07 23:37:40 2017 +0100 @@ -438,7 +438,7 @@ # Record attribute usage in the tracker, and record the branch # information for the access. - branches = tracker.use_attribute(name, attrname, self.in_invocation, assignment) + branches = tracker.use_attribute(name, attrname, self.in_invocation is not None, assignment) if not branches: raise InspectError("Name %s is accessed using %s before an assignment." % ( @@ -746,12 +746,12 @@ # target of an invocation, potentially affecting attribute accesses. in_invocation = self.in_invocation - self.in_invocation = True + self.in_invocation = len(n.args) # Process the expression, obtaining any identified reference. name_ref = self.process_structure_node(n.node) - self.in_invocation = False + self.in_invocation = None # Process the arguments. @@ -885,7 +885,7 @@ if branches: self.record_branches_for_access(branches, n.name, None) - access_number = self.record_access_details(n.name, None, False, False) + access_number = self.record_access_details(n.name, None, None, None) return LocalNameRef(n.name, access_number) # Possible global or built-in name. @@ -1147,7 +1147,7 @@ """ For the given 'name' and 'attrnames', record an access indicating - whether 'assignment' is occurring. + whether an 'assignment' or an 'invocation' is occurring. These details correspond to accesses otherwise recorded by the attribute accessor and attribute access dictionaries. diff -r c4203b800007 -r 11a673460532 modules.py --- a/modules.py Mon Feb 06 22:30:47 2017 +0100 +++ b/modules.py Tue Feb 07 23:37:40 2017 +0100 @@ -22,7 +22,7 @@ from common import get_builtin_class, get_builtin_module, init_item, \ remove_items, CommonModule -from encoders import decode_modifier_term, decode_usage, encode_modifiers, encode_usage +from encoders import decode_modifiers, decode_usage, encode_modifiers, encode_usage from referencing import decode_reference, Reference from results import ResolvedNameRef import sys @@ -605,7 +605,7 @@ access = name, attrnames init_item(self.attr_access_modifiers, objpath, dict) init_item(self.attr_access_modifiers[objpath], access, list) - modifiers = [decode_modifier_term(s) for s in value] + modifiers = decode_modifiers(value) self.attr_access_modifiers[objpath][access] = modifiers line = f.readline().rstrip() diff -r c4203b800007 -r 11a673460532 results.py --- a/results.py Mon Feb 06 22:30:47 2017 +0100 +++ b/results.py Tue Feb 07 23:37:40 2017 +0100 @@ -36,6 +36,9 @@ def references(self): return None + def unsuitable_invocations(self): + return None + def get_name(self): return None diff -r c4203b800007 -r 11a673460532 translator.py --- a/translator.py Mon Feb 06 22:30:47 2017 +0100 +++ b/translator.py Tue Feb 07 23:37:40 2017 +0100 @@ -91,9 +91,13 @@ "A reference to a name in the translation." - def __init__(self, name, ref, expr=None, parameter=None): + def __init__(self, name, ref, expr=None, parameter=None, unsuitable=None): results.ResolvedNameRef.__init__(self, name, ref, expr) self.parameter = parameter + self.unsuitable = unsuitable + + def unsuitable_invocations(self): + return self.unsuitable def __str__(self): @@ -189,14 +193,18 @@ "A translation result for an attribute access." - def __init__(self, instructions, refs, accessor_kinds): + def __init__(self, instructions, refs, accessor_kinds, unsuitable): InstructionSequence.__init__(self, instructions) self.refs = refs self.accessor_kinds = accessor_kinds + self.unsuitable = unsuitable def references(self): return self.refs + def unsuitable_invocations(self): + return self.unsuitable + def get_origin(self): return self.refs and len(self.refs) == 1 and first(self.refs).get_origin() @@ -724,8 +732,9 @@ name_ref = attr_expr and attr_expr.is_name() and attr_expr name = name_ref and self.get_name_for_tracking(name_ref.name, name_ref and name_ref.final()) or None - location = self.get_access_location(name) + location = self.get_access_location(name, self.attrs) refs = self.get_referenced_attributes(location) + unsuitable = self.get_referenced_attribute_invocations(location) # Generate access instructions. @@ -768,13 +777,13 @@ self.record_temp(temp_subs[sub]) del self.attrs[0] - return AttrResult(output, refs, self.get_accessor_kinds(location)) + return AttrResult(output, refs, self.get_accessor_kinds(location), unsuitable) def get_referenced_attributes(self, location): """ Convert 'location' to the form used by the deducer and retrieve any - identified attribute. + identified attributes. """ access_location = self.deducer.const_accesses.get(location) @@ -783,17 +792,27 @@ refs.append(attr) return refs + def get_referenced_attribute_invocations(self, location): + + """ + Convert 'location' to the form used by the deducer and retrieve any + identified attribute invocation details. + """ + + access_location = self.deducer.const_accesses.get(location) + return self.deducer.reference_invocations_unsuitable.get(access_location or location) + def get_accessor_kinds(self, location): "Return the accessor kinds for 'location'." return self.optimiser.accessor_kinds[location] - def get_access_location(self, name): + def get_access_location(self, name, attrnames=None): """ - Using the current namespace and the given 'name', return the access - location. + Using the current namespace, the given 'name', and the 'attrnames' + employed in an access, return the access location. """ path = self.get_path_for_access() @@ -801,7 +820,7 @@ # Get the location used by the deducer and optimiser and find any # recorded access. - attrnames = ".".join(self.attrs) + attrnames = attrnames and ".".join(self.attrs) access_number = self.get_access_number(path, name, attrnames) self.update_access_number(path, name, attrnames) return (path, name, attrnames, access_number) @@ -1203,30 +1222,16 @@ # Other targets are retrieved at run-time. else: - refs = expr.references() - - # Attempt to test the number of arguments and warn about possible - # invocation problems. - - if refs: - for ref in refs: + unsuitable = expr.unsuitable_invocations() + + if unsuitable: + for ref in unsuitable: _objpath = ref.get_origin() - _parameters = self.importer.function_parameters.get(_objpath) - - if _parameters is None: - continue - - # Determine whether the possible target has a different - # number of parameters to the number of arguments given. - - num_parameters = len(_parameters) - _defaults = len(self.importer.function_defaults.get(_objpath, [])) - - if len(n.args) < num_parameters - _defaults or len(n.args) > num_parameters: - print "In %s, at line %d, inappropriate number of " \ - "arguments given. Need %d arguments to call %s." % ( - self.get_namespace_path(), n.lineno, num_parameters, - _objpath) + num_parameters = len(self.importer.function_parameters[_objpath]) + print "In %s, at line %d, inappropriate number of " \ + "arguments given. Need %d arguments to call %s." % ( + self.get_namespace_path(), n.lineno, num_parameters, + _objpath) # Arguments are presented in a temporary frame array with any context # always being the first argument. Where it would be unused, it may be @@ -1503,11 +1508,16 @@ parameter = n.name == "self" and self.in_method() or \ parameters and n.name in parameters + # Find any invocation details. + + location = self.get_access_location(n.name) + unsuitable = self.get_referenced_attribute_invocations(location) + # Qualified names are used for resolved static references or for # static namespace members. The reference should be configured to return # such names. - return TrResolvedNameRef(n.name, ref, expr=expr, parameter=parameter) + return TrResolvedNameRef(n.name, ref, expr=expr, parameter=parameter, unsuitable=unsuitable) def process_not_node(self, n):