# Starting point for a collaboratively developed golfing language interpreted in Python

Over on Code Golf, we've decided, for once, to write some readable code! However, we're obviously not very good at that, so we need your help to make it even better.

I've proposed this challenge on PPCG, which requires an interpreter for a new golfing language, written in Python 3.4. I've finished the base file, but, as this will be collaborated on by many users, I need to make sure it's readable, understandable and, well, not golfed.

The basic interpreter included a collection of different memory models, as to not restrict modifications, such as a tape (à la brainfuck), or a stack, similar to ><>, and also includes the basis of commands needed to perform the tasks in the linked challenge.

Unfortunately, it is sparse when it comes to comments, and in addition to feedback on how the code is written, I'd appreciate feedback or suggestions on the level of commenting needed, ranging from I can barely understand what this is doing, fill it all with comments to Pepper a few throughout, near the more complicated parts. The code is as follows:

import argparse
import collections
import sys

# === Constants === #

inf = float('inf')
nan = float('nan')

digits = str.maketrans('⁰¹²³⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉', '01234567890123456789')

code_page  = '''ÀÁÂÄÆÃÅĀĄ\t\nĆČÇĎÐÈÉÊËĒĖĚĘÌÍÎÏĪĮĹĽ'''
code_page += ''' !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~¶''' code_page += '''ŁŃŇÑŊÒÓÔÖŒÕØŌŔŘŚŠŤŦÙÚÛÜŮŪŴÝŶŸŹŽŻàáâäæãåāąćčçďðèéêëēėěęìíîïīįĺľłńňñŋòóôöœøōõŕřßśšťŧùúûüůūŵýŷÿźžż◊''' code_page += '''ΑΆΒΓΔΕΈΖΗΉΘΙΊΚΛΜΝΞΟΌΠΡΣΤΥΎΦΧΨΩΏ''' code_page += '''αάβγδεέζηήθιίΐκλμνξοόπσςτυύΰφχψωώ''' code_page += '''ǴḰḾṔẂǵḱḿṕẃḂḞĠḢṀȮṖṠṪẊḃḟġḣṁȯṗṡṫẋ§ĂĞĬŎŬĴăğĭŏŭĵªº‹›''' code_page += '''ƁƇƊƑƓƘⱮƝƤƬƲȤɓƈɗƒɠɦƙɱɲƥʠɼʂƭʋȥ©®ıȷ''' code_page += '''ЉЊЕРТЗУИОПШАСДФГХЈКЛЧЋЅЏЦВБНМЂЖљњертзуиопшасдфгхјклчћѕџцвбнмђжÞþ''' code_page += '''†∂∆≈≠√∈∉∌∋∩∪¬∧∨⊕¤₽¥£¢€₩‰¿¡⁇⁈‼⁉‽⸘…°•”“„’‘≤«·»≥ᴇ∞¦×⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾÷₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎''' # === Memory models === # class Model: def is_tape(self): return self.__class__.__name__ == 'Tape' def is_stack(self): return self.__class__.__name__ == 'Stack' def is_grid(self): return self.__class__.__name__ == 'Grid' def is_register(self): return self.__class__.__name__ == 'Register' def is_deque(self): return self.__class__.__name__ == 'Deque' class Stack(Model): def __init__(self, *values): self.stack = list(values) def push(self, *values): for value in values: self.stack.append(value) def pop(self, index = -1): return self.stack.pop(index) def peek(self, index = -1): return self.stack[index % len(self.stack)] def __repr__(self): return 'Stack({})'.format(self.stack) def __str__(self): return str(self.stack) def __iter__(self): return iter(self.stack) def __getitem__(self, index): return self.stack[index] def __setitem__(self, index, value): self.stack[index] = value class Tape(Model): def __init__(self, size = inf, wrap = True): self.size = size self.wrap = wrap self.tape = [0] self.index = 0 if size != inf: self.tape = [0] * size def __repr__(self): if self.size == inf: return 'Tape([ … ])' final = 'Tape([' for i, val in enumerate(self.tape): val = str(val) if i == self.index: final += '{' + val + '}' else: final += val final += ' ' final += '])' return final def __str__(self): if self.size == inf: if len(self.tape): template = '[ … {} … ]' final = '' for i, val in enumerate(self.tape): val = str(val) if i == self.index: final += '{' + val + '}' else: final += val final += ' ' return template.format(final[:-1]) else: return '[ … ]' else: final = '[' for i, val in enumerate(self.tape): val = str(val) if i == self.index: final += '{' + val + '}' else: final += val final += ' ' final = final.strip() + ']' return final def __iter__(self): return iter(self.tape) @property def cell(self): return self.tape[self.index] def write_cell(self, value): self.tape[self.index] = value def move_right(self): self.index += 1 if self.size != inf: if self.wrap: self.index %= self.size else: while self.index >= len(self.tape): self.tape.append(0) def move_left(self): self.index -= 1 if self.size != inf: if self.wrap and self.index < 0: self.index = self.size - 1 elif not self.wrap and self.index < 0: raise IndexError else: if self.index < 0: self.tape.insert(0, 0) self.index = 0 class Grid(Model): def __init__(self, xlen = inf, ylen = inf, xwrap = True, ywrap = True): self.xlen = xlen self.ylen = ylen self.xwrap = xwrap self.ywrap = ywrap self.xindex = 0 self.yindex = 0 self.grid = [] self.pointer_value = 0 if xlen != inf: if ylen != inf: for _ in range(ylen): self.grid.append([0] * xlen) else: self.grid.append([0] * xlen) else: if ylen != inf: for _ in range(ylen): self.grid.append([0]) else: for _ in range(10): self.grid.append([0] * 10) def __repr__(self): final = '' if self.xlen == inf: if self.ylen == inf: final = '[ … …\n ⋮ ⋮ ]' else: final = '' for line in self.grid: final += ' ' + str(line) \ .replace(',', '') \ .replace('[', '') \ .replace(']', '') + ' …\n' final = '[' + final[1:-1] + ']' else: if self.ylen == inf: final = '[' + ('⋮ ' * int(len(self.grid[0]) * 4/5)) + '\n' for line in self.grid: final += ' ' + str(line) \ .replace(',', '') \ .replace('[', '') \ .replace(']', '') + '\n' final += ' ' + ('⋮ ' * int(len(self.grid[0]) * 4/5)) + ']' else: final = '' for line in self.grid: final += ' ' + str(line) \ .replace(',', '') \ .replace('[', '') \ .replace(']', '') + '\n' final = '[' + final[1:-1] + ']' return final def __str__(self): array = list(map(lambda a: list(map(str, a)), self.grid)) max_row_length = max(map(len, array)) for index, row in enumerate(array): while len(row) < max_row_length: row.append('0') array[index] = row pad = max(max(map(len, a)) for a in array) + 1 final = '' for row in array: for element in row: element = element.rjust(pad) final += element final += '\n' return final + '<> {} <> {}'.format(self.xindex, self.yindex) def __iter__(self): return iter(strip(flatten(self.grid), 0)) @property def cell(self): return self.grid[self.yindex][self.xindex] def write_cell(self, value): self.grid[self.yindex][self.xindex] = value def write_pointer(self): self.pointer_value = self.cell def move_right(self): self.xindex += 1 if self.xlen == inf: self.grid[self.yindex].append(0) else: if self.xwrap: self.xindex %= self.xlen def move_left(self): self.xindex -= 1 if self.xlen == inf and self.xindex < 0: self.grid[self.yindex].insert(0, 0) self.xindex = 0 else: if self.xindex < 0 and self.xwrap: self.xindex = self.xlen - abs(self.xindex) elif self.xindex < 0 and not self.xwrap: self.xindex = self.xlen + 1 def move_down(self): self.yindex += 1 if self.ylen == inf: if self.xlen != inf: self.grid.append([0] * self.xlen) else: self.grid.append([0]) else: if self.ywrap: self.yindex %= self.ylen def move_up(self): self.yindex -= 1 if self.ylen == inf and self.yindex < 0: self.grid[self.xindex].insert(0, 0) self.yindex = 0 else: if self.yindex < 0 and self.ywrap: self.yindex = self.ylen - abs(self.yindex) elif self.yindex < 0 and not self.ywrap: self.yindex = self.ylen + 1 class Register(Model, int): def __init__(self, value = None): self.value = value def __repr__(self): return 'Register({})'.format(self.value) def __str__(self): return str(self.value) class Deque(Model, collections.deque): def __repr__(self): rep = super().__repr__()[5:] return 'Deque' + rep def __str__(self): return super().__repr__()[6:-1] push = collections.deque.append def current_value(model): if model.is_tape() or model.is_grid(): return model.cell if model.is_stack() or model.is_deque(): return model.pop() if model.is_register(): return model.value return False def identity(value): return value def overload(stack_cmd = identity, tape_cmd = identity, grid_cmd = identity, register_cmd = identity, deque_cmd = identity, all_cmd = None): def inner(model): if all_cmd is not None: return all_cmd(model) if model.is_stack(): return stack_cmd(model) if model.is_tape(): return tape_cmd(model) if model.is_grid(): return grid_cmd(model) if model.is_register(): return register_cmd(model) return deque_cmd(model) return inner def make_nilad(value, model): value = eval(value) if model.is_tape() or model.is_grid(): model.write_cell(value) if model.is_stack(): model.push(value) if model.is_deque(): model.append(value) if model.is_register(): model.value = value # === Functions === # # = Stack Commands = # def add(stack): stack.push(stack.pop() + stack.pop()) def subtract(stack): stack.push(stack.pop() - stack.pop()) def multiply(stack): stack.push(stack.pop() * stack.pop()) def divide(stack): stack.push(stack.pop() / stack.pop()) def modulo(stack): stack.push(stack.pop() % stack.pop()) def swap(array): array[-2], array[-1] = array[-1], array[-2] def reverse(stack): if hasattr(stack.peek(), '__iter__'): stack.push(stack.pop()[::-1]) else: stack.stack = stack.stack[::-1] def product(array): total = 1 for element in array: total *= element return total def flatten(array): flat = [] if type(array) == list: for item in array: flat += flatten(item) else: flat.append(array) return flat def strip(array, trim): final = [] for value in array: if value != trim or final: final.append(value) array = final[::-1] final = [] for value in array: if value != trim or final: final.append(value) return final[::-1] def rotate(stack): a = stack.pop() b = stack.pop() c = stack.pop() stack.push(a, c, b) # = Tape / Grid Commands = # def increment_cell(tape_grid): tape_grid.write_cell(tape_grid.cell + 1) def decrement_cell(tape_grid): tape_grid.write_cell(tape_grid.cell - 1) def write_grid_pointer(grid): grid.pointer_value def getchar(): ret = sys.stdin.read(1) if ret: return ret return chr(0) def getinput(): try: inputted = input() except: return '' try: return eval(inputted) except: return inputted # === Operators === # def while_loop(code): def while_inner(model, used): while current_value(model): model = interpret(code, model, used) return while_inner def if_statement(code): def if_inner(model, used): else_, if_ = list(map(lambda a: a[::-1], code[::-1].split('}', 1))) if current_value(model): model = interpret(if_, model, used) else: model = interpret(else_, model, used) return if_inner # === Command lookups === # memories = { '#': (0, Tape), '$': (0, Stack),
'G': (0, Grid),
'D': (0, Deque),
'S': (1, Register),

'À': (2, lambda size, wrap: Tape(size, wrap)),
'Á': (1, lambda size: Tape(size, wrap = True)),
'Â': (1, lambda size: Tape(size, wrap = False)),
'Ä': (1, lambda wrap: Tape(wrap = wrap)),

'Ǵ': (4, lambda xlen, ylen, xwrap, ywrap: Grid(xlen, ylen, xwrap, ywrap)),

}

functions = {

'+': overload(
stack_cmd = add,
deque_cmd = add,
tape_cmd = increment_cell,
grid_cmd = increment_cell,
),

'-': overload(
stack_cmd = subtract,
deque_cmd = subtract,
tape_cmd = decrement_cell,
grid_cmd = decrement_cell,
),

'%': overload(
stack_cmd = modulo,
deque_cmd = modulo,
),

':': overload(
stack_cmd = lambda stack: stack.push(stack.peek()),
deque_cmd = lambda deque: deque.append(deque[-1]),
),

';': overload(
stack_cmd = lambda stack: stack.pop(),
deque_cmd = lambda deque: deque.pop(),
tape_cmd = lambda tape: tape.write_cell(0),
grid_cmd = lambda grid: grid.write_cell(0),
),

'<': overload(
tape_cmd = Tape.move_left,
grid_cmd = Grid.move_left,
),

'=': overload(
stack_cmd = lambda stack: stack.push(stack.pop() == stack.pop()),
grid_cmd = Grid.move_down,
),

'>': overload(
tape_cmd = Tape.move_right,
grid_cmd = Grid.move_right,
),

'?': overload(
stack_cmd = lambda stack: stack.push(getinput()),
deque_cmd = lambda deque: deque.append(getinput()),
tape_cmd = lambda tape: tape.write_cell(ord(getchar())),
grid_cmd = lambda grid: grid.write_cell(ord(getchar())),
),

'O': overload(
all_cmd = lambda model: print(end = str(current_value(model))),
),

'Z': overload(
stack_cmd = lambda stack: stack.push(stack.pop() > 0),
),

'R': overload(
stack_cmd = reverse,
),

'^': overload(
grid_cmd = Grid.move_up,
),

'h': overload(
all_cmd = print,
),

'o': overload(
all_cmd = lambda model: print(end = chr(current_value(model))),
),

'r': overload(
stack_cmd = rotate,
deque_cmd = lambda deque: deque.rotate(deque.pop()),
),

's': overload(
stack_cmd = swap,
deque_cmd = swap,
),

'Π': overload(
stack_cmd = lambda stack: stack.push(product(stack)),
tape_cmd = lambda tape: tape.write_cell(product(tape)),
grid_cmd = lambda grid: grid.write_cell(product(flatten(list(grid)))),
),

'Σ': overload(
stack_cmd = lambda stack: stack.push(sum(stack)),
tape_cmd = lambda tape: tape.write_cell(sum(tape)),
grid_cmd = lambda grid: grid.write_cell(sum(flatten(list(grid)))),
),

'×': overload(
stack_cmd = multiply,
grid_cmd = lambda grid: grid.write_cell(grid.pointer_value),
),

'÷': overload(
stack_cmd = divide,
grid_cmd = Grid.write_pointer,
),

}

operators = {

'[': while_loop,
'{': if_statement,

}

def decode(bytestring):
decoded = ''
continue_byte = False

for index, byte in enumerate(bytestring):
if continue_byte:
continue_byte = False
continue

if byte == 0xff:
continue_byte = True
byte += bytestring[index + 1] + 1

try:
decoded += code_page[byte]
except:
raise UnicodeDecodeError('Unknown byte value: {}'.format(byte))

return decoded

def next_index(string, start, char):
index = start
depth = 1
while depth and index < len(string) - 1:
index += 1
if string[index] in operators.keys():
depth += 1
if string[index] == char:
depth -= 1
return index

def parse(code):
tokens = []
index = 0
comment = False

while index < len(code):
char = code[index]

if char == '◊':
comment = not comment

if comment:
index += 1
continue

if char.isdigit() or char == '_':
tokens.append(char)
index += 1
if index < len(code):
char = code[index]
while char.isdigit() and index < len(code):
tokens[-1] += char
index += 1
char = code[index]
tokens[-1] = tokens[-1].replace('_', '-')

if char in operators.keys():
ret = next_index(code, index, ']')
tokens.append(operators[char](code[index + 1 : ret]))
index = ret
else:
tokens.append(char)

index += 1
return tokens

def interpret(code, memory = None, used = None):
tokens = parse(code)
operable = (memory is not None)
if used is None:
used = []

for tkn in tokens:

if callable(tkn) and operable:
tkn(memory, used)

elif tkn in '₀₁₂₃₄₅₆₇₈₉' and operable:
tkn = int(tkn.translate(digits))
memory = used[tkn]

elif tkn.isdigit() or (tkn[0] in '-' and tkn[1:].isdigit()):
make_nilad(tkn, memory)

elif tkn in memories.keys():
arity, mem_type = memories[tkn]

memory = mem_type(*[current_value(memory) for _ in range(arity)])
used.append(memory)

operable = True

elif tkn == '@' and operable:
used.append(memory)
memory = used.pop(-2)

elif tkn in functions.keys() and operable:
functions[tkn](memory)

else:
continue

return memory

if __name__ == '__main__':

argparser = argparse.ArgumentParser()

a = 'store_true'

getcode = argparser.add_mutually_exclusive_group()
getcode.add_argument('-f', '--file', help = 'Specifies that code be read from a file', action = a)
getcode.add_argument('-c', '--cmd', '--cmdline', help = 'Specifies that code be read from the command line', action = a)

argparser.add_argument('-u', '--unicode', help = 'Use utf-8 encoding for files', action = a)

argparser.add_argument('program')
argparser.add_argument('argv', nargs = '*')

settings = argparser.parse_args()

if settings.file:
with open(settings.program, mode = 'rb') as file:
contents = file.read()

if settings.unicode:
contents = ''.join([char for char in contents.decode('utf-8') if char in code_page])
else:
contents = decode(contents)

if settings.cmd:
contents = bytes(settings.program, 'utf-8')

if settings.unicode:
contents = ''.join([char for char in contents.decode('utf-8') if char in code_page])
else:
contents = decode(contents)

interpret(contents)


Try it online!

For those interested, the GitHub repository contains 18 examples, all responses to the challenges listed in the question linked in the second paragraph

• If I understand correctly, at least one of file or cmd CLI argument is required. Am I right ? I am experiencing issue trying to run a simple example from your Github project. Apr 5 '18 at 9:09
• @Josay Correct. In order to read and run a file, you must provide the --file/-f flag followed by the filename, and the same with the --cmdline/-c flag for CLI Apr 5 '18 at 9:15

## 2 Answers

Object programming

Y'ouve split your code into various classes. Yet, it seems like the way you use them could be improved.

Indeed, the various "is_SOMETHING" method are very suspicious, both in the fact that it exists and the way it is implemented.

The implementation relies on the name of the class. This is fragile but also wrong if you actually plan to use inheritance. You could have an instance of a subclass of "Deque": its class would not be called "Deque" but it would actually be a Deque.

Thus, isinstance is what you should be using. For instance:

def is_tape(self):
return isinstance(self, Tape)

def is_stack(self):
return isinstance(self, Stack)

def is_grid(self):
return isinstance(self, Grid)

def is_register(self):
return isinstance(self, Register)

def is_deque(self):
return isinstance(self, Deque)


This is better but as I said previously, even having these methods in a first place seems to be against the basic principle of object programming.

You should not need to care about the type of an object, the behavior of the object itself should be defined properly. For instance, instead of having a current_value function checking the model class, you could have a abstract method, overriden in each subclass to have the behavior you need. This applies to make_nilad.

This needs quite a lot of changes, at this stage, the code looks like http://termbin.com/q8jc .

Then, the same principle applies to overload but it is trickier - work in progress in http://termbin.com/7x7cd .

Index out of bound error

I had a feeling that the boundary check in ithe while char.isdigit()... loop was weird and indeed, giving an input ending with 2 (or more) digits, en end up going too far in the input and throwing an exception.

I guess a better (but far from perfect) solution would be:

def parse(code):
tokens = []
index = 0
comment = False

while index < len(code):
char = code[index]
if char == '◊':
comment = not comment
elif not comment:
if char in operators.keys():
ret = next_index(code, index, ']')
token = operators[char](code[index + 1 : ret])
index = ret
elif char.isdigit() or char == '_':
#######################################
token = char.replace('_', '-')
index += 1
while index < len(code):
char = code[index]
if not char.isdigit():
break
token += char
index += 1
#######################################
else:
token = char
tokens.append(token)

index += 1
return tokens


There's a lot of code here, so I'm going to go through it very sketchily. If you need a point explaining in more detail, ask.

1. There's no documentation. What is the specification of the language being interpreted? The idea of the challenge to extend and modify the language and its interpreter. The documentation will need to be extended and modified at the same time. How is this going to be done?

2. There are no docstrings. How are people supposed to modify this code if there's no specification for what the classes mean and the methods do?

3. PEP8 recommends UPPER_CASE for module-level constants.

4. Instead of digits, use unicodedata.normalize.

5. Python concatenates adjacent string constants, so you can create code_page without the need for +=.

6. code_page[32:128] looks as if it is supposed to be the same as the corresponding code points in ASCII so why not make use of that fact and avoid the risk of typos?

7. There ought to be an encode function to correspond to decode otherwise how are people going to write their programs? Better still, make it a codec so that it can be passed to open.

8. What is the purpose of the Model class? It doesn't provide any common behaviour.

9. The design doesn't make use of object orientation. Consider a function like:

def current_value(model):
if model.is_tape() or model.is_grid():
return model.cell
if model.is_stack() or model.is_deque():
return model.pop()
if model.is_register():
return model.value
return False


In an object-oriented program this function would be a method on the Model class and its subclasses. In the Model class this would be an abstract implementation:

@abstractmethod
@property
def current_value(self):
"""Docstring here."""


and then the various subclasses would override this method with concrete implementations.

10. It seems wrong for current_value to modify the model in some cases (stack and deque) but not in others (tape, grid and register).

11. The Stack model seems to be a fairly thin wrapper around a list, so it could be implemented more simply by inheriting from list (as the Deque model inherits from collections.deque).

12. The Register model inherits from int but does not make use of this fact. Instead it uses a superfluous attribute value.

13. What is a "nilad"? Google says that it is like a monad, but taking no arguments. This does not seem to correspond to anything in the make_nilad function. Could you choose a better name?

14. Commands that operate on a Stack object (add, subtract etc.) ought to be methods on the Stack class. Similarly for tape and grid commands.

15. The arguments to the overload function all end with _cmd; this shared suffix is unnecessary.

16. I think it would be clearer if the Model subclasses did their own command dispatch, instead of doing this via the functions mapping and the overload function.

17. Input is delivered to the model via getinput and getchar, which read from standard input. This makes it difficult to write unit tests because you have to make a file or a pipe in order to deliver the input for the test case. It would be better to take input in some other way, for example from an buffer object passed to interpret.

18. There are lots of opportunities for η-conversion. For example lambda size, wrap: Tape(size, wrap) could be just Tape and lambda xlen, ylen, xwrap, ywrap: Grid(xlen, ylen, xwrap, ywrap) could be just Grid.

19. It seems strange that an if expression starts with { but ends with ]. I would have expected it to end with }.

20. next_index goes wrong if ] appears in a comment. (Maybe ] is not allowed to appear in a comment but if so I think this would need to be checked in parse.)

21. while_loop re-parses the body of the loop each time round the loop. Is this deliberate? This seems like a waste of time compared to the normal approach of parsing just once.

22. I recommend splitting up the tasks of tokenization, parsing and interpretation, instead of mixing them as here. (See these answers.)

23. There are no unit tests.

• I got the term nilad from this golfing language I use. However, when I googled "nilad", all the results were relating to a plant. What did you search? Apr 6 '18 at 17:57
• Also, what do you mean by η-conversion`? I'm not familiar with the term Apr 6 '18 at 18:12
• η-conversion is a term from the λ-calculus — it refers to the operation that transforms $λx.yx$ into $y$. Apr 6 '18 at 18:33