Functional Programming Language
Funk is a functional programming language inspired by Erlang and Python, with a practical toolchain centered on Python-based compilation plus cpp20 and Bytecode backends.
The language focuses on readable syntax, simple semantics, and deterministic behavior for tests, examples, and benchmarking.
The currently supported backends are cpp20 and bytecode; LLVM is not part of the active toolchain.
Please note the the state of this project is under early stages of development.
Install the python requirements:
pip install -r requirements.txt
Install build/runtime toolchains:
- C++ toolchain (for
cpp20backend):
Linux (Debian/Ubuntu):
sudo apt install clang
macOS (Homebrew):
brew install llvm
- Rust toolchain (for Bytecode VM):
Install from https://rustup.rs and ensure cargo is available in your shell.
A browser-only Funk Playground is available under web/ and deploys as a static site on GitHub Pages.
It reuses the existing Python compiler pipeline in-browser (via Pyodide) and executes Bytecode with the Rust WASM VM wrapper (crates/funk_wasm).
- Ensure WebAssembly target/tooling is available:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
- Build WASM package:
wasm-pack build ./crates/funk_wasm --target web --out-dir ../../web/src/pkg
- Run Vite dev server:
cd web
npm install
npm run dev
npm run dev syncs runtime compiler/stdlib files into web/public/runtime/, rebuilds WASM, and starts the local web app.
Deployment is defined in .github/workflows/pages.yml.
- Enable Pages in repo settings:
Settings -> Pages -> Source: GitHub Actions
- Push to
main(or run the workflow manually) to publish.
- No filesystem, network, or process effects are allowed at runtime.
- Programs using disallowed effects return a structured
E_EFFECTerror. - Execution is fuel-limited; exhaustion returns
E_FUEL. - Output is capped (default 64KB); overflow returns
E_OUTPUT_LIMIT.
Some examples (e.g. $FUNK_EXAMPLES_PATH/graphics/barnsly_fern.f) use SDL via sdl_simple. Install SDL2 development headers:
Linux (Debian/Ubuntu):
sudo apt install libsdl2-dev
macOS (Homebrew):
brew install sdl2
This repo uses git submodules for:
funk/(core compiler/runtime)stdlib/(standard library files used viaFUNK_INCLUDE_PATH)
After cloning, sync and initialize submodules:
make sync-submodules
If submodule URLs change later, re-sync and update:
make sync-submodules
To pull the latest changes for all submodules later:
make update-submodules
You can inspect current pins with:
make submodule-status
Validate local toolchain + submodule/env setup with:
make doctor
FUNK_INCLUDE_PATH defaults to ./stdlib in the Makefile.
You still need to set FUNK_EXAMPLES_PATH to your examples repo path (for make tests / make examples).
Version compatibility across funk, lib_funky, funk_stdlib, and funky_example_files is tracked in VERSIONS.md.
Quick local validation:
make tests-fast
make examples-smoke
Full local validation:
make tests
make tests-integration
make examples
Bytecode VM validation:
make vm-test
make bytecode-tests-subset
make test-bytecode-main
make test-bytecode
make bytecode-disasm-smoke
make bytecode-run-smoke
make test-bytecode-main requires FUNK_EXAMPLES_PATH (same as make test) because funk/tests/test_main.f pulls game helpers from $(FUNK_EXAMPLES_PATH)/games.
The bytecode backend emits both *.fkb.json (debug JSON) and *.fkb (binary) artifacts.
Release/pre-tag smoke:
make release-check
release-check now includes bytecode parity (make test-bytecode) in addition to the cpp20 fast checks.
The repository includes cross-language benchmarks (Funk vs Python vs C) and an auto-generated report.
make bench-report BENCH_RUNS=7 BENCH_WARMUP=2
This runs all benchmark workloads and regenerates:
benchmarks/benchmarks.mdbenchmarks/raw/results.csvbenchmarks/plots/*.svg
These files are generated locally and are not versioned.
BENCH_RUNS controls timed samples per configuration, and BENCH_WARMUP controls untimed warmup runs.
make bench-fib-compare
make bench-bytecode-smoke
make bench-fib-tr
make bench-concat-compare
make bench-sum-range
make bench-collatz
make bench-mutual-recursion
make bench-fp-dot
make bench-fp-axpy
make bench-fp-triad
You can also run fastpath/i32 variants:
make bench-fib-fastpath
make bench-concat-fastpath
make bench-fib-i32
make bench-concat-i32
make bench-fib-tr-fastpath
- Benchmarks use scripts under
scripts/benchmark_*.py. make bench-bytecode-smokeruns deterministic bytecode VM workloads (tests/bytecode/core_lists_ranges.fandtests/bytecode/clauses_recursion.f) and reports min/median/max runtime.- The default benchmark report command uses the local virtualenv python:
./venv_3.11/bin/python. - For stable comparisons, avoid running heavy background workloads while benchmarking.
- Contributor workflow and required local checks are documented in
CONTRIBUTING.md.
The most important programming blocks of Funk are functions.
Funk programs are essentially a collection of functions.
Functions are declared using the following syntax:
# This here is a function
myFunction(x,y):
x + y.
The last statement of a function is always the return statement.
In the previous example, the function myFunction has a single statement, which also happens to be the return statement.
In summary: All functions in Funk have a single return statement, and this is always the last statement of the function.
Function arguments in Funk can be any valid symbol.
This includes variables, lists, and off course other functions.
For example:
addMe(x):
x + 1.
useFunctionArg(F, y):
F(y).
Consider the following snippet.
# Calculate Fibonacci, using the naive approach
fibo( 0 ): 0.
fibo( 1 ): 1.
fibo( n ):
fibo( n - 1 ) + fibo( n - 2 ).
This example illustrates how you can specify special function behavior for specific values of the input arguments of a function.
Each of these function definitions for fibo is called a function clause*.
The order in which these function clauses are declared is important.
When the compiler lowers your code, it uses this same declaration order for clause pattern matching.
Funk has no if statements.
Instead, you use a combination of function preconditions and conditional variable assignments in order to achieve the same effect as have if statements.
Consider the following code:
fibo( n | n <= 1 ): n.
fibo( n ):
fibo( n - 1 ) + fibo( n - 2 ).
This example introduces the concept of function preconditions.
For example n | n <= 1 is read as 'n given that n is less of equal than 1`.
Note: In Funk a precondition can be any valid boolean statement. This includes things like function calls. Please note that Funk precondtions are different from Erlang guards. Erlang guarantees that guards are free from side effects. Funk makes no such guarantee, but in exchange, it allows a richer set of expression to be used.
All variables in Funk are immutable.
This means that once you assign a value to a variable, you cannot assign another value, it will result in a runtime error message.
To assign a value to variable you use the following syntax:
myFunction(x,y):
z <- x + y
z + 1.
In this example the variable z was assigned the value x + y.
Also note that this time myFunction has 2 statements.
The compiler does not need a special indentation to distinguish between statements.
The only requirement is that all statements are well formed, and the last statement in a function is followed by the dot terminator .
Conditional assignments are done using the <-? symbol:
x <-? y % 2 = 0: 1, -1
The previous expression can assign either 1 or -1 to x, depending on the evaluation of the boolean expression y % 2 = 0.
Also note that unlike Python, in Funk the symbol = stands for equality.
Lists are be specified using the following syntax:
# create an empty list
my_empty_list <- []
# create a list with integers from 1 to 10
my_list <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Or more succinctly, using a list comprehension like this:
# use a list comprehension to quickly create a list
my_list <- [ x | 1 <= x <= 10]
Funk lists are internally represented as linked lists.
This allows to very easily pop head element from the list, like so:
# pop the first element from my_list and store it in head
head <~ [my_list]
Extracting the first element is specially useful to implement recursion, which is fundamental to Funk.
Consider the following code:
foreach([],_): 1.
foreach( head <~ [tail], F):
F(head)
foreach(tail, F).
The previous code shows how the <~ operation can be used as part of the function firm.
Now consider the following snippet:
len([]): 0.
len(_<~[t]): 1 + len(t).
What is important about the previous snippet is the use of the _ symbol.
This simply means don't care, and can be used to discard elements that you don't need.
In order to prepend an element to list, you use the following notation:
map(_, []): [].
map(F, h <~ [t] ):
F(h) ~> [map(F, t)].
Note that you can also use in the opposite side of the array like so:
reverse([]): [].
reverse(h<~[t]): [reverse(t)] <~ h.
You can use functions declared in different files using the use keyword.
use foreach
fbz(x | x % 15 = 0): say(x, ' FizzBuzz') .
fbz(x | x % 3 = 0): say(x, ' Fizz') .
fbz(x | x % 5 = 0): say(x, ' Buzz') .
fbz(x) : say(x).
main():
foreach( [x | 1 <= x <= 100] , fbz ).
Lastly, there is special function called the main function.
This function shall take no arguments, and marks the entry point to your program.
