One program, several pointers
In the original Befunge, one pointer runs through the program from start to finish. Funge-98 allows many. The instruction t, called split, creates a new pointer at the cell where it was executed. After the split there are two pointers sharing one program and one grid, both continuing from that point.
This is concurrency in a very literal form. There are no threads or locks, just several markers moving across the same cells. The result is a style of programming that feels more like choreography than like a to-do list.
What the new pointer inherits
When a pointer splits, the new one starts as a copy of the original in most respects:
- It begins at the same position.
- It gets its own copy of the stack of stacks, so the two pointers do not share a stack after the split. Values pushed by one are not visible to the other.
- It carries the same storage offset.
- Its movement is reversed. The new pointer starts by heading in the opposite direction from the parent. A parent moving east produces a child moving west.
Each pointer also has an identifier, which can be read with the system information instruction. A program can use that value to behave differently depending on which pointer is running it, which is how one piece of code can serve two roles.
What the two pointers do share is the grid. If one pointer writes to a cell, the other sees the change when it reads or executes there. This is useful for communication and is also the main source of bugs.
How the pointers interleave
Funge-98 describes execution in ticks. In each tick, every active pointer executes one instruction. Within a tick the pointers are generally taken in a consistent order, such as by identifier, but the details are the interpreter’s to define, and you should not depend on a particular order for correctness.
Some consequences:
- Two pointers on the same cell in the same tick each run that cell’s instruction, in turn.
- One output instruction per tick per pointer means output from different pointers tends to alternate rather than be grouped.
- A single instruction counts as one step even if it is a complex one. Pointers move in lockstep per instruction, not per unit of real time.
- The jump or skip instructions still count as a single step, so a pointer that skips ahead gets “ahead” of one that does not.
If you write a program where two pointers print, expect interleaved output, and design for it by letting only one pointer print at a time or by making the order irrelevant.
Ending pointers and ending the program
The two ways of stopping behave very differently:
- The
@instruction ends only the pointer that executes it. Others continue. The program ends when no pointers remain. - The
qinstruction ends the whole program immediately, with an exit code.
A frequent beginner mistake is to expect the program to stop when the first pointer finishes, and then to get an apparently frozen program because another pointer is running in a loop forever. Another is to use q carelessly and cut off output that other pointers had not yet produced. Decide on one clear way of stopping, and arrange for it deliberately.
A small illustration
Consider a pointer running east that meets a t. After the split there are two pointers: the original continues east along the same row, and the child heads west, back along the cells the parent just crossed. If those cells contain instructions, the child will execute them in reverse order. Usually, you want the child to turn onto a different row immediately, so the cell after the split often holds a turn instruction that only makes sense for one of them. A common trick is to put a direction-changing cell where the child will land, and to let the parent pass through a layout that the child’s turn avoids. Working this out on paper with two markers and a list of ticks is the best way to learn it.
Design cautions
Shared cells. If pointers modify the same cell, the order matters. Use separate regions of the grid for separate pointers unless sharing is the whole point.
Self-modifying code. Code that rewrites itself becomes much harder to reason about when more than one pointer might be reading it. Be especially careful with self-modifying code in a multi-pointer program.
Unbounded splitting. A split placed in a loop creates a new pointer on every pass. A program can quickly create many pointers and consume memory. Keep splits out of loops unless the number is bounded, or have the extra pointers end themselves.
Testing. A multi-pointer program is difficult to test by looking at output alone. A trace that prints each pointer’s position and the tick number is more informative, and building a trace visualizer helps. Interpreters also differ in subtle scheduling details, so test on more than one if you can.
Reproducibility. If the program also uses the question-mark instruction, the random direction choices add another source of variation. Combining randomness with concurrency makes bugs hard to repeat, so add one at a time.
When splitting is a good idea
Concurrency suits problems where independent searches proceed together, such as exploring several paths of a maze, or where a pattern naturally repeats across the grid. It also makes for entertaining puzzles, since each pointer behaves like a small actor. For most everyday programs a single pointer is simpler and just as good, and it is wise to treat the feature as a specialist tool.