Navigation
- README
- Pure-Python feature set
- Syntactic macro feature set
- Examples of creating dialects using
mcpyrate - REPL server
- Troubleshooting
- Design notes
- Essays
- Additional reading
- Contribution guidelines
Table of Contents
A Befunge-93 interpreter wrapped as a whole-module source dialect.
Powered by mcpyrate.
"""Hello from Befunge!"""
from unpythonic.dialects.befunge import dialects, Befunge # noqa: F401
"!egnufeB morf olleH">:#,_@The body of the file (everything after the dialect-import) is parsed as a Befunge-93 playfield and run by the runtime interpreter shipped in this module.
The recommended form for commentary is a module docstring above the dialect-import line, as shown — help(some_befunge_module) then displays it the same way it would for any documented Python module. Stand-alone # … comments above the dialect-import line work too. Below the dialect-import everything is the playfield; comments inside the body are not supported, because Befunge has no comment syntax (# is a real command — trampoline / skip-next-cell).
- Strict Befunge-93: 80 × 25 toroidal playfield, byte-valued cells, unbounded-int stack.
- The IP wraps toroidally on motion: off the right edge → column 0 of the same row, off the bottom → row 0 of the same column.
- Stack underflow on
popreturns 0 (per spec).
- All 93 commands supported:
- Arithmetic:
+,-,*,/,%(division and modulo by zero push 0, per spec). - Comparison and logic:
!(not),`(greater-than). - Direction:
>,<,^,v,?(random). - Conditional direction:
_(pop; 0 → east, else west),|(pop; 0 → south, else north). - String mode:
"toggles. While on, every cell pushesord(ch)instead of executing. - Stack:
:duplicate,\swap,$discard. - I/O:
.print int (followed by a space),,print char,&read int,~read char. - Trampoline:
#skip the next cell. - Self-modifying:
g(get cell value),p(put cell value); both pop(x, y)first. - Halt:
@. - Space is a no-op.
- Arithmetic:
?(random direction) is seedable for tests:run(src, *, seed=int)uses arandom.Randominstance, so the seed reaches the dispatch and doesn't perturb the user's process-wide RNG.seed=None(the default) picks via OS entropy.
The same interpreter is available as a plain function:
from unpythonic.dialects.befunge import run
run(playfield_source)I/O goes through sys.stdin / sys.stdout. To capture output in tests, wrap the call with contextlib.redirect_stdout; for input, use unpythonic.redirect_stdin (the third sibling — contextlib ships only the output redirectors).
Three distinct conditions, three distinct exception types:
SyntaxError— the source is malformed atPlayfield(src)construction time: more than 25 rows, or any line longer than 80 columns. Pre-execution.IndexError— runtime out-of-grid access viagorp. The IP itself never goes out of grid (its motion wraps toroidally);IndexErroronly fires on programs that compute their own coordinates and address outside the 80 × 25 bounds.UnknownOpcodeError(aRuntimeErrorsubclass exported fromunpythonic.dialects.befunge) — the IP visited a cell whose byte value isn't a recognized command. Includes both source-level typos andp-modified cells that ended up holding a non-command byte.
Befunge is a dialect of Python implemented as a whole-module source-to-source transform — at the text level, before mcpyrate's AST stage. The dialect definition and runtime interpreter both live in unpythonic.dialects.befunge. Usage examples can be found in the unit tests.
The transform_source hook for the Befunge dialect class wraps the playfield text in a single run(<source>) call, so a Befunge-dialect file ultimately compiles to two lines of Python:
from unpythonic.dialects.befunge import run
run(<the original playfield text>)Leading entirely-blank lines in the body are stripped before the playfield is built. Without that strip, the blank line that typically follows the dialect-import would become row 0 (all spaces); the IP would walk the full no-op row, wrap toroidally back to column 0 of row 0, and loop forever before reaching the actual program.
unpythonic.dialects.bf and unpythonic.dialects.befunge are the package's two source-level dialects, but they make complementary teaching points about transform_source:
- BF is a transpiler.
bf.compile(src)produces structured, legible Python that mirrors the input program. The pedagogic value is in the output text — you can read a BF program by reading the Python it compiles to. - Befunge is a reader.
transform_sourcewraps the playfield in a runtime interpreter call; the compiled output is a thin shim, and the language's semantics live in the interpreter. This isn't a design failure: BF is structurally close to Python (linear stream, lexical loops), but Befunge is fundamentally IP-driven on a 2-D, self-modifying grid, and a legible static translation is impossible. (Thepcommand rewrites the playfield at runtime, so no static analysis can be sound.)
The same transform_source hook serves both shapes; what differs is how much of the language's behavior the hook can statically lower into Python.
I/O operator names also differ between the two: BF uses . for character output, while Befunge uses . for integer output and , for character output. Worth keeping in mind when switching contexts.
Source-transforming dialects consume the whole module body, so combining Befunge with another source-transforming dialect (BF, or itself) on the same file doesn't really make sense. Composition with AST-transforming dialects is supported: from … import dialects, Befunge, SomeOptimizer (or on separate from lines) places SomeOptimizer after Befunge in the transform chain, running its AST pass on the output of the Befunge compiler — which, since Befunge's output is a one-liner run(...) call, mostly amounts to processing that one statement.
The mechanism is the mcpyrate.dialects.split_at_dialectimport helper (new in mcpyrate 4.1.0): the dialect's transform_source uses it to peel off its own dialect-import line while preserving any others for the next round of dialect processing.
Not intended for serious use.
See Wikipedia.