Thun/implementations/Python/simplejoy.py

765 lines
27 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright © 2022 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
████████╗██╗ ██╗██╗ ██╗███╗ ██╗
╚══██╔══╝██║ ██║██║ ██║████╗ ██║
██║ ███████║██║ ██║██╔██╗ ██║
██║ ██╔══██║██║ ██║██║╚██╗██║
██║ ██║ ██║╚██████╔╝██║ ╚████║
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
This script implements an interpreter for a dialect of Joy.
'''
from functools import wraps
from re import Scanner
from traceback import print_exc
import operator
JOY_BOOL_LITERALS = 'false', 'true'
class NotAListError(Exception):
pass
class NotAnIntError(Exception):
pass
class StackUnderflowError(Exception):
pass
class UnknownSymbolError(KeyError):
pass
'''
██╗███╗ ██╗████████╗███████╗██████╗ ██████╗ ██████╗ ███████╗████████╗███████╗██████╗
██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗
██║██╔██╗ ██║ ██║ █████╗ ██████╔╝██████╔╝██████╔╝█████╗ ██║ █████╗ ██████╔╝
██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗██╔═══╝ ██╔══██╗██╔══╝ ██║ ██╔══╝ ██╔══██╗
██║██║ ╚████║ ██║ ███████╗██║ ██║██║ ██║ ██║███████╗ ██║ ███████╗██║ ██║
╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
'''
def joy(stack, expr, dictionary):
while expr:
term, expr = expr
if isinstance(term, Symbol):
try:
func = dictionary[term]
except KeyError:
raise UnknownSymbolError(term) from None
stack, expr, dictionary = func(stack, expr, dictionary)
else:
stack = term, stack
return stack, expr, dictionary
'''
██████╗ █████╗ ██████╗ ███████╗███████╗██████╗
██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗
██████╔╝███████║██████╔╝███████╗█████╗ ██████╔╝
██╔═══╝ ██╔══██║██╔══██╗╚════██║██╔══╝ ██╔══██╗
██║ ██║ ██║██║ ██║███████║███████╗██║ ██║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝
There is a single function for converting text to a joy
expression as well as a single Symbol class and a single Exception type.
The Symbol string class is used by the interpreter to recognize literals
by the fact that they are not Symbol objects.
A crude grammar::
joy = term*
term = integer | '[' joy ']' | symbol
A Joy expression is a sequence of zero or more terms. A term is a
literal value (integer or Joy expression) or a function symbol.
Function symbols are sequences of non-blanks and cannot contain square
brackets. Terms must be separated by blanks, which can be omitted
around square brackets.
'''
BRACKETS = r'\[|\]' # Left or right square bracket.
BLANKS = r'\s+' # One-or-more blankspace.
WORDS = (
'[' # Character class
'^' # not a
'[' # left square bracket nor a
'\]' # right square bracket (escaped so it doesn't close the character class)
'\s' # nor blankspace
']+' # end character class, one-or-more.
)
token_scanner = Scanner(
[
(BRACKETS, lambda _, token: token),
(BLANKS, None),
(WORDS, lambda _, token: token),
]
)
class Symbol(str):
'''A string class that represents Joy function names.'''
__repr__ = str.__str__
def text_to_expression(text):
'''Convert a string to a Joy expression.
When supplied with a string this function returns a Python datastructure
that represents the Joy datastructure described by the text expression.
Any unbalanced square brackets will raise a ParseError.
:param str text: Text to convert.
:rtype: stack
:raises ParseError: if the parse fails.
'''
return _parse(_tokenize(text))
class ParseError(ValueError):
'''Raised when there is a error while parsing text.'''
def _tokenize(text):
'''Convert a text into a stream of tokens.
Converts function names to Symbols.
Raise ParseError (with some of the failing text) if the scan fails.
'''
tokens, rest = token_scanner.scan(text)
if rest:
raise ParseError(
'Scan failed at position %i, %r'
% (len(text) - len(rest), rest[:10])
)
return tokens
def _parse(tokens):
'''
Return a stack/list expression of the tokens.
'''
frame = []
stack = []
for tok in tokens:
if tok == '[':
stack.append(frame)
frame = []
elif tok == ']':
v = frame
try:
frame = stack.pop()
except IndexError:
raise ParseError('Extra closing bracket.') from None
frame.append(list_to_stack(v))
elif tok == 'true':
frame.append(True)
elif tok == 'false':
frame.append(False)
else:
try:
thing = int(tok)
except ValueError:
thing = Symbol(tok)
frame.append(thing)
if stack:
raise ParseError('Unclosed bracket.')
return list_to_stack(frame)
'''
███████╗████████╗ █████╗ ██████╗██╗ ██╗
██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
███████╗ ██║ ███████║██║ █████╔╝
╚════██║ ██║ ██╔══██║██║ ██╔═██╗
███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
'''
def list_to_stack(el, stack=()):
'''Convert a Python list (or other sequence) to a Joy stack::
[1, 2, 3] -> (1, (2, (3, ())))
:param list el: A Python list or other sequence (iterators and generators
won't work because ``reverse()`` is called on ``el``.)
:param stack stack: A stack, optional, defaults to the empty stack. This
allows for concatinating Python lists (or other sequence objects)
onto an existing Joy stack.
:rtype: stack
'''
for item in reversed(el):
stack = item, stack
return stack
def iter_stack(stack):
'''Iterate through the items on the stack.
:param stack stack: A stack.
:rtype: iterator
'''
while stack:
item, stack = stack
yield item
def concat(quote, expression):
'''Concatinate quote onto expression.
In joy [1 2] [3 4] would become [1 2 3 4].
:param stack quote: A stack.
:param stack expression: A stack.
:rtype: stack
'''
# This is the fastest implementation, but will trigger
# RuntimeError: maximum recursion depth exceeded
# on quotes longer than sys.getrecursionlimit().
# :raises RuntimeError: if quote is larger than sys.getrecursionlimit().
## return (quote[0], concat(quote[1], expression)) if quote else expression
# Original implementation.
## return list_to_stack(list(iter_stack(quote)), expression)
# In-lining is slightly faster (and won't break the
# recursion limit on long quotes.)
if not isinstance(quote, tuple):
raise NotAListError('Not a list.')
temp = []
while quote:
item, quote = quote
temp.append(item)
for item in reversed(temp):
expression = item, expression
return expression
'''
██████╗ ██████╗ ██╗███╗ ██╗████████╗███████╗██████╗
██╔══██╗██╔══██╗██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗
██████╔╝██████╔╝██║██╔██╗ ██║ ██║ █████╗ ██████╔╝
██╔═══╝ ██╔══██╗██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗
██║ ██║ ██║██║██║ ╚████║ ██║ ███████╗██║ ██║
╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
'''
def stack_to_string(stack):
'''
Return a "pretty print" string for a stack.
The items are written right-to-left::
(top, (second, ...)) -> '... second top'
:param stack stack: A stack.
:rtype: str
'''
f = lambda stack: reversed(list(iter_stack(stack)))
return _to_string(stack, f)
def expression_to_string(expression):
'''
Return a "pretty print" string for a expression.
The items are written left-to-right::
(top, (second, ...)) -> 'top second ...'
:param stack expression: A stack.
:rtype: str
'''
return _to_string(expression, iter_stack)
def _joy_repr(thing):
return (
JOY_BOOL_LITERALS[thing]
if isinstance(thing, bool)
else repr(thing)
)
def _to_string(stack, f):
if not isinstance(stack, tuple):
return _joy_repr(stack)
if not stack:
return '' # shortcut
return ' '.join(map(_s, f(stack)))
_s = lambda s: (
'[%s]' % expression_to_string(s)
if isinstance(s, tuple)
else _joy_repr(s)
)
'''
██████╗ ███████╗██████╗ ██╗
██╔══██╗██╔════╝██╔══██╗██║
██████╔╝█████╗ ██████╔╝██║
██╔══██╗██╔══╝ ██╔═══╝ ██║
██║ ██║███████╗██║ ███████╗
╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝
Read-Evaluate-Print Loop
'''
def repl(stack=(), dictionary=None):
'''
Read-Evaluate-Print Loop
Accept input and run it on the stack, loop.
:param stack stack: The stack.
:param dict dictionary: A ``dict`` mapping names to Joy functions.
:rtype: stack
'''
if dictionary is None:
dictionary = {}
try:
while True:
print()
print(stack_to_string(stack), '<-top')
print()
try:
text = input('joy? ')
except (EOFError, KeyboardInterrupt):
break
try:
stack, _, dictionary = run(text, stack, dictionary)
except:
print_exc()
except:
print_exc()
print()
return stack
def run(text, stack, dictionary):
expr = text_to_expression(text)
return joy(stack, expr, dictionary)
'''
██████╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██████╗ ██╗ ██╗
██╔══██╗██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝
██║ ██║██║██║ ██║ ██║██║ ██║██╔██╗ ██║███████║██████╔╝ ╚████╔╝
██║ ██║██║██║ ██║ ██║██║ ██║██║╚██╗██║██╔══██║██╔══██╗ ╚██╔╝
██████╔╝██║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║██║ ██║██║ ██║ ██║
╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
'''
# This is the main dict we're building.
_dictionary = {}
def inscribe(function, d=_dictionary):
'''A decorator to inscribe functions into the default dictionary.'''
d[function.__name__.rstrip('_')] = function
return function
def initialize():
'''Return a dictionary of Joy functions for use with joy().'''
return _dictionary.copy()
def SimpleFunctionWrapper(f):
'''Wrap functions that take and return just a stack.'''
@wraps(f)
def inner(stack, expr, dictionary):
return f(stack), expr, dictionary
return inner
'''
██████╗ ██████╗ ███╗ ███╗██████╗ ██╗███╗ ██╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗
██╔════╝██╔═══██╗████╗ ████║██╔══██╗██║████╗ ██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
██║ ██║ ██║██╔████╔██║██████╔╝██║██╔██╗ ██║███████║ ██║ ██║ ██║██████╔╝███████╗
██║ ██║ ██║██║╚██╔╝██║██╔══██╗██║██║╚██╗██║██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║
╚██████╗╚██████╔╝██║ ╚═╝ ██║██████╔╝██║██║ ╚████║██║ ██║ ██║ ╚██████╔╝██║ ██║███████║
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
'''
@inscribe
def branch(stack, expr, dictionary):
(then, (else_, (flag, stack))) = stack
do = then if flag else else_
return stack, concat(do, expr), dictionary
@inscribe
def dip(stack, expr, dictionary):
quote, (x, stack) = stack
return stack, concat(quote, (x, expr)), dictionary
@inscribe
def i(stack, expr, dictionary):
quote, stack = stack
return stack, concat(quote, expr), dictionary
LOOP = Symbol('loop')
@inscribe
def loop(stack, expr, dictionary):
quote, (flag, stack) = stack
if flag:
expr = concat(quote, (quote, (LOOP, expr)))
return stack, expr, dictionary
'''
██████╗ ██████╗ ██████╗ ███████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██║ ██║██╔═══██╗██╔══██╗██╔══██╗██╔════╝
██║ ██║ ██║██████╔╝█████╗ ██║ █╗ ██║██║ ██║██████╔╝██║ ██║███████╗
██║ ██║ ██║██╔══██╗██╔══╝ ██║███╗██║██║ ██║██╔══██╗██║ ██║╚════██║
╚██████╗╚██████╔╝██║ ██║███████╗ ╚███╔███╔╝╚██████╔╝██║ ██║██████╔╝███████║
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝
'''
@inscribe
@SimpleFunctionWrapper
def clear(stack):
return ()
@inscribe
@SimpleFunctionWrapper
def concat_(stack):
(tos, (second, stack)) = stack
return concat(second, tos), stack
@inscribe
@SimpleFunctionWrapper
def cons(stack):
s0, (a1, stack) = stack
return ((a1, s0), stack)
@inscribe
@SimpleFunctionWrapper
def dup(stack):
(a1, s23) = stack
return (a1, (a1, s23))
@inscribe
@SimpleFunctionWrapper
def first(stack):
((a1, s1), s23) = stack
return (a1, s23)
@inscribe
@SimpleFunctionWrapper
def pop(stack):
(_, s23) = stack
return s23
@inscribe
@SimpleFunctionWrapper
def rest(stack):
(_, s1), stack = stack
return (s1, stack)
@inscribe
@SimpleFunctionWrapper
def stack(stack):
return stack, stack
@inscribe
@SimpleFunctionWrapper
def swaack(stack):
(s1, s0) = stack
return (s0, s1)
@inscribe
@SimpleFunctionWrapper
def swap(stack):
(a2, (a1, s23)) = stack
return (a1, (a2, s23))
def BinaryFunc(f):
'''
Wrap functions that take two arguments and return a single result.
'''
@wraps(f)
def inner(stack, expression, dictionary):
(a, (b, stack)) = stack
result = f(b, a)
return (result, stack), expression, dictionary
return inner
def UnaryBuiltinWrapper(f):
'''
Wrap functions that take one argument and return a single result.
'''
@wraps(f)
def inner(stack, expression, dictionary):
(a, stack) = stack
result = f(a)
return (result, stack), expression, dictionary
return inner
for F in (
## ██████╗ ██████╗ ███╗ ███╗██████╗ █████╗ ██████╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗
##██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██║██╔═══██╗████╗ ██║
##██║ ██║ ██║██╔████╔██║██████╔╝███████║██████╔╝██║███████╗██║██║ ██║██╔██╗ ██║
##██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██╔══██╗██║╚════██║██║██║ ██║██║╚██╗██║
##╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║██║ ██║██║███████║██║╚██████╔╝██║ ╚████║
## ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝
BinaryFunc(operator.eq),
BinaryFunc(operator.ge),
BinaryFunc(operator.gt),
BinaryFunc(operator.le),
BinaryFunc(operator.lt),
BinaryFunc(operator.ne),
##██╗ ██████╗ ██████╗ ██╗ ██████╗
##██║ ██╔═══██╗██╔════╝ ██║██╔════╝
##██║ ██║ ██║██║ ███╗██║██║
##██║ ██║ ██║██║ ██║██║██║
##███████╗╚██████╔╝╚██████╔╝██║╚██████╗
##╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
BinaryFunc(operator.xor),
BinaryFunc(operator.and_),
BinaryFunc(operator.or_),
UnaryBuiltinWrapper(operator.not_),
##███╗ ███╗ █████╗ ████████╗██╗ ██╗
##████╗ ████║██╔══██╗╚══██╔══╝██║ ██║
##██╔████╔██║███████║ ██║ ███████║
##██║╚██╔╝██║██╔══██║ ██║ ██╔══██║
##██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║
##╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
BinaryFunc(operator.lshift),
BinaryFunc(operator.rshift),
BinaryFunc(operator.add),
BinaryFunc(operator.floordiv),
BinaryFunc(operator.mod),
BinaryFunc(operator.mul),
BinaryFunc(operator.pow),
BinaryFunc(operator.sub),
UnaryBuiltinWrapper(abs),
UnaryBuiltinWrapper(bool),
UnaryBuiltinWrapper(operator.neg),
):
inscribe(F)
'''
██████╗ ███████╗███████╗██╗███╗ ██╗██╗████████╗██╗ ██████╗ ███╗ ██╗███████╗
██╔══██╗██╔════╝██╔════╝██║████╗ ██║██║╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝
██║ ██║█████╗ █████╗ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║███████╗
██║ ██║██╔══╝ ██╔══╝ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║
██████╔╝███████╗██║ ██║██║ ╚████║██║ ██║ ██║╚██████╔╝██║ ╚████║███████║
╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
'''
class Def(object):
def __init__(self, name, body):
self.__name__ = name
self.body = tuple(iter_stack(body))
def __call__(self, stack, expr, dictionary):
expr = list_to_stack(self.body, expr)
return stack, expr, dictionary
@classmethod
def load_definitions(class_, stream, dictionary):
for line in stream:
if line.lstrip().startswith('#'):
continue
name, body = text_to_expression(line)
if name not in dictionary:
inscribe(class_(name, body), dictionary)
DEFS = '''\
-- 1 -
? dup bool
&& nulco [nullary [false]] dip branch
++ 1 +
|| nulco [nullary] dip [true] branch
!- 0 >=
<{} [] swap
<<{} [] rollup
abs dup 0 < [] [neg] branch
anamorphism [pop []] swap [dip swons] genrec
app1 grba infrst
app2 [grba swap grba swap] dip [infrst] cons ii
app3 3 appN
appN [grabN] codi map disenstacken
at drop first
average [sum] [size] cleave /
b [i] dip i
binary unary popd
ccccons ccons ccons
ccons cons cons
clear [] swaack pop
cleave fork popdd
clop cleave popdd
cmp [[>] swap] dipd [ifte] ccons [=] swons ifte
codi cons dip
codireco codi reco
dinfrirst dip infrst
dipd [dip] codi
disenstacken ? [uncons ?] loop pop
down_to_zero [0 >] [dup --] while
drop [rest] times
dupd [dup] dip
dupdd [dup] dipd
dupdip dupd dip
dupdipd dup dipd
enstacken stack [clear] dip
first uncons pop
flatten <{} [concat] step
fork [i] app2
fourth rest third
gcd true [tuck mod dup 0 >] loop pop
genrec [[genrec] ccccons] nullary swons concat ifte
grabN <{} [cons] times
grba [stack popd] dip
hypot [sqr] ii + sqrt
ifte [nullary] dipd swap branch
ii [dip] dupdip i
infra swons swaack [i] dip swaack
infrst infra first
make_generator [codireco] ccons
mod %
neg 0 swap -
not [true] [false] branch
nulco [nullary] cons
nullary [stack] dinfrirst
of swap at
pam [i] map
pm [+] [-] clop
popd [pop] dip
popdd [pop] dipd
popop pop pop
popopop pop popop
popopd [popop] dip
popopdd [popop] dipd
product 1 swap [*] step
quoted [unit] dip
range [0 <=] [1 - dup] anamorphism
range_to_zero unit [down_to_zero] infra
reco rest cons
rest uncons popd
reverse <{} shunt
roll> swap swapd
roll< swapd swap
rollup roll>
rolldown roll<
rrest rest rest
run <{} infra
second rest first
shift uncons [swons] dip
shunt [swons] step
size [pop ++] step_zero
spiral_next [[[abs] ii <=] [[<>] [pop !-] ||] &&] [[!-] [[++]] [[--]] ifte dip] [[pop !-] [--] [++] ifte] ifte
split_at [drop] [take] clop
split_list [take reverse] [drop] clop
sqr dup *
stackd [stack] dip
step_zero 0 roll> step
stuncons stack uncons
sum [+] step_zero
swapd [swap] dip
swons swap cons
swoncat swap concat
sqr dup mul
tailrec [i] genrec
take <<{} [shift] times pop
ternary binary popd
third rest second
tuck dup swapd
unary nullary popd
uncons [first] [rest] cleave
unit [] cons
unquoted [i] dip
unswons uncons swap
while swap nulco dupdipd concat loop
x dup i
step [_step0] x
_step0 _step1 [popopop] [_stept] branch
_step1 [?] dipd roll<
_stept [uncons] dipd [dupdipd] dip x
times [_times0] x
_times0 _times1 [popopop] [_timest] branch
_times1 [dup 0 >] dipd roll<
_timest [[--] dip dupdipd] dip x
map [_map0] cons [[] [_map?] [_mape]] dip tailrec
_map? pop bool not
_mape popd reverse
_map0 [_map1] dipd _map2
_map1 stackd shift
_map2 [infrst] cons dipd roll< swons
'''
if __name__ == '__main__':
dictionary = initialize()
Def.load_definitions(DEFS.splitlines(), dictionary)
stack = repl(dictionary=dictionary)