RPython is a statically typed, interpreted language with Python-inspired syntax. Unlike Python, it uses explicit end delimiters instead of indentation and requires type annotations on function signatures. The entire toolchain—lexer, parser, type checker, interpreter, and standard library—is implemented in Rust.
Tip: View
README.mdin Raw mode on GitHub to copy code snippets without HTML rendering.
- Motivation
- Language Overview
- Type System
- Control Flow
- Functions
- Standard Library (Metabuiltins)
- Error Handling
- Algebraic Data Types
- Test Blocks
- Project Architecture
- Build & Test
- Running Programs
- Current Limitations
- Contributing
RPython was created as a teaching tool for undergraduate courses on programming language implementation. The codebase demonstrates:
- Parsing with combinators: the parser uses
nomto build an AST from source text. - Scoped environments: lexical scoping is implemented via a stack of symbol tables.
- Static type checking: a type checker validates function signatures and expressions before execution.
- Tree-walking interpretation: the interpreter directly evaluates the AST.
The language surface resembles Python to lower the barrier for students, while the explicit block delimiters and mandatory type annotations expose concepts often hidden in dynamic languages.
val pi = 3.14159;
var counter = 0;
counter = counter + 1;
valdeclares an immutable variable; reassignment is a compile-time error.vardeclares a mutable variable.
Note: RPython does not support comments in source code. The examples in this README omit comments for accuracy.
| Type | Example |
|---|---|
| Integer | 42, -7 |
| Real | 3.14, 0.0 |
| String | "hello", "line\nbreak" |
| Boolean | True, False |
| List | [1, 2, 3] |
| Tuple | (1, "a", True) |
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, / |
| Equality | ==, != |
| Ordering | <, >, <=, >= |
| Logical | and, or, not |
Note: Equality operators support numbers, strings, and booleans. Ordering operators are currently limited to numbers.
RPython uses explicit, static types for function parameters and return values. Variables infer their type from the initializer expression.
Int— 32-bit signed integerReal— 64-bit floating pointBoolean—TrueorFalseString— UTF-8 textUnit— returned by side-effect-only functions (equivalent tovoid)Any— escape hatch; matches any type (used sparingly)
List[T]— homogeneous list of elements of typeTTuple[T1, T2, ...]— fixed-size, heterogeneous tupleMaybe[T]— optional value (Just(value)orNothing)Result[Ok, Err]— success/failure carrier (Ok(value)orErr(error))fn(T1, T2) -> R— first-class function type
if score >= 90:
grade = "A";
elif score >= 80:
grade = "B";
else:
grade = "C";
end
A single end closes the entire if-chain. The elif and else keywords introduce new branches without requiring separate end markers.
var i = 0;
while i < 5:
var _ = print_line(to_string(i));
i = i + 1;
end;
for x in [1, 2, 3]:
var _ = print_line(to_string(x));
end;
breakexits the innermost loop immediately.continueskips to the next iteration.
Functions require type annotations for parameters and return type.
def factorial(n: Int) -> Int:
if n <= 1:
return 1;
else:
return n * factorial(n - 1);
end;
end;
val result = factorial(5);
asserttrue(result == 120, "5! should be 120");
Syntax note: Block statements (
if,while,for,def) require a semicolon after the closingendwhen followed by additional statements at the same level.
Anonymous functions can be assigned to variables or passed as arguments.
val add = lambda (a: Int, b: Int) -> Int: return a + b end;
val sum = add(2, 3);
Current limitation: Lambdas are parsed correctly but not yet fully implemented in the interpreter.
Metabuiltins are functions implemented in Rust and exposed to user code. They handle I/O, type conversions, and common operations.
| Function | Description |
|---|---|
input(prompt?) |
Read a line from stdin; optional prompt string. |
input_int(prompt?) |
Read and parse an integer from stdin. |
input_real(prompt?) |
Read and parse a real number from stdin. |
print(value) |
Print value without trailing newline. |
print_line(value) |
Print value followed by a newline. |
| Function | Description |
|---|---|
to_string(value) |
Convert any value to its string representation. |
to_string_fixed(value, places) |
Format a number with fixed decimal places. |
to_int(value) |
Convert a string or real to an integer. |
to_real(value) |
Convert a string or integer to a real. |
| Function | Description |
|---|---|
str_concat(left, right) |
Concatenate two strings. |
join(values: List[String], sep) |
Join a list of strings with a separator. |
len(value) |
Return the length of a string, list, or tuple. |
| Function | Description |
|---|---|
open(path, mode, content) |
Open a file. Modes: r (read), w (write), a (append). |
RPython provides two monadic types for representing optional or fallible values: Maybe[T] and Result[Ok, Err].
Maybe[T]— Optional value:Just(value)orNothingResult[Ok, Err]— Success/failure:Ok(value)orErr(error)
isNothing(maybe)— returnsTrueif the value isNothingisError(result)— returnsTrueif the value isErrunwrap(value)— extracts the inner value (panics ifNothingorErr)tryUnwrap(value)— extracts or propagates errors automatically
Current limitation: The parser does not yet support
Just(),Nothing,Ok(), andErr()as expression syntax. These types exist in the AST and type system, and are used internally by the interpreter and type checker. Parser support for constructing these values from source code is planned for a future release.
ADTs can be declared with multiple constructors. Pattern matching is not yet implemented; values are constructed and passed around opaquely.
Current limitation: ADT declarations are parsed as types but cannot yet be declared as top-level statements. The syntax shown below is the planned syntax; it is not yet functional.
data Shape:
| Circle Int
| Rectangle Int Int
end
val c = Circle(5);
val r = Rectangle(3, 4);
Inline test definitions allow embedding unit tests directly in source files.
test addition_works():
val result = 2 + 2;
asserttrue(result == 4, "2 + 2 should be 4");
end
Assertions:
| Function | Description |
|---|---|
assert(cond, msg) |
Fail with msg if cond is false. |
asserttrue(cond, msg) |
Same as assert. |
assertfalse(cond, msg) |
Fail if cond is true. |
asserteq(a, b, msg) |
Fail if a != b. |
assertneq(a, b, msg) |
Fail if a == b. |
src/
├── ir/ # AST definitions (expressions, statements, types)
├── parser/ # nom-based parsers for expressions, statements, types
├── type_checker/ # Static type checking for expressions and statements
├── interpreter/ # Tree-walking interpreter and test runner
├── stdlib/ # Metabuiltins table and implementations
├── pretty_print/ # AST → readable source formatter
├── environment/ # Scoped symbol tables (variables, functions, types)
└── main.rs # Entry point (currently test-driven)
ir/ast.rs— DefinesExpression,Statement,Type,Function, and ADT structures.parser/parser_stmt.rs— Parses statements (if,while,for,def,break,continue, etc.).parser/parser_expr.rs— Parses expressions (literals, operators, function calls).type_checker/— Validates types; checks function signatures and return types.interpreter/statement_execute.rs— Executes statements; handles loops withbreak/continue.interpreter/expression_eval.rs— Evaluates expressions; dispatches metabuiltin calls.stdlib/standard_library.rs— Implements 13 metabuiltins.pretty_print/— AST-to-source formatter (see below).
The pretty_print module converts parsed AST nodes back into readable RPython source code. It is primarily used for:
- Testing round-trip correctness: parse → pretty-print → compare.
- Debugging: inspect how the parser understood your code.
- Future tooling: code formatters, IDE integrations.
The pretty printer is not invoked during normal program execution. It is available via the prelude module for programmatic use:
use r_python::prelude::{pretty, ToDoc};
// pretty(80, &statement.to_doc()) → formatted StringOptional feature flags enable performance instrumentation:
# Timing metrics
cargo run --features pp-timing --example pp_timing
# Profile counters
cargo run --features pp-profile --example pp_bench- Rust (stable toolchain)
- Cargo (bundled with Rust)
# Clone the repository
git clone https://github.com/UnBCIC-TP2/r-python.git
cd r-python
# Build the project
cargo build
# Run all tests
cargo test
# Run tests with output (useful for debugging)
cargo test -- --nocaptureThe test suite currently includes 270+ unit tests covering the parser, type checker, interpreter, and standard library.
RPython includes a CLI to execute .rpy files directly:
# Run a program
cargo run -- path/to/program.rpy
# Or build first, then run the binary
cargo build --release
./target/release/r-python path/to/program.rpyThe interpreter reads from stdin and writes to stdout, making it suitable for automated judging systems like beecrowd.
- No module system: all code lives in a single file.
- No pattern matching: ADT constructors can be built but not destructured.
- No comments: the parser does not support
#or any comment syntax. - No interactive REPL: only file-based execution is supported.
- Limited error messages: parser and type checker errors are functional but not always user-friendly.
- No tail-call optimization: deep recursion may overflow the stack.
- Maybe/Result constructors:
Just(),Nothing,Ok(),Err()cannot be parsed from source code yet. - Small standard library: only a small set of metabuiltins is available (basic I/O, conversions, simple string/list helpers); there are no rich libraries for math, dates/times, networking, etc.
- No exceptions: there is no
try/catchmechanism or exception hierarchy; errors are represented viaMaybe/Resulttypes or abort execution with an error message. - No objects or methods: there are no classes, interfaces, or method calls; programs are written with functions, lists/tuples, and algebraic data types.
- No concurrency or async: the language has no built-in support for threads, async/await, or parallel execution.
- Interpreter-only, unoptimized: execution is performed by a tree-walking interpreter without bytecode/JIT or optimization passes, so performance is below production-grade VMs/compilers.
- Minimal tooling: beyond the CLI and internal pretty printer, there is no dedicated debugger, formatter binary, or IDE integration yet.
Contributions are welcome! Please read the contribution guides before submitting issues or pull requests: