A cursed IGCSE pseudocode interpreter/transpiler
Project description
beancode
WARNING
This is the development branch of beancode. If you want stable builds or specifically versioned releases, head to the respective branches. This branch may have breaking changes and bugs added/removed anytime.
This is a tree-walking interpreter for IGCSE pseudocode, as shown in the 2023-2025 syllabus, written in Python (3.10+).
IMPORTANT: Some examples using raylib are provided. They were written entirely for fun; in order to run those examples one must install the raylib package for those examples to run, else, you will get an error.
IMPORTANT: I do not guarantee this software to be bug-free; most major bugs have been patched by now, and the interpreter has been tested against various examples and IGCSE Markschemes. Version 0.3.0 and up should be relatively stable, but if you find bugs, please report them and I will fix them promptly. consider this software (all 0.x versions) unstable and alpha-quality, breaking changes may happen at any time.
Once I deem it stable enough, I will tag v1.0.0.
Dependencies
None!
Installation
Notice
If you want to enjoy actually good performance, please use PyPy! It is a Python JIT (Just-in-time) compiler, making it far faster than the usual Python implementation CPython. I would recommend you use PyPy even if you werent using this project for running serious work, but it works really well for this project.
Check the appendix for some stats.
Installing from PyPI (pip)
pip install --break-system-packages beancodesince this package does not actually have dependencies, you can pass--break-system-packagessafely. It can still be a bad idea.pipx install beancode(the safer way, but you needpipxon your system first.)
Installing from this repository
- Clone the respository with
git clone https://github.com/ezntek/beancode cd beancodepipx install .
Notes on using pip
If you use pip, you may be faced with an error as such:
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try 'pacman -S
python-xyz', where xyz is the package you are trying to
install.
=== snip ===
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
You can either choose to run pip install . --break-system-packages, which does not actually cause any issues, as to my knowledge, nobody packages beancode outside of PyPI. You can always run it in a virtual environment.
Either way, it is still recommended to use pipx, as all the hard work of isolating beancode is done for you.
Running
note: the extension of the source file does not matter, but I recommend .bean.
If you installed it globally:
beancode file.bean
If you wish to run it in the project directory:
python -m beancode file.bean
You may also run
./main.py file.bean
The REPL
The REPL (or Read-Eval-Print-Loop) allows you to write beancode directly in your terminal. Run beancode (with the above instructions) without any arguments (i.e. just the command), and you will be dropped into this prompt:
=== welcome to beancode 0.5.0 ===
Using Python 3.13.7 (main, Sep 9 2025, 16:20:24) [GCC 15.2.1 20250813]
type ".help" for a list of REPL commands, ".exit" to exit, or start typing some code.
>>
You can immediately begin typing Pseudocode, and all errors will be reported to you. If you want to run a beancode script, you can just INCLUDE "MyScript.bean" to execute it, and then immediately return to the REPL.
You can also start typing dot-commands, which do not control the beancode interpreter, but controls the wrapper around it that provides you with REPL functionality. You can see the list of commands with .help, and detailed help is listed below:
REPL features
.var [name]gets information regarding an existing variable. It prints its name, type, and value. Substitute[name]for an actual constant or variable variable's name..varsprints information regarding all variables..func [name]gets information regarding existing functions or procedures. Substitute[name]for an actual function or procedure's name..funcsprints information regarding all functions and procedures..delete [name]lets- Delete a variable if you need to with
.delete [name]. (Version0.3.4and up) - reset the entire interpreter's state with
.reset.- This effectively clears all variables, functions, constants, procedures, and included symbols.
Extra Features
There are many extra features, or beancode extensions, which are not standard to IGCSE Pseudocode.
In theory, you can write this in your exams, and examiners should understand what you are doing, but it is safer to steer away from these extensions for formal purposes, and only use them for your own personal testing.
- Lowercase keywords are supported; but cases may not be mixed. All library routines are fully case-insensitive.
- Includes can be done with
INCLUDE "file.bean", relative to the file.
- Mark a declaration, constant, procedure, or function as exportable with
EXPORT, likeEXPORT DECLARE X:INTEGER. - Symbols marked as export will be present in whichever scope the include was called.
- Use
include_ffito include a bundled FFI module. Support for custom external modules will be added later.beanrayis an incomplete set of raylib bindings that supports some basic examples.demo_ffimodis just a demo.beanstdwill be a standard library to make testing a little easier.
-
You can declare a manual scope with:
SCOPE OUTPUT "Hallo, Welt." ENDSCOPEExporting form a custom scope also works:
SCOPE EXPORT CONSTANT Age <- 5 ENDSCOPE OUTPUT Age -
There are many custom library routines:
FUNCTION GETCHAR() RETURNS CHARPROCEDURE PUTCHAR(ch: CHAR)PROCEDURE EXIT(code: INTEGER)And many more. Look at the full list of library routines by runninghelp("libroutines")in the REPL.
- Type casting is supported:
Any Type -> STRINGSTRING -> INTEGER(returnsnullon failure)STRING -> REAL(returnsnullon failure)INTEGER -> REALREAL -> INTEGERINTEGER -> BOOLEAN(0is false,1is true)BOOLEAN -> INTEGER
- Declaration and assignment on the same line is also supported:
DECLARE Num:INTEGER <- 5
- You can also declare variables without types and directly assign them:
DECLARE Num <- 5
- Get the type of any value as a string with
TYPE(value)orTYPEOF(value). - Array literals are supported:
Arr <- {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}It will declare an array of the given type, with the size of the elements in it. You may not append items, but you can always get the type of the array withTYPEOF(Arr).
- You can directly assign variables without declaring its type through type inference:
X <- 5 OUTPUT X // works - If you need more help, or a reference to some features in the language, always check out the
help()library routine. Pass in a string, likehelp("help")to get help.
Tips and Tricks
- If you want to pass arrays of an unknown size into functions or procedures, you can read the
BubbleSort.beanexample in the examples directory. In short, you can pass both the array and the length of it together:
PROCEDURE PrintArray(Data: ARRAY[1:End] OF INTEGER, End: INTEGER)
FOR Counter <- 1 TO End
OUTPUT Data[Counter]
NEXT Counter
ENDPROCEDURE
- If you somehow do want multiple statement in CASE OFs, you can abuse the following trick:
CASE OF Var
CASE 'a': IF TRUE THEN
OUTPUT "Message 1"
OUTPUT "Message 2"
ENDIF
// Other stuff goes here
ENDCASE
This works because beancode only looks for one statement after your case. Since an if statement is just a statement, this will work just fine :)
- Don't declare variables when you use the REPL! Just assign them.
- If you ever need to work with arrays in the REPL, just use array literals! Just like in Python with square brackets
[], you can declare and initialize an array already with elements in it on the spot. It works for 2D arrays too!
EvenNumbers <- {2, 4, 6, 8, 10}
// EvenNumbers is now an ARRAY[1:5] OF INTEGER
Grid <- {
{1, 2},
{3, 4},
{5, 6},
}
// Grid is now an ARRAY[1:3,1:2] OF INTEGER.
// You can leave the declaration on the same line
- If you are ever in a situation like so:
>> FOR i <- 2 TO 10 STEP 2
.. OUTPUT "Num: ", i
..
And you want to quit editing the current block, type Ctrl-C, which will drop you back to the normal REPL.
If you want to properly exit the REPL, type .exit or .quit.
- If you are stuck in an infinite loop (that you may have written), you can always hit
Ctrl-Cto terminate the running program or the REPL.
quirks
- Multiple statements in CASE OFs are not supported! Therefore, the following code is illegal:
Please put your code into a procedure instead, or use the recommended trick above.CASE OF Var 'a': OUTPUT "foo" OUTPUT "bar" ENDCASE - No-declare assignments are only bound to the
local block-level scope, they are not global. Please declare it globally if you want to use it like a global variable. - File IO is completely unsupported. You might get cryptic errors if you try.
- Not more than 1 parse error can be reported at one time.
- Variable shadowing is extremely weird!
- You can shadow variables perfectly fine in functions and procedures, check
examples/ShadowingDemo.beanfor examples. However, you cannot do so in any other scope, as there is no concept of a local/global variable, and the interpreter does not know when you declared a variable. - You can shadow variable declarations of the same type and same kind (i.e. variable or constant), but you cannot, lets say, shadow a variable with a constant, etc.
- You can shadow variables perfectly fine in functions and procedures, check
Appendix
This turned out to be a very cursed non-optimizing super-cursed super-cursed-pro-max-plus-ultra IGCSE pseudocode tree-walk interpreter written in the best language, Python.
(I definitely do not have 30,000 C projects and I definitely do not advocate for C and the burning of Python at the stake for projects such as this).
It's slow, it's horrible, it's hacky, but it works :) and if it ain't broke, don't fix it.
This is my foray into compiler engineering; through this project I have finally learned how to perform recursive-descent parsing. I will most likely adapt this into C/Rust (maybe not C++) and play with a bytecode VM sooner or later (with a different language, because Python is slow and does not have null safety in 2025).
WARNING: This is NOT my best work. please do NOT assume my programming ability to be this, and do NOT use this project as a reference for yours. The layout is horrible. The code style is horrible. The code is not idiomatic. I went through 607,587,384 hacks and counting just for this project to work.
</rant>
Why Python?
Originally this interpreter was only written for me to learn compiler engineering (and how to write a recursive-descent parser and ast walker). However, it quickly spiralled into something usable that I wanted other people to use.
Python was perfect due to its dynamism, and the fact that I could abuse it to the max; and it came in super handy when I realized that students who already have a Python toolchain on their system should only need to run a single pip install to use my interpreter. It's meant as a learning tool anyway; it's slow as hell.
Performance
It's really bad. However, PyPy makes it a lot better. Here's some data for the PrimeTorture benchmark in the examples, ran on an i7-14700KF with 32GB RAM on Arch Linux:
| Language | Time Taken (s) |
|---|---|
| beancode (CPython 3.13.5) | 148 |
| beancode (PyPy3 7.3.20) | 11 |
| beancode (CPython Nuitka) | 185 |
| Python (CPython 3.13.5) | 0.88 |
| Python (PyPy3) | 0.19 |
| C (gcc 15.2.1) | 0.1 |
Errata
This section shares notable bugs that may impact daily use.
- Some errors will report as
invalid statement or expression, which is expected for this parser design.
Version-specific
- Before
v0.3.6, equal expressions will actually result in<>being true. For example,5 = 5isTRUE, but5 <> 5is alsoTRUE. - Before
v0.4.0, every word that is not a valid keyword is an identifier. Therefore, you could technically assign dollar signs and backslashes. - Before
v0.4.0, function names could be strange, like empty quotation marks. - Before
v0.4.0, you could shadow dot-commands in the REPL. - Before
v0.4.0, arithmetic with INTEGERs and REALs were very inconsistent, especially in type checking. There may be very weird behavior. - Before
v0.4.0, function return types were not checked at all, which may result in unexpected behavior. - Before
v0.5.0, assignments were not properly type-checked sometimes. You could not assign array literals to declared arrays. - Before
v0.5.0, you could not assign arrays, even of the same length and type to one another. - Before
v0.5.0, you could not declare arrays with only one item in it.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file beancode-0.5.2.tar.gz.
File metadata
- Download URL: beancode-0.5.2.tar.gz
- Upload date:
- Size: 57.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 PyPy/7.3.20 Linux/6.17.7-gentoo-dist
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
202b3195bb81d12dac7cd300fd4d4a3120645d2cb01340245b5484bb73c4ad6f
|
|
| MD5 |
19ddf810d39252034f7f433fa5891a6f
|
|
| BLAKE2b-256 |
dda19bc227f8dcfa1f43daf8cc7793714e28e1030327b5333602be317e98a9ea
|
File details
Details for the file beancode-0.5.2-py3-none-any.whl.
File metadata
- Download URL: beancode-0.5.2-py3-none-any.whl
- Upload date:
- Size: 59.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 PyPy/7.3.20 Linux/6.17.7-gentoo-dist
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
482e17942b6f75e11621b8701bbed841ffff594a7195ff46164790f51ad9db9a
|
|
| MD5 |
5bf7ded9ef866b067a5595768eed0036
|
|
| BLAKE2b-256 |
dbb423ab22de0f93b15aae9a854bb19be270ab11e9587c199a209f3ebd693956
|