# -*- 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 . # ''' ████████╗██╗ ██╗██╗ ██╗███╗ ██╗ ╚══██╔══╝██║ ██║██║ ██║████╗ ██║ ██║ ███████║██║ ██║██╔██╗ ██║ ██║ ██╔══██║██║ ██║██║╚██╗██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ 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)