From db8c29ce3204b79aed7b9679c91f7abf3f6f2102 Mon Sep 17 00:00:00 2001 From: Marion Zepf Date: Tue, 29 Oct 2013 21:25:26 +0000 Subject: convert to type branch of python export code --- (limited to 'TurtleArt/taprimitive.py') diff --git a/TurtleArt/taprimitive.py b/TurtleArt/taprimitive.py index d4fd5d5..112540a 100644 --- a/TurtleArt/taprimitive.py +++ b/TurtleArt/taprimitive.py @@ -20,14 +20,21 @@ import ast from gettext import gettext as _ +from math import sqrt +from random import uniform +import traceback #from ast_pprint import * # only used for debugging, safe to comment out +from tablock import Media from tacanvas import TurtleGraphics from taconstants import (Color, CONSTANTS) -from talogo import LogoCode +from talogo import (LogoCode, logoerror, NegativeRootError) from taturtle import (Turtle, Turtles) +from tatype import * +from tautils import debug_output from tawindow import (global_objects, TurtleArtWindow) +from util import ast_extensions class PyExportError(BaseException): @@ -48,19 +55,18 @@ class PyExportError(BaseException): class Primitive(object): - """ Something that can be called when the block code is executed in TA, + """ Something that can be called when the block code is executed in TA, but that can also be transformed into a Python AST. """ + _DEBUG = False + STANDARD_OPERATORS = {'plus': (ast.UAdd, ast.Add), 'minus': (ast.USub, ast.Sub), 'multiply': ast.Mult, 'divide': ast.Div, 'modulo': ast.Mod, 'power': ast.Pow, - 'integer_division': ast.FloorDiv, - 'bitwise_and': ast.BitAnd, - 'bitwise_or': ast.BitOr, 'and_': ast.And, 'or_': ast.Or, 'not_': ast.Not, @@ -68,261 +74,253 @@ class Primitive(object): 'less': ast.Lt, 'greater': ast.Gt} - def __init__(self, func, constant_args=None, slot_wrappers=None, - call_afterwards=None, call_me=True, export_me=True): - """ constant_args -- A dictionary containing constant arguments to be - passed to the function. It uses the same key scheme as - slot_wrappers, except that argument ranges are not supported. - The constant args and kwargs are added to the runtime args and - kwargs before the slot wrappers are called. - slot_wrappers -- A dictionary mapping from the index of an - argument in the args list to another Primitive that should be - wrapped around the actual argument value (e.g., to convert a - positive number to a negative one). For keyword arguments, the - key in slot_wrappers should be the same as the kwargs key. To pass - multiple arguments to the slot wrapper, use a tuple of the first - and last argument number (the latter increased by 1) as a key. - Negative argument indices are not supported. + def __init__(self, func, return_type=TYPE_OBJECT, arg_descs=None, kwarg_descs=None, + call_afterwards=None, export_me=True): + """ return_type -- the type (from the type hierarchy) that this + Primitive will return + arg_descs, kwarg_descs -- a list of argument descriptions and + a dictionary of keyword argument descriptions. An argument + description can be either an ArgSlot or a ConstantArg. call_afterwards -- Code to call after this Primitive has been called (e.g., for updating labels in LogoCode) (not used for creating AST) - call_me -- True if this Primitive should be called (default), False - if it should be passed on as a Primitive object export_me -- True iff this Primitive should be exported to Python code (the default case) """ self.func = func + self.return_type = return_type - if constant_args is None: - self.constant_args = {} + if arg_descs is None: + self.arg_descs = [] else: - self.constant_args = constant_args + self.arg_descs = arg_descs - if slot_wrappers is None: - self.slot_wrappers = {} + if kwarg_descs is None: + self.kwarg_descs = {} else: - # check for duplicate argument indices - msg = ("argument at index %d is associated with multiple slot " - "wrappers") - nums = set() - tuples = [] - for k in slot_wrappers.keys(): - if isinstance(k, int): - nums.add(k) - elif isinstance(k, tuple): - tuples.append(k) - tuples.sort() - prev_tuple = (0, 0) - for tuple_ in tuples: - if prev_tuple[1] > tuple_[0]: - raise KeyError(msg % (tuple_[0])) - for i in range(*tuple_): - if i in nums: - raise KeyError(msg % (i)) - prev_tuple = tuple_ - self.slot_wrappers = slot_wrappers + self.kwarg_descs = kwarg_descs self.call_afterwards = call_afterwards - self.call_me = call_me self.export_me = export_me + def copy(self): + """ Return a Primitive object with the same attributes as this one. + Shallow-copy the arg_descs and kwarg_descs attributes. """ + arg_descs_copy = self.arg_descs[:] + if isinstance(self.arg_descs, ArgListDisjunction): + arg_descs_copy = ArgListDisjunction(arg_descs_copy) + return Primitive(self.func, + return_type=self.return_type, + arg_descs=arg_descs_copy, + kwarg_descs=self.kwarg_descs.copy(), + call_afterwards=self.call_afterwards, + export_me=self.export_me) + def __repr__(self): - return "Primitive(" + repr(self.func) + ")" - - def _apply_wrappers(self, runtime_args, runtime_kwargs, - convert_to_ast=False): - """ Apply the slot wrappers """ - # make a map from the start indices of all ranges to their ends - range_ends = {} - for range_tuple in sorted(self.slot_wrappers.keys()): - if isinstance(range_tuple, tuple): - (start, end) = range_tuple - range_ends[start] = end + return "Primitive(%s -> %s)" % (repr(self.func), str(self.return_type)) - new_args = [] - i = 0 - while i < len(runtime_args): - arg = runtime_args[i] - wrapper = self.slot_wrappers.get(i) - if wrapper is None: - (start, end) = (i, range_ends.get(i)) - if end is None: - # no slot wrapper found - # convert to AST, but don't call - if convert_to_ast and isinstance(arg, Primitive): - new_args.append(arg.get_ast()) - else: - new_args.append(arg) - i += 1 - else: - # range -> slot wrapper around a range of arguments - wrapper = self.slot_wrappers.get((start, end)) - args_for_wrapper = runtime_args[start:end] - if not convert_to_ast and call_me(wrapper): - wrapper_output = wrapper(*args_for_wrapper) - elif convert_to_ast and export_me(wrapper): - wrapper_output = value_to_ast(wrapper, - *args_for_wrapper) + @property + def __name__(self): + return self.func.__name__ + + def get_name_for_export(self): + """ Return the expression (as a string) that represents this Primitive + in the exported Python code, e.g., 'turtle.forward'. """ + func_name = "" + if self.wants_turtle(): + func_name = "turtle." + elif self.wants_turtles(): + func_name = "turtles." + elif self.wants_canvas(): + func_name = "canvas." + elif self.wants_logocode(): + func_name = "logo." + elif self.wants_heap(): + func_name = "logo.heap." + elif self.wants_tawindow(): + func_name = "tw." + # get the name of the function directly from the function itself + func_name += self.func.__name__ + return func_name + + def are_slots_filled(self): + """ Return True iff none of the arg_descs or kwarg_descs is an + ArgSlot. """ + for arg_desc in self.arg_descs: + if isinstance(arg_desc, ArgSlot): + return False + for key in self.kwarg_descs: + if isinstance(self.kwarg_descs[key], ArgSlot): + return False + return True + + def fill_slots(self, arguments=None, keywords=None, convert_to_ast=False, + call_my_args=True): + """ Return a copy of this Primitive whose ArgSlots are filled with + the given arguments, turned into ConstantArgs. Call the arguments, + apply their wrappers, and check their types as appropriate. """ + if arguments is None: + arguments = [] + if keywords is None: + keywords = {} + + new_prim = self.copy() + + if isinstance(new_prim.arg_descs, ArgListDisjunction): + slot_list_alternatives = list(new_prim.arg_descs) + else: + slot_list_alternatives = [new_prim.arg_descs] + + # arguments + error = None + filler = None + for slot_list in slot_list_alternatives: + error = None + new_slot_list = [] + filler_list = list(arguments[:]) + for slot in slot_list: + if isinstance(slot, ArgSlot): + filler = filler_list.pop(0) + try: + const = slot.fill(filler, + convert_to_ast=convert_to_ast, + call_my_args=call_my_args) + except TATypeError as error: + if Primitive._DEBUG: + traceback.print_exc() + break else: - # apply all contained wrappers, but skip this one - (all_args, unused) = wrapper._add_constant_args( - args_for_wrapper, runtime_kwargs={}, - convert_to_ast=convert_to_ast) - (my_new_args, unused) = wrapper._apply_wrappers( - all_args, runtime_kwargs={}, - convert_to_ast=convert_to_ast) - wrapper_output = my_new_args - new_args.append(wrapper_output) - i += end - start - else: - # number -> slot wrapper around one argument - if not convert_to_ast and call_me(wrapper): - new_arg = wrapper(arg) - elif convert_to_ast and export_me(wrapper): - new_arg = value_to_ast(wrapper, arg) + new_slot_list.append(const) else: - # apply all contained wrappers, but skip this one - (all_args, unused) = wrapper._add_constant_args( - [arg], - runtime_kwargs={}, convert_to_ast=convert_to_ast) - (my_new_args, unused) = wrapper._apply_wrappers( - all_args, - runtime_kwargs={}, convert_to_ast=convert_to_ast) - new_arg = my_new_args[0] - new_args.append(new_arg) - i += 1 - + new_slot_list.append(slot) + if error is None: + new_prim.arg_descs = new_slot_list + break + if error is not None: + raise error + + # keyword arguments + for key in keywords: + kwarg_desc = new_prim.kwarg_descs[key] + if isinstance(kwarg_desc, ArgSlot): + const = kwarg_desc.fill(keywords[key], + convert_to_ast=convert_to_ast, + call_my_args=call_my_args) + new_prim.kwarg_descs[key] = const + + return new_prim + + def get_values_of_filled_slots(self, exportable_only=False): + """ Return the values of all filled argument slots as a list, and + the values of all filled keyword argument slots as a dictionary. + Ignore all un-filled (keyword) argument slots. + exportable_only -- return only exportable values and convert values + to ASTs instead of calling them """ + new_args = [] + for c_arg in self.arg_descs: + if (isinstance(c_arg, ConstantArg) + and (not exportable_only + or export_me(c_arg.value))): + new_args.append(c_arg.get(convert_to_ast=exportable_only)) new_kwargs = {} - for (key, value) in runtime_kwargs.iteritems(): - wrapper = self.slot_wrappers.get(key) - if wrapper is not None: - if not convert_to_ast and call_me(wrapper): - new_value = wrapper(value) - elif convert_to_ast and export_me(wrapper): - new_value = value_to_ast(wrapper, value) - else: - # apply all contained wrappers, but skip this one - (unused, all_kwargs) = wrapper._add_constant_args( - [], - runtime_kwargs={key: value}, - convert_to_ast=convert_to_ast) - (unused, my_new_kwargs) = wrapper._apply_wrappers( - [], - runtime_kwargs={key: all_kwargs[key]}, - convert_to_ast=convert_to_ast) - new_value = my_new_kwargs[key] - new_kwargs[key] = new_value - else: - new_kwargs[key] = value - + for key in self.kwarg_descs: + if (isinstance(self.kwarg_descs[key], ConstantArg) + and (not exportable_only + or export_me(self.kwarg_descs[key].value))): + new_kwargs[key] = self.kwarg_descs[key].get( + convert_to_ast=exportable_only) return (new_args, new_kwargs) - def _add_constant_args(self, runtime_args, runtime_kwargs, - convert_to_ast=False): - """ Add the constant args and kwargs to the given runtime args and - kwargs. Return a list containing all args and a dictionary with all - kwargs. - convert_to_ast -- convert all constant arguments to ASTs? """ - all_args = [] - all_kwargs = runtime_kwargs.copy() - - # args - i = 0 - - def _insert_c_args(i): - while i in self.constant_args: - c_arg = self.constant_args[i] - if not convert_to_ast and call_me(c_arg): - all_args.append(c_arg()) - elif convert_to_ast: - if export_me(c_arg): - all_args.append(value_to_ast(c_arg)) - else: - all_args.append(c_arg) - i += 1 - return i - for arg in runtime_args: - i = _insert_c_args(i) - all_args.append(arg) - i += 1 - i = _insert_c_args(i) - - # kwargs - for (key, value) in self.constant_args.iteritems(): - if isinstance(key, basestring): - if not convert_to_ast and call_me(value): - all_kwargs[key] = value() - elif convert_to_ast: - if export_me(value): - all_kwargs[key] = value_to_ast(value) - else: - all_kwargs[key] = value - - return (all_args, all_kwargs) + def allow_call_args(self, recursive=False): + """ Set call_args attribute of all argument descriptions to True + recursive -- recursively call allow_call_args on all constant args + that are Primitives """ + for arg_desc in self.arg_descs: + arg_desc.call_arg = True + if (recursive and isinstance(arg_desc, ConstantArg) and + isinstance(arg_desc.value, Primitive)): + arg_desc.value.allow_call_args(recursive=True) + for kwarg_desc in self.kwarg_descs: + kwarg_desc.call_arg = True + if (recursive and isinstance(kwarg_desc, ConstantArg) and + isinstance(kwarg_desc.value, Primitive)): + kwarg_desc.value.allow_call_args(recursive=True) def __call__(self, *runtime_args, **runtime_kwargs): - """ Execute the function, passing it the arguments received at + """ Execute the function, passing it the arguments received at runtime. Also call the function in self.call_afterwards and pass it all runtime_args and runtime_kwargs. - If the very first argument is a LogoCode instance, it may be - replaced with the active turtle, the canvas, or nothing (depending - on what this primitive wants as its first arg). This argument is - also exempt from the slot wrappers. """ + If the very first argument is a LogoCode instance, it is removed. + The active turtle, the Turtles object, the canvas, the LogoCode + object, or the TurtleArtWindow object will be prepended to the + arguments (depending on what this Primitive wants). """ # remove the first argument if it is a LogoCode instance if runtime_args and isinstance(runtime_args[0], LogoCode): runtime_args = runtime_args[1:] - runtime_args_copy = runtime_args[:] - runtime_args = [] - for arg in runtime_args_copy: - if isinstance(arg, tuple) and arg and callable(arg[0]): - runtime_args.append(arg[0](*arg[1:])) - else: - runtime_args.append(arg) + if Primitive._DEBUG: + debug_output(repr(self)) + debug_output(" runtime_args: " + repr(runtime_args)) + # fill the ArgSlots with the runtime arguments + new_prim = self.fill_slots(runtime_args, runtime_kwargs, + convert_to_ast=False) + if not new_prim.are_slots_filled(): + raise logoerror("#syntaxerror") + if Primitive._DEBUG: + debug_output(" new_prim.arg_descs: " + repr(new_prim.arg_descs)) + + # extract the actual values from the (now constant) arguments + (new_args, new_kwargs) = new_prim.get_values_of_filled_slots() + if Primitive._DEBUG: + debug_output(" new_args: " + repr(new_args)) + debug_output("end " + repr(self)) # what does this primitive want as its first argument? - if self.wants_turtle(): - first_arg = global_objects["turtles"].get_active_turtle() - elif self.wants_turtles(): - first_arg = global_objects["turtles"] - elif self.wants_canvas(): - first_arg = global_objects["canvas"] - elif self.wants_logocode(): - first_arg = global_objects["logo"] - elif self.wants_tawindow(): - first_arg = global_objects["window"] - else: - first_arg = None - - # constant arguments - (all_args, all_kwargs) = self._add_constant_args(runtime_args, - runtime_kwargs) - - # slot wrappers - (new_args, new_kwargs) = self._apply_wrappers(all_args, all_kwargs) + first_arg = None + if not is_bound_method(new_prim.func): + if new_prim.wants_turtle(): + first_arg = global_objects["turtles"].get_active_turtle() + elif new_prim.wants_turtles(): + first_arg = global_objects["turtles"] + elif new_prim.wants_canvas(): + first_arg = global_objects["canvas"] + elif new_prim.wants_logocode(): + first_arg = global_objects["logo"] + elif new_prim.wants_heap(): + first_arg = global_objects["logo"].heap + elif new_prim.wants_tawindow(): + first_arg = global_objects["window"] # execute the actual function - if first_arg is None or is_bound_instancemethod(self.func): - return_value = self.func(*new_args, **new_kwargs) + if first_arg is None: + return_value = new_prim.func(*new_args, **new_kwargs) else: - return_value = self.func(first_arg, *new_args, **new_kwargs) - - if self.call_afterwards is not None: - self.call_afterwards(*new_args, **new_kwargs) - + return_value = new_prim.func(first_arg, *new_args, **new_kwargs) + + if new_prim.call_afterwards is not None: + new_prim.call_afterwards(*new_args, **new_kwargs) + return return_value def get_ast(self, *arg_asts, **kwarg_asts): - """ Transform this object into a Python AST. When serialized and + """ Transform this object into a Python AST. When serialized and executed, the AST will do exactly the same as calling this object. """ - # constant arguments - (all_arg_asts, all_kwarg_asts) = self._add_constant_args( - arg_asts, kwarg_asts, convert_to_ast=True) - - # slot wrappers - (new_arg_asts, new_kwarg_asts) = self._apply_wrappers( - all_arg_asts, all_kwarg_asts, convert_to_ast=True) + if Primitive._DEBUG: + debug_output(repr(self)) + debug_output(" arg_asts: " + repr(arg_asts)) + new_prim = self.fill_slots(arg_asts, kwarg_asts, convert_to_ast=True) + if not new_prim.are_slots_filled(): + raise PyExportError("not enough arguments") + if Primitive._DEBUG: + debug_output(" new_prim.arg_descs: " + repr(new_prim.arg_descs)) + + # extract the actual values from the (now constant) arguments + (new_arg_asts, new_kwarg_asts) = new_prim.get_values_of_filled_slots( + exportable_only=True) + if Primitive._DEBUG: + debug_output(" new_arg_asts: " + repr(new_arg_asts)) + debug_output("end " + repr(self)) # SPECIAL HANDLING # @@ -347,11 +345,12 @@ class Primitive(object): elif controller == Primitive.controller_while: condition_ast = new_arg_asts[0].args[0] elif controller == Primitive.controller_until: - condition_ast = ast.UnaryOp( - op=ast.Not, operand=new_arg_asts[0].args[0]) + pos_cond_ast = new_arg_asts[0].args[0] + condition_ast = ast.UnaryOp(op=ast.Not, + operand=pos_cond_ast) else: - raise ValueError("unknown loop controller: " + - repr(controller)) + raise PyExportError("unknown loop controller: " + + repr(controller)) loop_ast = ast.While(test=condition_ast, body=new_arg_asts[1], orelse=[]) @@ -370,33 +369,41 @@ class Primitive(object): # boxes elif self == LogoCode.prim_set_box: - id_str = 'BOX[%s]' % (repr(ast_to_value(new_arg_asts[0]))) - target_ast = ast.Name(id=id_str, ctx=ast.Store) - value_ast = new_arg_asts[1] - assign_ast = ast.Assign(targets=[target_ast], value=value_ast) - return assign_ast + target_ast = ast.Subscript(value=BOX_AST, + slice=ast.Index(value=new_arg_asts[0]), ctx=ast.Store) + return ast.Assign(targets=[target_ast], value=new_arg_asts[1]) elif self == LogoCode.prim_get_box: - id_str = 'BOX[%s]' % (repr(ast_to_value(new_arg_asts[0]))) - return ast.Name(id=id_str, ctx=ast.Load) + return ast.Subscript(value=BOX_AST, + slice=ast.Index(value=new_arg_asts[0]), ctx=ast.Load) # action stacks elif self == LogoCode.prim_define_stack: return elif self == LogoCode.prim_invoke_stack: - stack_name = ast_to_value(new_arg_asts[0]) - stack_func_name = 'ACTION[%s]' % (repr(stack_name)) - stack_func = ast.Name(id=stack_func_name, ctx=ast.Load) - return get_call_ast('logo.icall', [stack_func]) + stack_func = ast.Subscript(value=ACTION_AST, + slice=ast.Index(value=new_arg_asts[0]), ctx=ast.Load) + call_ast = get_call_ast('logo.icall', [stack_func]) + return [call_ast, ast_yield_true()] + + # stop stack + elif self == LogoCode.prim_stop_stack: + return ast.Return() + + # sleep/ wait + elif self == LogoCode.prim_wait: + return [get_call_ast('sleep', new_arg_asts), ast_yield_true()] # standard operators elif self.func.__name__ in Primitive.STANDARD_OPERATORS: op = Primitive.STANDARD_OPERATORS[self.func.__name__] - # BEGIN hack for 'plus': unpack tuples - if (self == Primitive.plus and len(new_arg_asts) == 1 and - isinstance(new_arg_asts[0], (list, tuple)) and - len(new_arg_asts[0]) == 2): - new_arg_asts = new_arg_asts[0] - # END hack for 'plus' + # 'divide': prevent unwanted integer division + if self == Primitive.divide: + def _is_float(x): + return get_type(x)[0] == TYPE_FLOAT + if ( not _is_float(new_arg_asts[0]) and + not _is_float(new_arg_asts[1])): + new_arg_asts[0] = get_call_ast('float', [new_arg_asts[0]], + return_type=TYPE_FLOAT) if len(new_arg_asts) == 1: if isinstance(op, tuple): op = op[0] @@ -412,56 +419,82 @@ class Primitive(object): comparators=[right]) else: return ast.BinOp(op=op, left=left, right=right) - else: - raise ValueError(("operator Primitive.%s got unexpected" - " number of arguments (%d)") - % (str(self.func.__func__.__name__), - len(new_arg_asts))) - # type conversion - elif self in (Primitive.convert_for_cmp, Primitive.convert_to_number, - Primitive.convert_for_plus): - return self.func(*new_arg_asts, **new_kwarg_asts) + # f(x) + elif self == LogoCode.prim_myfunction: + param_asts = [] + for id_ in ['x', 'y', 'z'][:len(new_arg_asts)-1]: + param_asts.append(ast.Name(id=id_, ctx=ast.Param)) + func_ast = ast_extensions.LambdaWithStrBody( + body_str=new_arg_asts[0].s, args=param_asts) + return get_call_ast(func_ast, new_arg_asts[1:], + return_type=self.return_type) + + # square root + elif self == Primitive.square_root: + return get_call_ast('sqrt', new_arg_asts, new_kwarg_asts, + return_type=self.return_type) + + # random + elif self in (Primitive.random_char, Primitive.random_int): + uniform_ast = get_call_ast('uniform', new_arg_asts) + round_ast = get_call_ast('round', [uniform_ast, ast.Num(n=0)]) + int_ast = get_call_ast('int', [round_ast], return_type=TYPE_INT) + if self == Primitive.random_char: + chr_ast = get_call_ast('chr', [int_ast], return_type=TYPE_CHAR) + return chr_ast + else: + return int_ast # identity elif self == Primitive.identity: - if len(new_arg_asts) == 1: - return new_arg_asts[0] + return new_arg_asts[0] + + # constant + elif self == CONSTANTS.get: + return TypedSubscript(value=ast.Name(id='CONSTANTS', ctx=ast.Load), + slice_=ast.Index(value=new_arg_asts[0]), + return_type=self.return_type) + + # group of Primitives or sandwich-clamp block + elif self in (Primitive.group, LogoCode.prim_clamp): + ast_list = [] + for prim in new_arg_asts[0]: + if export_me(prim): + new_ast = value_to_ast(prim) + if isinstance(new_ast, ast.AST): + ast_list.append(new_ast) + return ast_list + + # comment + elif self == Primitive.comment: + if isinstance(new_arg_asts[0], ast.Str): + text = ' ' + str(new_arg_asts[0].s) else: - raise ValueError("Primitive.identity got unexpected number " - "of arguments (%d)" % (len(new_arg_asts))) - - # tuples - elif self == Primitive.make_tuple: - if not new_kwarg_asts: - return ast.Tuple(elts=new_arg_asts, ctx=ast.Load) - else: - raise ValueError("tuple constructor (Primitive.make_tuple) " - "got unexpected arguments: " + - repr(new_kwarg_asts)) - - # group of Primitives - elif self == Primitive.group: - return new_arg_asts[0].elts + text = ' ' + str(new_arg_asts[0]) + return ast_extensions.Comment(text) + + # print + elif self == TurtleArtWindow.print_: + func_name = self.get_name_for_export() + call_ast = get_call_ast(func_name, new_arg_asts) + print_ast = ast.Print(values=new_arg_asts[:1], dest=None, nl=True) + return [call_ast, print_ast] + + # heap + elif self == LogoCode.get_heap: + return TypedName(id_='logo.heap', return_type=self.return_type) + elif self == LogoCode.reset_heap: + target_ast = ast.Name(id='logo.heap', ctx=ast.Store) + value_ast = ast.List(elts=[], ctx=ast.Load) + return ast.Assign(targets=[target_ast], value=value_ast) # NORMAL FUNCTION CALL # else: - func_name = "" - if self.wants_turtle(): - func_name = "turtle." - elif self.wants_turtles(): - func_name = "turtles." - elif self.wants_canvas(): - func_name = "canvas." - elif self.wants_logocode(): - func_name = "logo." - elif self.wants_tawindow(): - func_name = "tw." - # get the name of the function directly from the function itself - func_name += self.func.__name__ - - return get_call_ast(func_name, new_arg_asts, new_kwarg_asts) + func_name = self.get_name_for_export() + return get_call_ast(func_name, new_arg_asts, new_kwarg_asts, + return_type=self.return_type) def __eq__(self, other): """ Two Primitives are equal iff their all their properties are equal. @@ -469,8 +502,9 @@ class Primitive(object): # other is a Primitive if isinstance(other, Primitive): return (self == other.func and - self.constant_args == other.constant_args and - self.slot_wrappers == other.slot_wrappers and + self.return_type == other.return_type and + self.arg_descs == other.arg_descs and + self.kwarg_descs == other.kwarg_descs and self.call_afterwards == other.call_afterwards and self.export_me == other.export_me) @@ -478,7 +512,7 @@ class Primitive(object): elif callable(other): if is_instancemethod(self.func) != is_instancemethod(other): return False - elif is_instancemethod(self.func): # and is_instancemethod(other) + elif is_instancemethod(self.func): # and is_instancemethod(other): return (self.func.im_class == other.im_class and self.func.im_func == other.im_func) else: @@ -492,7 +526,7 @@ class Primitive(object): return False def wants_turtle(self): - """ Does this Primitive want to get the active turtle as its first + """ Does this Primitive want to get the active turtle as its first argument? """ return self._wants(Turtle) @@ -509,7 +543,13 @@ class Primitive(object): def wants_logocode(self): """ Does this Primitive want to get the LogoCode instance as its first argument? """ - return self._wants(LogoCode) + return (self.func.__name__ == '' or self._wants(LogoCode)) + + def wants_heap(self): + """ Does this Primitive want to get the heap as its first argument? """ + return ((hasattr(self.func, '__self__') and + isinstance(self.func.__self__, list)) or + self.func in list.__dict__.values()) def wants_tawindow(self): """ Does this Primitive want to get the TurtleArtWindow instance @@ -517,27 +557,18 @@ class Primitive(object): return self._wants(TurtleArtWindow) def wants_nothing(self): - """ Does this Primitive want nothing as its first argument? I.e. does - it want to be passed all the arguments of the block and nothing + """ Does this Primitive want nothing as its first argument? I.e. does + it want to be passed all the arguments of the block and nothing else? """ return not is_instancemethod(self.func) def _wants(self, theClass): - if is_instancemethod(self.func): - return self.func.im_class == theClass - else: - return False + return is_instancemethod(self.func) and self.func.im_class == theClass # treat the following methods in a special way when converting the # Primitive to an AST @staticmethod - def make_tuple(*values): - """ This method corresponds to a Python tuple consisting of the given - values. """ - return tuple(values) - - @staticmethod def controller_repeat(num): """ Loop controller for the 'repeat' block """ for i in range(num): @@ -551,16 +582,22 @@ class Primitive(object): yield True @staticmethod - def controller_while(boolean): - """ Loop controller for the 'while' block """ - while boolean: + def controller_while(condition): + """ Loop controller for the 'while' block + condition -- Primitive that is evaluated every time through the + loop """ + condition.allow_call_args(recursive=True) + while condition(): yield True yield False @staticmethod - def controller_until(boolean): - """ Loop controller for the 'until' block """ - while not boolean: + def controller_until(condition): + """ Loop controller for the 'until' block + condition -- Primitive that is evaluated every time through the + loop """ + condition.allow_call_args(recursive=True) + while not condition(): yield True yield False @@ -574,18 +611,18 @@ class Primitive(object): return (callable(candidate) and candidate in Primitive.LOOP_CONTROLLERS) - # look at the first constant argument - first_const = self.constant_args.get(0, None) - if _is_loop_controller(first_const): - return first_const - - # look at the first slot wrapper - first_wrapper = self.slot_wrappers.get(0, None) - if _is_loop_controller(first_wrapper): - return first_wrapper + for desc in self.arg_descs: + if isinstance(desc, ConstantArg): + value = desc.value + if _is_loop_controller(value): + return value + elif isinstance(desc, ArgSlot): + wrapper = desc.wrapper + if _is_loop_controller(wrapper): + return wrapper # no controller found - raise ValueError("found no loop controller for " + repr(self)) + raise PyExportError("found no loop controller for " + repr(self)) @staticmethod def do_nothing(): @@ -606,67 +643,6 @@ class Primitive(object): return return_val @staticmethod - def convert_for_plus(value1, value2): - """ If at least one value is a string, convert both to a string. - Otherwise, convert both to a number. (Colors are converted to an - integer before they are converted to a string.) """ - convert_to_ast = False - (value1_ast, value2_ast) = (None, None) - - if isinstance(value1, ast.AST): - convert_to_ast = True - value1_ast = value1 - value1 = ast_to_value(value1_ast) - if isinstance(value2, ast.AST): - value2_ast = value2 - value2 = ast_to_value(value2_ast) - - def _to_string(val, val_ast): - """ Return strings as they are, convert Colors to an integer and - then to a string, and convert everything else directly to a - string. """ - val_conv = val - val_conv_ast = val_ast - if not isinstance(val, basestring): - if isinstance(val, Color): - conv_prim = Primitive(str, slot_wrappers={ - 0: Primitive(int)}) - else: - conv_prim = Primitive(str) - if not convert_to_ast: - val_conv = conv_prim(val) - else: - val_conv_ast = conv_prim.get_ast(val_ast) - return (val_conv, val_conv_ast) - - def _to_number(val, val_ast): - """ Return numbers as they are, and convert everything else to an - integer. """ - val_conv = val - val_conv_ast = val_ast - if not isinstance(val, (float, int, long)): - conv_prim = Primitive(int) - if not convert_to_ast: - val_conv = conv_prim(val) - else: - val_conv_ast = conv_prim.get_ast(val_ast) - return (val_conv, val_conv_ast) - - if isinstance(value1, basestring) or isinstance(value2, basestring): - # convert both to strings - (value1_conv, value1_conv_ast) = _to_string(value1, value1_ast) - (value2_conv, value2_conv_ast) = _to_string(value2, value2_ast) - else: - # convert both to numbers - (value1_conv, value1_conv_ast) = _to_number(value1, value1_ast) - (value2_conv, value2_conv_ast) = _to_number(value2, value2_ast) - - if convert_to_ast: - return (value1_conv_ast, value2_conv_ast) - else: - return (value1_conv, value2_conv) - - @staticmethod def plus(arg1, arg2=None): """ If only one argument is given, prefix it with '+'. If two arguments are given, add the second to the first. If the first @@ -680,77 +656,6 @@ class Primitive(object): return arg1 + arg2 @staticmethod - def convert_to_number(value, decimal_point='.'): - """ Convert value to a number. If value is an AST, another AST is - wrapped around it to represent the conversion, e.g., - Str(s='1.2') -> Call(func=Name('float'), args=[Str(s='1.2')]) - 1. Return all numbers (float, int, long) unchanged. - 2. Convert a string containing a number into a float. - 3. Convert a single character to its ASCII integer value. - 4. Extract the first element of a list and convert it to a number. - 5. Convert a Color to a float. - If the value cannot be converted to a number and the value is not - an AST, return None. If it is an AST, return an AST representing - `float(value)'. """ # TODO find a better solution - # 1. number - if isinstance(value, (float, int, long, ast.Num)): - return value - - converted = None - conversion_ast = None - convert_to_ast = False - if isinstance(value, ast.AST): - convert_to_ast = True - value_ast = value - value = ast_to_value(value_ast) - if isinstance(decimal_point, ast.AST): - decimal_point = ast_to_value(decimal_point) - - # 2./3. string - if isinstance(value, basestring): - if convert_to_ast: - conversion_ast = Primitive.convert_for_cmp(value_ast, - decimal_point) - if not isinstance(conversion_ast, ast.Num): - converted = None - else: - converted = Primitive.convert_for_cmp(value, decimal_point) - if not isinstance(converted, (float, int, long)): - converted = None - # 4. list - elif isinstance(value, list): - if value: - number = Primitive.convert_to_number(value[0]) - if convert_to_ast: - conversion_ast = number - else: - converted = number - else: - converted = None - if convert_to_ast: - conversion_ast = get_call_ast('float', [value_ast]) - # 5. Color - elif isinstance(value, Color): - converted = float(value) - if convert_to_ast: - conversion_ast = get_call_ast('float', [value_ast]) - else: - converted = None - if convert_to_ast: - conversion_ast = get_call_ast('float', [value_ast]) - - if convert_to_ast: - if conversion_ast is None: - return value_ast - else: - return conversion_ast - else: - if converted is None: - return value - else: - return converted - - @staticmethod def minus(arg1, arg2=None): """ If only one argument is given, change its sign. If two arguments are given, subtract the second from the first. """ @@ -767,7 +672,7 @@ class Primitive(object): @staticmethod def divide(arg1, arg2): """ Divide the first argument by the second """ - return arg1 / arg2 + return float(arg1) / arg2 @staticmethod def modulo(arg1, arg2): @@ -782,20 +687,12 @@ class Primitive(object): return arg1 ** arg2 @staticmethod - def integer_division(arg1, arg2): - """ Divide the first argument by the second and return the integer - that is smaller than or equal to the result """ - return arg1 // arg2 - - @staticmethod - def bitwise_and(arg1, arg2): - """ Return the bitwise AND of the two arguments """ - return arg1 & arg2 - - @staticmethod - def bitwise_or(arg1, arg2): - """ Return the bitwise OR of the two arguments """ - return arg1 | arg2 + def square_root(arg1): + """ Return the square root of the argument. If it is a negative + number, raise a NegativeRootError. """ + if arg1 < 0: + raise NegativeRootError(neg_value=arg1) + return sqrt(arg1) @staticmethod def and_(arg1, arg2): @@ -814,59 +711,6 @@ class Primitive(object): return not arg @staticmethod - def convert_for_cmp(value, decimal_point='.'): - """ Convert value such that it can be compared to something else. If - value is an AST, another AST is wrapped around it to represent the - conversion, e.g., - Str(s='a') -> Call(func=Name('ord'), args=[Str(s='a')]) - 1. Convert a string containing a number into a float. - 2. Convert a single character to its ASCII integer value. - 3. Return all other values unchanged. """ - converted = None - conversion_ast = None - convert_to_ast = False - if isinstance(value, ast.AST): - convert_to_ast = True - value_ast = value - value = ast_to_value(value_ast) - if isinstance(decimal_point, ast.AST): - decimal_point = ast_to_value(decimal_point) - - if isinstance(value, basestring): - # 1. string containing a number - replaced = value.replace(decimal_point, '.') - try: - converted = float(replaced) - except ValueError: - pass - else: - if convert_to_ast: - conversion_ast = get_call_ast('float', [value_ast]) - - # 2. single character - if converted is None: - try: - converted = ord(value) - except TypeError: - pass - else: - if convert_to_ast: - conversion_ast = get_call_ast('ord', [value_ast]) - - # 3. normal string or other type of value (nothing to do) - - if convert_to_ast: - if conversion_ast is None: - return value_ast - else: - return conversion_ast - else: - if converted is None: - return value - else: - return converted - - @staticmethod def equals(arg1, arg2): """ Return arg1 == arg2 """ return arg1 == arg2 @@ -881,23 +725,339 @@ class Primitive(object): """ Return arg1 > arg2 """ return arg1 > arg2 + @staticmethod + def comment(text): + """ In 'snail' execution mode, display the comment. Else, do nothing. """ + tw = global_objects["window"] + if not tw.hide and tw.step_time != 0: + tw.showlabel('print', text) + + @staticmethod + def random_int(lower, upper): + """ Choose a random integer between lower and upper, which must be + integers """ + return int(round(uniform(lower, upper), 0)) + + @staticmethod + def random_char(lower, upper): + """ Choose a random Unicode code point between lower and upper, + which must be integers """ + return chr(Primitive.random_int(lower, upper)) + + +class Disjunction(tuple): + """ Abstract disjunction class (not to be instantiated directly) """ + + def __init__(self, iterable): + self = tuple(iterable) + + def __repr__(self): + s = ["("] + for disj in self: + s.append(repr(disj)) + s.append(" or ") + s.pop() + s.append(")") + return "".join(s) + + def get_alternatives(self): + """ Return a tuple of alternatives, i.e. self """ + return self + + +class PrimitiveDisjunction(Disjunction,Primitive): + """ Disjunction of two or more Primitives. PrimitiveDisjunctions may not + be nested. """ + + @property + def return_type(self): + """ Tuple of the return_types of all disjuncts """ + return TypeDisjunction((prim.return_type for prim in self)) -def is_instancemethod(method): - # TODO how to access the type `instancemethod` directly? - return type(method).__name__ == "instancemethod" + def __call__(self, *runtime_args, **runtime_kwargs): + """ Loop over the disjunct Primitives and try to fill their slots + with the given args and kwargs. Call the first Primitives whose + slots could be filled successfully. If all disjunct Primitives + fail, raise the last error that occurred. """ + + # remove the first argument if it is a LogoCode instance + if runtime_args and isinstance(runtime_args[0], LogoCode): + runtime_args = runtime_args[1:] + + error = None + for prim in self: + try: + new_prim = prim.fill_slots(runtime_args, runtime_kwargs, + convert_to_ast=False) + except TATypeError as error: + # on failure, try the next one + continue + else: + # on success, call this Primitive + return new_prim() + + # if we get here, all disjuncts failed + if error is not None: + raise error + + +class ArgListDisjunction(Disjunction): + """ Disjunction of two or more argument lists """ + pass + + +class ArgSlot(object): + """ Description of the requirements that a Primitive demands from an + argument or keyword argument. An ArgSlot is filled at runtime, based + on the block program structure. """ + + def __init__(self, type_, call_arg=True, wrapper=None): + """ + type_ -- what type of the type hierarchy the argument should have + (after the wrapper has been applied) + call_arg -- if this argument is callable, should it be called and + its return value passed to the parent Primitive (True, the + default), or should it be passed as it is (False)? + wrapper -- a Primitive that is 'wrapped around' the argument before + it gets passed to its parent Primitive. Wrappers can be nested + infinitely. """ + self.type = type_ + self.call_arg = call_arg + self.wrapper = wrapper + + def __repr__(self): + s = ["ArgSlot(type="] + s.append(repr(self.type)) + if not self.call_arg: + s.append(", call=") + s.append(repr(self.call_arg)) + if self.wrapper is not None: + s.append(", wrapper=") + s.append(repr(self.wrapper)) + s.append(")") + return "".join(s) + + def get_alternatives(self): + """ Return a tuple of slot alternatives, i.e. (self, ) """ + return (self, ) + + def fill(self, argument, convert_to_ast=False, call_my_args=True): + """ Try to fill this argument slot with the given argument. Return + a ConstantArg containing the result. If there is a type problem, + raise a TATypeError. """ + if isinstance(argument, ast.AST): + convert_to_ast = True + + # 1. can the argument be called? + (func_disjunction, args) = (None, []) + if (isinstance(argument, tuple) and argument + and callable(argument[0])): + func_disjunction = argument[0] + if len(argument) >= 2 and isinstance(argument[1], LogoCode): + args = argument[2:] + else: + args = argument[1:] + elif callable(argument): + func_disjunction = argument + + # make sure we can loop over func_disjunction + if not isinstance(func_disjunction, PrimitiveDisjunction): + func_disjunction = PrimitiveDisjunction((func_disjunction, )) + + error = None + bad_value = argument # the value that caused the TATypeError + for func in func_disjunction: + error = None + for slot in self.get_alternatives(): + + if isinstance(slot.wrapper, PrimitiveDisjunction): + wrapper_disjunction = slot.wrapper + else: + wrapper_disjunction = PrimitiveDisjunction((slot.wrapper,)) + + for wrapper in wrapper_disjunction: + + # check if the argument can fill this slot (type-wise) + # (lambda functions are always accepted) + if getattr(func, '__name__', None) == '': + converter = identity + old_type = TYPE_OBJECT + new_type = slot.type + else: + if wrapper is not None: + arg_types = get_type(wrapper)[0] + bad_value = wrapper + elif func is not None: + arg_types = get_type(func)[0] + bad_value = func + else: + arg_types = get_type(argument)[0] + bad_value = argument + converter = None + if not isinstance(arg_types, TypeDisjunction): + arg_types = TypeDisjunction((arg_types, )) + if isinstance(slot.type, TypeDisjunction): + slot_types = slot.type + else: + slot_types = TypeDisjunction((slot.type, )) + for old_type in arg_types: + for new_type in slot_types: + converter = get_converter(old_type, new_type) + if converter is not None: + break + if converter is not None: + break + # unable to convert, try next wrapper/ slot/ func + if converter is None: + continue + + # 1. (cont'd) call the argument or pass it on as a callable + called_argument = argument + if func is not None: + func_prim = func + if not isinstance(func_prim, Primitive): + func_prim = Primitive(func_prim, + [ArgSlot(TYPE_OBJECT)] * len(args)) + try: + func_prim = func_prim.fill_slots(args, + convert_to_ast=convert_to_ast, + call_my_args=(slot.call_arg and call_my_args)) + except TATypeError as error: + if Primitive._DEBUG: + traceback.print_exc() + # on failure, try next wrapper/ slot/ func + bad_value = error.bad_value + continue + if convert_to_ast: + called_argument = func_prim.get_ast() + else: + if slot.call_arg and call_my_args: + # call and pass on the return value + called_argument = func_prim() + else: + # don't call and pass on the callable + called_argument = func_prim + + # 2. apply any wrappers + wrapped_argument = called_argument + if wrapper is not None: + if convert_to_ast: + if not hasattr(wrapper, "get_ast"): + raise PyExportError(("cannot convert callable" + " %s to an AST") % (repr(wrapper))) + wrapped_argument = wrapper.get_ast( + called_argument) + else: + if slot.call_arg and call_my_args: + wrapped_argument = wrapper(called_argument) + else: + wrapped_argument = wrapper.fill_slots( + [called_argument], call_my_args=False) + + # last chance to convert raw values to ASTs + # (but not lists of ASTs) + if (convert_to_ast and + not isinstance(wrapped_argument, ast.AST) and + not (isinstance(wrapped_argument, list) and + wrapped_argument and + isinstance(wrapped_argument[0], ast.AST))): + wrapped_argument = value_to_ast(wrapped_argument) + + # 3. check the type and convert the argument if necessary + converted_argument = wrapped_argument + if slot.call_arg and call_my_args: + try: + converted_argument = convert(wrapped_argument, + new_type, old_type=old_type, + converter=converter) + except TATypeError as error: + if Primitive._DEBUG: + traceback.print_exc() + # on failure, try next wrapper/ slot/ func + bad_value = wrapped_argument + continue + elif converter != identity: + converted_argument = Primitive(converter, + return_type=new_type, + arg_descs=[ConstantArg(wrapped_argument, + value_type=old_type, call_arg=False)]) + # on success, return the result + return ConstantArg(converted_argument, + value_type=new_type, + call_arg=(slot.call_arg and call_my_args)) + + # if we haven't returned anything yet, then all alternatives failed + if error is not None: + raise error + else: + raise TATypeError(bad_value=bad_value, bad_type=old_type, + req_type=new_type) -def is_bound_instancemethod(method): - return is_instancemethod(method) and method.im_self is not None +class ArgSlotDisjunction(Disjunction,ArgSlot): + """ Disjunction of two or more argument slots """ + pass -def is_unbound_instancemethod(method): - return is_instancemethod(method) and method.im_self is None +class ConstantArg(object): + """ A constant argument or keyword argument to a Primitive. It is + independent of the block program structure. """ + def __init__(self, value, call_arg=True, value_type=None): + """ call_arg -- call the value before returning it? + value_type -- the type of the value (from the TA type system). This + is useful to store e.g., the return type of call ASTs. """ + self.value = value + self.call_arg = call_arg + self.value_type = value_type -def is_staticmethod(method): - # TODO how to access the type `staticmethod` directly? - return type(method).__name__ == "staticmethod" + def get(self, convert_to_ast=False): + """ If call_arg is True and the value is callable, call the value + and return its return value. Else, return the value unchanged. + convert_to_ast -- return the equivalent AST instead of a raw value """ + if self.call_arg and callable(self.value): + if convert_to_ast: + return value_to_ast(self.value) + else: + return self.value() + else: + if convert_to_ast and not isinstance(self.value, list): + return value_to_ast(self.value) + else: + return self.value + + def get_value_type(self): + """ If this ConstantArg has stored the type of its value, return + that. Else, use get_type(...) to guess the type of the value. """ + if self.value_type is None: + return get_type(self.value)[0] + else: + return self.value_type + + def __repr__(self): + s = ["ConstantArg("] + s.append(repr(self.value)) + if not self.call_arg: + s.append(", call=") + s.append(repr(self.call_arg)) + s.append(")") + return "".join(s) + + +def or_(*disjuncts): + """ Return a disjunction object of the same type as the disjuncts. If + the item type cannot be linked to a Disjunction class, return a tuple + of the disjuncts. """ + if isinstance(disjuncts[0], Primitive): + return PrimitiveDisjunction(disjuncts) + elif isinstance(disjuncts[0], (list, ArgListDisjunction)): + return ArgListDisjunction(disjuncts) + elif isinstance(disjuncts[0], ArgSlot): + return ArgSlotDisjunction(disjuncts) + elif isinstance(disjuncts[0], Type): + return TypeDisjunction(disjuncts) + else: + return tuple(disjuncts) def value_to_ast(value, *args_for_prim, **kwargs_for_prim): @@ -905,20 +1065,25 @@ def value_to_ast(value, *args_for_prim, **kwargs_for_prim): bool, basestring, list If the value is already an AST, return it unchanged. If the value is a non-exportable Primitive, return None. """ - # TODO media + # already an AST if isinstance(value, ast.AST): return value + # Primitive elif isinstance(value, Primitive): if value.export_me: return value.get_ast(*args_for_prim, **kwargs_for_prim) else: return None + # boolean elif isinstance(value, bool): return ast.Name(id=str(value), ctx=ast.Load) + # number elif isinstance(value, (int, float)): return ast.Num(n=value) + # string elif isinstance(value, basestring): return ast.Str(value) + # list (recursively transform to an AST) elif isinstance(value, list): ast_list = [] for item in value: @@ -926,54 +1091,24 @@ def value_to_ast(value, *args_for_prim, **kwargs_for_prim): if item_ast is not None: ast_list.append(item_ast) return ast.List(elts=ast_list, ctx=ast.Load) + # color elif isinstance(value, Color): - if str(value) in CONSTANTS: - # repr(str(value)) is necessary; it first converts the Color to a - # string and then adds appropriate quotes around that string - return ast.Name(id='CONSTANTS[%s]' % repr(str(value)), - ctx=ast.Load) - else: - # call to the Color constructor with this object's values, - # e.g., Color('red', 0, 50, 100) - return get_call_ast('Color', [value.name, value.color, - value.shade, value.gray]) - else: - raise ValueError("unknown type of raw value: " + repr(type(value))) - - -def ast_to_value(ast_object): - """ Retrieve the value out of a value AST. Supported AST types: - Num, Str, Name, List, Tuple, Set - If no value can be extracted, return None. """ - if isinstance(ast_object, ast.Num): - return ast_object.n - elif isinstance(ast_object, ast.Str): - return ast_object.s - elif isinstance(ast_object, (ast.List, ast.Tuple, ast.Set)): - return ast_object.elts - elif (isinstance(ast_object, ast.Name)): - try: - return eval(ast_object.id) - except NameError: - return None + # call to the Color constructor with this object's values, + # e.g., Color('red', 0, 50, 100) + return get_call_ast('Color', [value.name, value.color, + value.shade, value.gray], + return_type=TYPE_COLOR) + # media + elif isinstance(value, Media): + args = [value_to_ast(value.type), value_to_ast(value.value)] + return get_call_ast('Media', args, return_type=TYPE_MEDIA) + # unknown else: - return None + raise PyExportError("unknown type of raw value: " + repr(type(value))) -def get_call_ast(func_name, args=[], keywords={}): - return ast.Call(func=ast.Name(id=func_name, - ctx=ast.Load), - args=args, - keywords=keywords, - starargs=None, - kwargs=None) - - -def call_me(something): - """ Return True iff this is a Primitive and its call_me attribute is - True, i.e. nothing is callable except for Primitives with - call_me == True """ - return isinstance(something, Primitive) and something.call_me +def ast_yield_true(): + return ast.Yield(value=ast.Name(id='True', ctx=ast.Load)) def export_me(something): @@ -981,3 +1116,5 @@ def export_me(something): is True, i.e. everything is exportable except for Primitives with export_me == False """ return not isinstance(something, Primitive) or something.export_me + + -- cgit v0.9.1