### # Copyright (c) 2013, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import ast import copy import operator import fractions import functools import collections import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Scheme') NUMBER_TYPES = ( ('integer', int), ('rational', fractions.Fraction), ('real', float), ('complex', complex), ('number', str), ) class SchemeException(Exception): pass def no_side_effect(f): def newf(tree, env): return f(tree, env.copy()) return newf def eval_argument(arg, env): if isinstance(arg, list): return eval_scheme(arg, env) elif isinstance(arg, str): if arg in env: return eval_argument(env[arg], {}) else: for name, parser in NUMBER_TYPES: try: return parser(arg) except ValueError: pass # You shall not pass raise SchemeException(_('Unbound variable: %s') % arg) else: return arg def py2scheme(tree): if isinstance(tree, list): return '(%s)' % (' '.join(map(py2scheme, tree))) else: return str(tree) def schemify_math(f): # Makes a two-arguments function an *args function, with correct # type parsing. def rec(args): if args[2:]: return f(args[0], rec(args[1:])) else: return f(args[0], args[1]) def newf(tree, env): return rec(map(functools.partial(eval_argument, env=env), tree[1:])) newf.__name__ = 'schemified_%s' % f.__name__ return newf ARGUMENTS_ERROR = _('%s takes %s %i arguments not %i (in (%s))') @no_side_effect def scm_lambda(tree, env): try: self, args, expr = tree except ValueError: raise SchemeException(ARGUMENTS_ERROR % ('lambda', _('exactly'), 2, len(tree)-1, py2scheme(tree))) if not isinstance(args, list): args = ['.', args] try: if args.index('.') != len(args)-2: raise SchemeException(_('Invalid arguments list: %s') % py2scheme(args)) rest = args[-1] args = args[0:-2] except ValueError: # No rest rest = None @no_side_effect def f(tree2, env2): self2, args2 = tree2[0], tree2[1:] arguments_error = ARGUMENTS_ERROR % \ (self2, '%s', len(args), len(args2), tree2) env3 = env2.copy() if len(args2) < len(args): raise SchemeException(arguments_error % _('at least') if rest else _('exactly')) elif not rest and len(args2) > len(args): raise SchemeException(arguments_error % _('exactly')) else: env3.update(dict(zip(args, args2))) if rest: env3.update({rest: args2[len(args):]}) return eval_scheme(expr, env3) f.__name__ = 'scheme_%s' % py2scheme(tree) return f def scm_begin(tree, env): for arg in tree[1:-1]: eval_scheme(arg) return eval_scheme(tree[-1]) def scm_set(tree, env): try: self, name, value = tree except ValueError: raise SchemeException(ARGUMENTS_ERROR % ('set!', _('exactly'), 2, len(tree)-1, py2scheme(tree))) env[name] = value DEFAULT_ENV = [ ('lambda', scm_lambda), ('begin', scm_begin), ('set!', scm_set), ] # Add some math operators DEFAULT_ENV += map(lambda x:(x[0], schemify_math(x[1])), ( ('+', operator.add), ('-', operator.sub), ('*', operator.mul), ('/', operator.truediv), )) DEFAULT_ENV = dict(DEFAULT_ENV) def parse_scheme(code, start=0, end=None, unpack=False): if end is None: end = len(code)-1 while code[start] == ' ': start += 1 while code[end] == ' ': end -= 1 if code[start] == '(' and code[end] == ')': return parse_scheme(code, start+1, end-1, unpack=False) level = 0 in_string = False escaped = False tokens = [] token_start = start for i in xrange(start, end+1): if code[i] == '"' and not escaped: in_string = not in_string elif in_string: pass elif code[i] == '\'': escaped = not escaped elif code[i] == '(': level += 1 elif code[i] == ')': level -=1 if level == -1: raise SchemeException(_('At index %i, unexpected `)\' near %s') % (end, code[max(0, end-10):end+10])) elif level == 0: tokens.append(parse_scheme(code, token_start, i)) token_start = i+1 elif level == 0 and code[i] == ' ' and token_start != i: tokens.append(parse_scheme(code, token_start, i)) token_start = i+1 else: continue # Nothing to do if level != 0: raise SchemeException(_('Unclosed parenthesis in: %s') % code[start:end+1]) if start == token_start: return code[start:end+1] elif start < end: tokens.append(parse_scheme(code, token_start, end)) tokens = filter(bool, tokens) if unpack: assert len(tokens) == 1, tokens tokens = tokens[0] return tokens def eval_scheme(tree, env=DEFAULT_ENV): if isinstance(tree, str): if tree in env: return env[tree] else: print(repr(env)) raise SchemeException(_('Undefined keyword %s.') % tree) first = eval_scheme(tree[0]) if callable(first): return first(tree, env) else: return tree def eval_scheme_result(tree): if isinstance(tree, list): return '(%s)' % ' '.join(map(eval_scheme_result, tree)) else: return str(eval_argument(tree, [])) @internationalizeDocstring class Scheme(callbacks.Plugin): """Add the help for "@plugin help Scheme" here This should describe *how* to use this plugin.""" threaded = True @internationalizeDocstring def scheme(self, irc, msg, args, code): """ Evaluates Scheme.""" try: tree = parse_scheme(code) except SchemeException as e: irc.error('Syntax error: ' + e.args[0], Raise=True) try: result = eval_scheme(tree) except SchemeException as e: irc.error('Runtime error: ' + e.args[0], Raise=True) irc.reply(eval_scheme_result(result)) scheme = wrap(scheme, ['text']) Class = Scheme # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: