implementing goto statements in python (in under 50 lines)
please note: this one knows nothing about python. if the reader sees something that it is wrong about, they are encouraged to e-mail it.
this is post is really rather self-explanatory, so it supposes it will start by showing the end result of this endeavor:
from goto_label import *
count = 0
#LABEL lol
print('this prints twice')
count += 1
goto &lol if (count <= 1) else goto &goto_statements_are_a_perfectly_reasonable_thing_to_have_in_python
#LABEL last_print
print('this prints last')
goto &stop
#LABEL ellipsis
print('at least Ellipsis() has some use now, for normal reasons that make sense')
goto &last_print
#LABEL goto_statements_are_a_perfectly_reasonable_thing_to_have_in_python
print('it is sure this is how the variety of language features it is abusing were intended to be used')
goto &ellipsis
#LABEL stop
...
this outputs the following:
this prints twice
this prints twice
it is sure this is how the variety of language features it is abusing were intended to be used
at least Ellipsis() has some use now, for normal reasons that make sense
this prints last
how it works
design decisions
given that—according to available data—python programs can be editted, it decided to implement goto statements based off of labels on some line from which a number can be calculated, and not based solely on jumping to line numbers. this allows programs containing goto statements to be editted without needing to recalculate each line number to jump to. additionally, for ease of use, simply importing * from goto_label.py in any file allows goto statements and labels for them to jump to to be defined and used.
technical implementation
firstly, labels:
labels are defined like so:
#LABEL label_namegoto_label.py has a function prepare_labels(fp, scope). this function looks for any labels (defined by matching /^#LABEL .*/) in the file fp. for each matching string, a variable labelname (whatever was after "#LABEL ") is created in the scope passed to the function, with a value of label(lineno+2). the reason we add two to the line number of the label is to account for the fact that the parser that creates labels at import time iterates over a list of lines, so we must add one to the index to account for 0-indexing, and then add one again to account for the fact that the label definition is its own line.
label = lambda lineno: type(
'label',
(object,),
dict(line=lineno)
)
def prepare_labels(fp, scope):
with open(fp, 'r') as f:
lines = f.readlines()
for lineno, line in enumerate(lines):
if line.startswith('#LABEL'):
# 1 + 1, first to account for 0 index then to account for comment line
scope.__setitem__(line.split(' ')[1].strip(), label(lineno+2))
next, the actual goto implementation:[2]
this one wanted the implementation of the goto function to feel very non-pythonic. it thinks it achieved this through making the syntax for calling goto be goto &label_name. this is done by simply creating a wrapper object (the first instance of _goto defined) with a custom __and__ method defined: the __and__ method takes the wrapper class and the label object, and calls the actual goto function on the label object. it simply does nothing with the reference to itself that gets passed to it.
so, given self-defining labels to jump to, and a function that will operate on those labels, all we need is an actual means of changing what code python is executing. this is shockingly simple: we just call sys._getframe() to get the current frame, and then access the f_back field once, to traverse back to the frame in which the _goto wrapper object's __and__ method is executing, and then again a second time to traverse back to the frame in which we call the wrapper object's __and__ method (read: the place in which goto &label is envoked).
given the frame which we want to jump to, we define a hook function to be installed as a traceback function for the target frame. when this hook function is called, it will attempt to set the line number of the target frame to the line number of the label we wish to jump to. if this fails, it will print an error. after modifying the frame's line number, we then remove our trace function from the target frame, and each of its parent frames. this results in execution continuing from the line number of the target label, provided no errors occur.
_goto = type('goto', (object,), dict(__and__=lambda _, other: (_goto(other.line))))
goto = _goto()
def _goto(lineno):
frame = sys._getframe().f_back.f_back
called_from = frame
def hook(frame, event, _):
if event == 'line' and frame == called_from:
try:
frame.f_lineno = lineno
except ValueError as e:
print("jump failed:", e)
while frame:
frame.f_trace = None
frame = frame.f_back
return None
return hook
# it doesn't think we actually need to set the hook for each frame
# while frame:
# frame.f_trace = hook
# frame = frame.f_back
called_from.f_trace = hook
sys.settrace(hook)
finally, the code that automatically creates labels for any file that imports it is quite simple. prepare_labels() just gets called with the filename and scope of the importing file passed as arguments. these values are gotten through similarly cursed stack inspection. all together, this yields the following code:
import sys, inspect
label = lambda lineno: type(
'label',
(object,),
dict(line=lineno)
)
def prepare_labels(fp, scope):
with open(fp, 'r') as f:
lines = f.readlines()
for lineno, line in enumerate(lines):
if line.startswith('#LABEL'):
# 1 + 1, first to account for 0 index then to account for comment line
scope.__setitem__(line.split(' ')[1].strip(), label(lineno+2))
_goto = type('goto', (object,), dict(__and__=lambda _, other: (_goto(other.line))))
goto = _goto()
def _goto(lineno):
frame = sys._getframe().f_back.f_back
called_from = frame
def hook(frame, event, _):
if event == 'line' and frame == called_from:
try:
frame.f_lineno = lineno
except ValueError as e:
print("jump failed:", e)
while frame:
frame.f_trace = None
frame = frame.f_back
return None
return hook
# it doesn't think we actually need to set the hook for each frame
# while frame:
# frame.f_trace = hook
# frame = frame.f_back
called_from.f_trace = hook
sys.settrace(hook)
prepare_labels(inspect.stack()[-1].frame.f_code.co_filename, inspect.stack()[-1].frame.f_globals)
limitations
from goto_label import *
gaurd = 0
#LABEL say_hi
print('hi')
#LABEL loop_start
gaurd += 1
if gaurd > 2: goto &call_b
def a():
global gaurd
goto &a_skip
#LABEL a_label
print('inside a_label')
goto &loop_start
if gaurd:
return
#LABEL a_skip
print('a')
if gaurd < 2:
goto &say_hi
goto &a_label
...
def b():
global gaurd
goto &b_skip
#LABEL b_label
print('inside b_label')
goto &say_hi
goto &a_label
if gaurd > 2:
return
#LABEL b_skip
print('b')
goto &a_label
a()
goto &b_label
...
a()
print('done with a()')
goto &say_hi
...
#LABEL call_b
b()
hi
a
jump failed: line 6 comes before the current code block
inside a_label
jump failed: line 9 comes before the current code block
done with a()
hi
a
done with a()
hi
b
jump failed: line 17 comes before the current code block
a
inside b_label
jump failed: line 6 comes before the current code block
jump failed: line 17 comes before the current code block
it is not sure if there is a good way to get around this.
various hacks using gotos
yes, all of these work. if the reader is ever in a job interview and is asked to write fizzbuzz, this one highly encourages them to use the last one. it is sure the interviewer would be thrilled.
from goto_label import *
def fib(n):
depth = 0
a = 1
b = 0
#LABEL fib_start
print(a)
a, b, depth = b, b + a, depth + 1
if depth > n+1: goto &fib_end
goto &fib_start
#LABEL fib_end
Ellipsis
fib(30)
from goto_label import *
def fizzbuzz(n):
count = 0
#LABEL fizz_start
str = ''
if n == count: goto &fizz_end;
count += 1
if count % 3 == 0: goto &fizz;
#LABEL buzz_check
if count % 5 == 0: goto &buzz;
goto &output;
#LABEL fizz
str += 'fizz'
goto &buzz_check;
#LABEL buzz
str += 'buzz'
#LABEL output
print(str if str else count)
goto &fizz_start;
#LABEL fizz_end
...
fizzbuzz(100)
from goto_label import *
def fizzbuzz(n):
count = 0
#LABEL fizz_start
count += 1
string = ''
if count == n+1: goto &fizz_end;
goto &label(output.line + int(count%3==0)*4+int(count%5==0)*8);
#LABEL output
print(string if string else count)
goto &fizz_start
string += 'fizz'
goto &output;
string += 'buzz'
goto &output;
string += 'fizzbuzz'
goto &output;
#LABEL fizz_end
...
fizzbuzz(100)
comments
as an anti-bot measure, in order for $VIEWER's comment to be stored on the server, $VIEWER MUST enter the commit hash of the current deployment found in the bottom right of the page footer. failure to do so will result in the comment being disregarded.