diff --git a/joy/library.py b/joy/library.py index 90139cc..4b62c71 100644 --- a/joy/library.py +++ b/joy/library.py @@ -24,10 +24,12 @@ returns a dictionary of Joy functions suitable for use with the joy() function. ''' from inspect import getdoc +from functools import wraps import operator, math from .parser import text_to_expression, Symbol from .utils.stack import list_to_stack, iter_stack, pick, pushback +from .utils.brutal_hackery import rename_code_object _dictionary = {} @@ -152,61 +154,55 @@ step_zero == 0 roll> step ) -class FunctionWrapper(object): - ''' - Allow functions to have a nice repr(). - - At some point it's likely this class and its subclasses would gain - machinery to support type checking and inference. - ''' - - def __init__(self, f): - self.f = f - self.name = f.__name__.rstrip('_') # Don't shadow builtins. - self.__doc__ = f.__doc__ or str(f) - - def __call__(self, stack, expression, dictionary): - ''' - Functions in general receive and return all three. - ''' - return self.f(stack, expression, dictionary) - - def __repr__(self): - return self.name +def FunctionWrapper(f): + '''Set name attribute.''' + if not f.__doc__: + raise ValueError('Function %s must have doc string.' % f.__name__) + f.name = f.__name__.rstrip('_') # Don't shadow builtins. + return f -class SimpleFunctionWrapper(FunctionWrapper): +def SimpleFunctionWrapper(f): ''' Wrap functions that take and return just a stack. ''' - - def __call__(self, stack, expression, dictionary): - return self.f(stack), expression, dictionary + @FunctionWrapper + @wraps(f) + @rename_code_object(f.__name__) + def inner(stack, expression, dictionary): + return f(stack), expression, dictionary + return inner -class BinaryBuiltinWrapper(FunctionWrapper): +def BinaryBuiltinWrapper(f): ''' Wrap functions that take two arguments and return a single result. ''' - - def __call__(self, stack, expression, dictionary): + @FunctionWrapper + @wraps(f) + @rename_code_object(f.__name__) + def inner(stack, expression, dictionary): (a, (b, stack)) = stack - result = self.f(b, a) + result = f(b, a) return (result, stack), expression, dictionary + return inner -class UnaryBuiltinWrapper(FunctionWrapper): +def UnaryBuiltinWrapper(f): ''' Wrap functions that take one argument and return a single result. ''' - - def __call__(self, stack, expression, dictionary): + @FunctionWrapper + @wraps(f) + @rename_code_object(f.__name__) + def inner(stack, expression, dictionary): (a, stack) = stack - result = self.f(a) + result = f(a) return (result, stack), expression, dictionary + return inner -class DefinitionWrapper(FunctionWrapper): +class DefinitionWrapper(object): ''' Provide implementation of defined functions, and some helper methods. ''' @@ -692,12 +688,15 @@ floor.__doc__ = math.floor.__doc__ @inscribe @SimpleFunctionWrapper def divmod_(S): + ''' + divmod(x, y) -> (quotient, remainder) + + Return the tuple (x//y, x%y). Invariant: div*y + mod == x. + ''' a, (b, stack) = S d, m = divmod(a, b) return d, (m, stack) -divmod_.__doc__ = divmod.__doc__ - def sqrt(a): ''' @@ -738,12 +737,14 @@ def rolldown(S): @inscribe @SimpleFunctionWrapper def id_(stack): + '''The identity function.''' return stack @inscribe @SimpleFunctionWrapper def void(stack): + '''True if the form on TOS is void otherwise False.''' form, stack = stack return _void(form), stack diff --git a/joy/utils/brutal_hackery.py b/joy/utils/brutal_hackery.py new file mode 100644 index 0000000..0854691 --- /dev/null +++ b/joy/utils/brutal_hackery.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2018 Simon Forman +# +# This file is part of Joypy +# +# Joypy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Joypy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Joypy. If not see . +# +''' +I really want tracebacks to show which function was being executed when +an error in the wrapper function happens. In order to do that, you have +to do this (the function in this module.) + +Here's what it looks like when you pass too few arguments to e.g. "mul". + + >>> from joy.library import _dictionary + >>> m = _dictionary['*'] + >>> m((), (), {}) + + Traceback (most recent call last): + File "", line 1, in + m((), (), {}) + File "joy/library.py", line 185, in mul:inner + (a, (b, stack)) = stack + ValueError: need more than 0 values to unpack + >>> + + +Notice that line 185 in the library.py file is (as of this writing) in +the BinaryBuiltinWrapper's inner() function, but this hacky code has +managed to insert the name of the wrapped function ("mul") along with a +colon into the wrapper function's reported name. + +Normally I would frown on this sort of mad hackery, but... this is in +the service of ease-of-debugging! Very valuable. And note that all the +hideous patching is finished in the module-load-stage, it shouldn't cause +issues of its own at runtime. + +The main problem I see with this is that people coming to this code later +might be mystified if they just see a traceback with a ':' in the +function name! Hopefully they will discover this documentation. +''' + + +def rename_code_object(new_name): + ''' + If you want to wrap a function in another function and have the wrapped + function's name show up in the traceback, you must do this brutal + hackery to change the func.__code__.co_name attribute. See: + + https://stackoverflow.com/questions/29919804/function-decorated-using-functools-wraps-raises-typeerror-with-the-name-of-the-w + + https://stackoverflow.com/questions/29488327/changing-the-name-of-a-generator/29488561#29488561 + + I'm just glad it's possible. + ''' + def inner(func): + name = new_name + ':' + func.__name__ + code_object = func.__code__ + return type(func)( + type(code_object)( + code_object.co_argcount, + code_object.co_nlocals, + code_object.co_stacksize, + code_object.co_flags, + code_object.co_code, + code_object.co_consts, + code_object.co_names, + code_object.co_varnames, + code_object.co_filename, + name, + code_object.co_firstlineno, + code_object.co_lnotab, + code_object.co_freevars, + code_object.co_cellvars + ), + func.__globals__, + name, + func.__defaults__, + func.__closure__ + ) + return inner