Extending PyFunge

PyFunge can be extended with various means. In particular, since PyFunge comes as a library you can experiment with them. (See Internals for extensive API documentation.)

Writing fingerprint

You can write your own Funge-98 fingerprints and use them in PyFunge. Thanks to Python’s dynamic nature, PyFunge can directly load your fingerprint; even fingerprints shipped with PyFunge are dynamically loaded.

The typical Funge-98 fingerprint looks like this:

from funge.fingerprint import Fingerprint

class HELO(Fingerprint):
    'Prints "Hello, world!"'

    API = 'PyFunge v2'
    ID = 0x48454c4f

    @Fingerprint.register('P')
    def print_hello(self, ip):
        self.platform.putstr('Hello, world!\n')

    @Fingerprint.register('S')
    def store_hello(self, ip):
        ip.push_string('Hello, world!\n')

You can save this Python code as fp_HELO.py and load it with -f:

$ pyfunge -f HELO -v98 -
"OLEH"4(PS>:#,_@
(EOF)
Hello, world!
Hello, world!

The name of Python code is not important, so you can change it to anything like fp_abracadabra.py and use correct option (in this case -f abracdabra). Even one module can contain several fingerprints. But by convention it uses same name with the ASCII representation of fingerprint ID, and only contains one fingerprint.

From now on this document assume that you are friendly in Python, or at least have written some programs in it.

Fingerprint class

Every fingerprint has a common base class: funge.fingerprint.Fingerprint. Actually this class provides nothing, besides from register decorator used to register new command. After the fingerprint module is imported PyFunge scans for a subclass of Fingerprint, and instantiates it when ( is executed.

Let’s analyze the HELO fingerprint above. It needs two attributes to function correctly.

  • API attribute says this fingerprint class is written for the correct API version. It should be "PyFunge v2" for current version. If the later version changes fingerprint API, it will work hard to support earlier versions.
  • ID attribute says this fingerprint is mapped to the given ID. In this example, we use ID 0x48454c4f, or HELO in the ASCII representation.

In addition you can give a docstring to describe this fingerprint shortly. Of course that is optional, and will only be seen with --list-fprints.

Then it registers two commands: P and S. This command callback receives one parameter (not counting for self), ip. This object, an instance of funge.ip.IP class, exposes many methods and attributes:

  • dimension gives the number of dimension.
  • position gives the current position of IP. You can also change it.
  • delta gives the current direction (delta) of IP. You can also change it.
  • space gives the attached Funge space. You can read from Funge space via get() or write to it via put().
  • push() pushes one value to the stack. You can also use push_string() or push_vector() to push a null-terminated string or vector.
  • Likewise, pop(), pop_string() and pop_vector() pops one value, string or vector from the stack. It ignores the stack underflow and returns zeroes for your convenience.
  • popmany() is handy; you can replace c = ip.pop(); b = ip.pop(); a = ip.pop() with c, b, a = ip.popmany(3).

Command callbacks are ordinary methods in the fingerprint class; the decorator, i.e. @Fingerprint.register(...), does register those methods for later use. The command can be two or more characters, in that case it registers many same commands:

@Fingerprint.register('0123456789')
def push_number(self, ip):
    ip.push(ip.space.get(ip.position) - ord('0'))

Fingerprint class itself got many methods from the underlying semantics. For example, self.reflect(ip) will reflect the IP. (Actual method is in funge.languages.funge98.Unefunge98 — check it!) Also you can walk to next instruction, using self.walk(ip).

One last thing to note is a Vector class, since every coordinates in PyFunge is a vector. For example you can change the delta of IP to non-cardinal one:

@Fingerprint.register('K')
def knight_walk(self, ip):
    import random

    if random.randint(0, 1):
        x, y = 1, 2
    else:
        x, y = 2, 1
    if random.randint(0, 1): x = -x
    if random.randint(0, 1): y = -y

    ip.delta = Vector.zero(ip.dimension).replace(_0=x, _1=y)

Since we deal not only with Befunge but Trefunge, we should build a generic vector. This won’t work in Unefunge, but you can add some sanity check for it:

@Fingerprint.register('K')
def knight_walk(self, ip):
    # reflect in Unefunge.
    if ip.dimension < 2:
        self.reflect(ip)
        return

    # ...

Initialization and finalization

The fingerprint class can have two special methods: init() and final(). These methods also receives the IP parameter, and are executed right after ( or ).

class USLS(Fingerprint):
    'Some useless fingerprint without any command.'
    API = 'PyFunge v2'; ID = 0x55534c53

    def init(self, ip):
        self.platform.putstr('Hey, you just loaded the useless fingerprint.\n')

    def final(self, ip):
        self.platform.putstr('Hey, you just unloaded the useless fingerprint.\n')

By default these methods register the commands to IP, so you may want to call the original methods in Fingerprint if you override them:

def init(self, ip):
    Fingerprint.init(self, ip)
    self.platform.putstr('Hey, you just loaded the useless fingerprint and '
                         '(possibly) some commands.\n')

If these methods raise the exception the loading or unloading rolls back and ( or ) reflects. But you still have to roll back your own changes, if any:

def init(self, ip):
    Fingerprint.init(self, ip)
    if self.some_check():
        # check failed: rolls back and raise the exception.
        Fingerprint.final(self, ip) # unregisters already registered commands
        raise RuntimeError('check failed!')

Also note that these methods can be executed out of order, and it is possible that the command callback is called even after final() method is called. So work can be done in final() is in fact quite limited.

Storing additional information

Sometimes your fingerprint needs to store some informations, like IP flags or call stack. Since Python is a dynamic language you are free to store them in any context, but you have to know where to store exactly.

If the information is only stored between the load and unload, you can just store it in the fingerprint class:

def init(self, ip):
    Fingerprint.init(self, ip)
    self.exoticflag = False

@Fingerprint.register('X')
def toggle_exotic(self, ip):
    self.exoticflag = not self.exoticflag

If the information is local to IP (but should be retained after unload), you can store it in the IP object. If the information is global you should store it in the Program object (ip.program). Since they are public objects, you have to use some unique prefix for the name.

def init(self, ip):
    Fingerprint.init(self, ip)

    # initialize default value if none.
    if not hasattr(ip, 'EXOT_exoticflag'):
        ip.EXOT_exoticflag = False
    if not hasattr(ip.program, 'EXOT_globalflag'):
        ip.program.EXOT_globalflag = False

@Fingerprint.register('X')
def toggle_exotic(self, ip):
    if ip.pop():
        ip.program.EXOT_globalflag = not ip.program.EXOT_globalflag
    else:
        ip.EXOT_exoticflag = not ip.EXOT_exoticflag

In the any case, do not use the global variable besides from constants. It won’t work correctly.