]> python-goto

implementing goto statements in python (in under 50 lines)

2024-09-12 08:43:04

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:

goto-example.py
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_name

goto_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.

functionally, this works out to the following:[1]
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.

the code described above works out to be the following:
_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:

goto_label.py
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

jumping between function scopes raises issues:
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()


output
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.

fibonacci
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)


fizzbuzz (normal)
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)


fizzbuzz (from hell)[3]
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)


this post was rewritten on 2025-05-12, as part of the migration to the new website. the wording has been updated, however it remains similar to the original.

it did not use the class keyword when creating classes in this project. it doesn't really have a reason for this, other than it thought it was funny.[1] the way it named the constructs here is rather confusing. don't code on 0 hours of sleep after having been awake for > 30 hours.[2] this works by jumping a different number of lines for each condition by defining new positions to jump to based on the value of count.[3]

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.