Supybot-plugins/Scheme/plugin.py

259 lines
8.1 KiB
Python

###
# 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):
"""<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: