Note
This is a community-maintained fork of a MIT-licensed student project in the Effective Programming with Effects course in winter semester 2025/2026. Be warned that this is not an officially endorsed project, the code in this repository may be not idiomatic Effekt.
The original repository is https://github.com/DennisKleinhans/EffektPy
EffektPy is a lightweight interpreter for a Python-like programming language, implemented purely in Effekt. It features a fully functioning pipeline including a lexer, parser, desugarer, typechecker, and evaluator, all built using algebraic effects to manage state, environments, and error handling.
- Python-like Syntax: Familiar syntax with braces
{}for blocks. - Type Inference: Types for variables and function parameters can often be inferred, making type annotations optional in many cases.
- Smart Recursion: Support for mutual recursion and nested recursion (functions defined inside other functions).
- Interactive REPL: A Read-Eval-Print Loop with persistent state, multi-line input support, and smart history.
- First-Class Functions: Support for closures, higher-order functions, lambdas and default parameter values.
- Control Flow:
if/else,whileloops,break,continue, and implicit returns. - Built-in Standard Library: Includes essential functions like
print,input, and string conversions. - Syntactic Sugar: Supports convenient operators like
+=and-=.
To build and run this project, you need:
- The Effekt Compiler: Install Effekt
- Node.js: Required as the JavaScript backend runtime.
Compile the project to JavaScript:
effekt --compile --backend=js src/main.effektOnce compiled, the executable JavaScript file is located in the out directory (typically named main__main.js). You use Node.js to execute it.
1. Start the REPL (Interactive Mode): Run without arguments to enter the interactive shell.
node out/main__main.js2. Execute a File: Pass a filename to execute a specific script.
node out/main__main.js examples/fibonacci.pff3. Show Help:
node out/main__main.js --helpYou can annotate types explicitly, but EffektPy can also infer them.
// Explicit typing
val pi: Int = 3
var count: Int = 0
// Type Inference (types are automatically detected)
val num = 42
var active = true
// Syntactic sugar for assignment
count += 1Functions support implicit returns (the last expression is returned) and default parameter values.
// Return type inferred, 'return' keyword omitted
def add(a, b) {
a + b
}
// Default arguments
def greet(name = "World") {
print("Hello, " + name)
}EffektPy handles complex recursion patterns, including mutual recursion (functions calling each other) and nested recursion, without needing forward declarations.
def isEven(n) {
if n == 0 then true else isOdd(n - 1)
}
def isOdd(n) {
if n == 0 then false else isEven(n - 1)
}
print(isEven(4)) // trueEffektPy treats functions as first-class citizens, meaning they can be passed as arguments, returned from functions, and assigned to variables.
You can define functions without a name using the lambda keyword. These are ideal for use with Higher-Order Functions (HOFs).
// Passing a lambda directly to a HOF
def applyOp(a, b, op) { op(a, b) }
val result = applyOp(5, 5, lambda(x, y) { x * y }) // 25Lambdas capture their surrounding environment. This allows for powerful patterns like factory functions.
def makeMultiplier(factor) {
// The inner function 'remembers' factor
lambda(n) { n * factor }
}
val double = makeMultiplier(2)
print(double(10)) // 20EffektPy's type inference is powerful enough to handle most Higher-Order Functions (HOFs) automatically.
// Type inference works even for HOFs
def apply(f, x) {
f(x)
}
def increment(n) { n + 1 }
apply(increment, 10) // Returns 11Note on Advanced Types: While inference works for standard cases, explicit type annotations are required for complex scenarios such as Rank-2 types (functions that take polymorphic functions as arguments) or self-application patterns.
The interpreter comes with a set of built-in functions available in the global scope:
print(args...): Prints values to the console (accepts variable arguments of different types).input(prompt = ""): Reads a string from standard input.str(value): Converts any value to its string representation.min(a, b, ...)/max(a, b, ...): Returns the minimum or maximum of at least two numbers.
The REPL supports incremental type checking and evaluation. It features a brace-counting heuristic to support multi-line input.
> def fib(n) {
... if (n <= 1) { n }
... else { fib(n-1) + fib(n-2) }
... }
> fib(10)
55