From 6e41c1cdd8e4348ed9bdedbeaef8728364af72f2 Mon Sep 17 00:00:00 2001 From: Marion Date: Fri, 23 Aug 2013 09:50:55 +0000 Subject: introduce type system for Primitives and simplify argument slot definitions - This commit breaks everything! Only the square example still works. The rest will be fixed in subsequent commits. --- (limited to 'TurtleArt/tatype.py') diff --git a/TurtleArt/tatype.py b/TurtleArt/tatype.py new file mode 100644 index 0000000..c7c1fe3 --- /dev/null +++ b/TurtleArt/tatype.py @@ -0,0 +1,269 @@ +#Copyright (c) 2013 Marion Zepf + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +""" type system for Primitives and their arguments """ + +import ast +from gettext import gettext as _ + +from taconstants import (Color, CONSTANTS) +from tautils import debug_output + + +# types are defined as numbers because they are faster to compare than strings +TYPE_OBJECT = 0 +TYPE_CHAR = 1 +TYPE_COLOR = 2 +TYPE_FLOAT = 3 +TYPE_INT = 4 +TYPE_NEGATIVE = 5 +TYPE_NUMBER = 6 +TYPE_NUMERIC_STRING = 7 +TYPE_POSITIVE = 8 +TYPE_STRING = 9 +TYPE_ZERO = 10 +ARG_TYPES = ( + # possible types of arguments to Primitives + TYPE_CHAR, TYPE_COLOR, TYPE_FLOAT, TYPE_INT, TYPE_NEGATIVE, TYPE_NUMBER, + TYPE_NUMERIC_STRING, TYPE_OBJECT, TYPE_POSITIVE, TYPE_STRING, TYPE_ZERO + # TODO add list types +) + + +def get_type(x): + """ Return the most specific type in the type hierarchy that applies to x + and a boolean indicating whether x is an AST. If the type cannot be + determined, return TYPE_OBJECT as the type. """ + # non-AST types + if isinstance(x, (float, int, long)): + if x > 0: + return (TYPE_POSITIVE, False) + elif x == 0: + return (TYPE_ZERO, False) + else: + return (TYPE_NEGATIVE, False) + elif isinstance(x, basestring): + if len(x) == 1: + return (TYPE_CHAR, False) + try: + float(x) + except ValueError: + return (TYPE_STRING, False) + else: + return (TYPE_NUMERIC_STRING, False) + elif isinstance(x, Color): + return (TYPE_COLOR, False) + elif hasattr(x, "return_type"): + return (x.return_type, False) + + # AST types + elif isinstance(x, ast.Num): + return (get_type(x.n)[0], True) + elif isinstance(x, ast.Str): + return (get_type(x.s)[0], True) + elif isinstance(x, ast.Name): + try: + # we need to have imported CONSTANTS for this to work + value = eval(x.id) + except NameError: + return (TYPE_OBJECT, True) + else: + return (get_type(value)[0], True) + elif isinstance(x, ast.Call): + if x.func.__name__ in (TYPE_FLOAT, TYPE_INT): + return (x.func.__name__, True) + elif x.func.__name__ in ('str', 'unicode'): + return (TYPE_STRING, True) + + return (TYPE_OBJECT, isinstance(x, ast.AST)) + + +def is_instancemethod(method): + # TODO how to access the type `instancemethod` directly? + return type(method).__name__ == "instancemethod" + +def is_bound_instancemethod(method): + return is_instancemethod(method) and method.im_self is not None + +def is_unbound_instancemethod(method): + return is_instancemethod(method) and method.im_self is None + +def is_staticmethod(method): + # TODO how to access the type `staticmethod` directly? + return type(method).__name__ == "staticmethod" + + +def identity(x): + return x + +TYPE_CONVERTERS = { + # Type hierarchy: If there is a converter A -> B, then A is a subtype of B. + # The converter from A to B is stored under TYPE_CONVERTERS[A][B]. + # The relation describing the type hierarchy must be transitive, i.e. + # converting A -> C must yield the same result as converting A -> B -> C. + # TYPE_OBJECT is the supertype of everything. + TYPE_CHAR: { + TYPE_POSITIVE: ord, # ignore the zero byte, chr(0) + TYPE_STRING: identity}, + TYPE_COLOR: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_NUMBER: int, + TYPE_STRING: Color.get_number_string}, + TYPE_FLOAT: { + TYPE_INT: int, + TYPE_NUMBER: identity, + TYPE_STRING: str}, + TYPE_INT: { + TYPE_FLOAT: float, + TYPE_NUMBER: identity, + TYPE_STRING: str}, + TYPE_NEGATIVE: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_NUMBER: identity, + TYPE_STRING: str}, + TYPE_NUMBER: { + TYPE_STRING: str}, + TYPE_NUMERIC_STRING: { + TYPE_FLOAT: float, + TYPE_STRING: identity}, + TYPE_POSITIVE: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_NUMBER: identity, + TYPE_STRING: str}, + TYPE_ZERO: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_NUMBER: identity, + TYPE_STRING: str} +} + + +def get_call_ast(func_name, args=None, keywords=None): + """ Return an AST representing the call to a function with the name + func_name, passing it the arguments args (given as a list) and the + keyword arguments keywords (given as a dictionary). """ + if args is None: + args = [] + if keywords is None: + keywords = {} + return ast.Call(func=ast.Name(id=func_name, + ctx=ast.Load), + args=args, + keywords=keywords, + starargs=None, + kwargs=None) + + +class ConversionError(BaseException): + """ Error that is raised when something goes wrong during type conversion """ + + def __init__(self, message): + """ message -- the error message """ + self.message = message + + def __str__(self): + return _("error during type conversion") + ": " + str(self.message) + + +def convert(x, new_type, old_type=None): + """ Convert x to the new type if possible. + old_type -- the type of x. If not given, it is computed. """ + if new_type not in ARG_TYPES: + raise ValueError('%s is not a type in the type hierarchy' + % (repr(new_type))) + # every type can be converted to TYPE_OBJECT + if new_type == TYPE_OBJECT: + return x + if old_type not in ARG_TYPES: + (old_type, is_an_ast) = get_type(x) + else: + is_an_ast = isinstance(x, ast.AST) + # every type can be converted to itself + if old_type == new_type: + return x + + # is there a converter for this pair of types? + converters_from_old = TYPE_CONVERTERS.get(old_type) + if converters_from_old is not None: + converter = converters_from_old.get(new_type) + if converter is not None: + # apply the converter + if is_an_ast: + if converter == identity: + return x + elif is_instancemethod(converter): + func = ast.Attribute(value=x, + attr=converter.im_func.__name__, + ctx=ast.Load) + return ast.Call(func=func, + args=[], + keywords={}, + starargs=None, + kwargs=None) + else: + func_name = converter.__name__ + return get_call_ast(func_name, x) + else: + return converter(x) + else: + # form the transitive closure of all types that old_type can be + # converted to, and look for new_type there + backtrace = converters_from_old.copy() + new_backtrace = backtrace + break_all = False + while True: + newest_backtrace = {} + for t in new_backtrace: + for new_t in TYPE_CONVERTERS.get(t, {}): + if new_t not in backtrace: + newest_backtrace[new_t] = t + backtrace[new_t] = t + if new_t == new_type: + break_all = True + break + if break_all: + break + if break_all or not newest_backtrace: + break + new_backtrace = newest_backtrace + # use the backtrace to find the path from old_type to new_type + if new_type in backtrace: + conversion_path = [new_type] + while conversion_path[-1] in backtrace: + conversion_path.append(backtrace[conversion_path[-1]]) + # the last thing must be the conversion function + conversion_path.pop() + # the rest are the intermediate types and new_type + result = x + intermed_type = old_type + while conversion_path: + t = conversion_path.pop() + result = convert(result, t, old_type=intermed_type) + intermed_type = t + return result + + # if we have not returned the result, then there is no converter available + raise ConversionError("type %s cannot be converted to type %s" + % (repr(old_type), repr(new_type))) + + -- cgit v0.9.1