An interpreter for the array language G — a simplified dialect of J (derived from APL) — built from scratch with Python, ANTLR and NumPy.
miniJ is a complete interpreter for G, a functional array-programming language modelled on J (the ASCII successor of APL, whose creator Kenneth Iverson won the Turing Award). Like APL/J, G operates on whole vectors at once, evaluates expressions right-to-left, and lets you build new functions by composing existing ones.
I implemented the full language pipeline end to end — lexing → parsing → AST construction → semantic analysis → evaluation — designing the grammar, the tree-walking interpreter and the execution engine myself.
parell =: 0 = ] @: 2 | ] NB. define "is even" by composing functions
parell i. 6 NB. result: 1 0 1 0 1 0 (0..5, 1 where even)🎓 Built for the Languages & Programming Paradigms (LP) course of the Bachelor's Degree in Informatics Engineering at UPC – Universitat Politècnica de Catalunya (2024–2025). Final grade: 10/10.
- A real language front-end, not a toy calculator. Tokenizer, an LL(*) grammar, AST, symbol tables and an evaluator — the same architecture used by production interpreters.
- Clean, layered architecture. Parsing, tree traversal and numeric execution live in three decoupled modules, so the numeric backend (NumPy) could be swapped without touching the interpreter logic.
- Non-trivial language features. First-class user-defined functions, function composition (
@:), right-to-left evaluation, scalar/vector broadcasting, and lazy evaluation of function bodies. - Robust error handling. Lexical, syntactic and semantic errors are caught and reported clearly instead of crashing — including length-mismatch checks implemented via Python closures.
- Tested. Six curated, reproducible test suites with golden
.outfiles and a one-command runner.
| Category | Operators / features |
|---|---|
| Arithmetic | + - * % (divide) ^ (power) | (modulo) |
| Relational / boolean | > < >= <= = <> |
| List manipulation | , (concat) # (length / filter) { (index) i. (iota) |
| Adverbs | ~ (reflex), / (fold/insert), : (variants like *: square) |
| Variables & functions | =: assignment, user-defined functions, composition with @: |
| Evaluation | right-to-left, scalar↔vector broadcasting, lazy function bodies |
1 + 1 2 3 NB. 2 3 4 — scalar broadcasts over a vector
5 * 2 + 3 NB. 25 — right-to-left evaluation
+/ 1 2 3 NB. 6 — fold (+) over the list
1 0 1 0 # 1 2 3 4 NB. 1 3 — boolean filter
square =: *:
square 1 + i. 3 NB. 1 4 9 — compose iota, increment and squaring ┌───────────────┐
source .j ──▶│ g.py │ entry point: lexing + parsing (ANTLR)
│ (driver) │ reports lexical/syntactic errors
└──────┬────────┘
│ AST
┌──────▼────────┐
│ visitor.py │ AST visitor: symbol tables (vars & funcs),
│ (interpreter) │ operand stack, lazy function evaluation
└──────┬────────┘
│ operations
┌──────▼────────┐
│ motor_g.py │ execution engine: NumPy-backed vector ops,
│ (engine) │ encode/decode J literals, semantic checks
└───────────────┘
| File | Responsibility |
|---|---|
g.g4 |
ANTLR grammar for G (right-associative expressions, functions, operators as context-sensitive rules) |
g.py |
Driver / entry point: runs lexical & syntactic analysis, builds the AST, delegates evaluation |
visitor.py |
Tree-walking interpreter: symbol tables, an operand stack, and lazy evaluation of function bodies |
motor_g.py |
Execution engine encapsulating all numeric logic on NumPy vectors and semantic validation |
Selected design decisions
- Decoupled engine. Interpreter logic (
visitor.py) is separated from numeric operations (motor_g.py), improving maintainability and making the numeric backend replaceable. - Stack-based evaluation. Operands live on an explicit stack, which makes multi-valued operands and user functions uniform and easy to extend.
- Lazy functions. Function definitions store their AST rather than a result, so the body is evaluated only when an argument arrives — naturally yielding lazy evaluation.
- Closures for safety checks. Binary operators that require matching operand lengths return a closure that validates sizes at call time, when the operands are finally known.
Requires Python 3.10, NumPy and ANTLR4.
# 1. set up an environment
python -m venv env && source env/bin/activate
pip install -r requirements.txt # numpy, antlr4-tools, antlr4-python3-runtime
# 2. generate the parser from the grammar
make
# 3. run the interpreter on a G program
python g.py tests/exemples.jThe repository ships with six reproducible test suites (arithmetic, boolean/relational, functions & variables, the assignment examples, and lexical/syntactic & semantic errors), each paired with a golden .out file:
./run-tests.shThe script runs every tests/*.j program and diffs the output against its expected result — no differences means everything passes.
g.g4 ANTLR grammar for the G language
g.py interpreter entry point (lexing + parsing)
visitor.py AST visitor / tree-walking interpreter
motor_g.py NumPy-backed execution engine
Makefile generates the ANTLR parser
run-tests.sh runs all test suites against golden outputs
tests/ .j programs paired with expected .out files
enunciat.md original assignment statement (Catalan)
entrega.zip the exact archive submitted for evaluation
- Course: GEI-LP, UPC (2024–2025 Q2). Evaluated by Prof. Edelmira Pasarella — grade 10/10.
- Original assignment: gebakx/lp-mini-j (Catalan).
- 📄 The original project documentation, written in Catalan, is preserved in
README.ca.md.
