NUMEX is a small, dynamically-typed interpreted programming language implemented in Racket. It was built as a Programming Languages course project and demonstrates core concepts of language implementation: expression evaluation under an environment, first-class functions with lexical scoping, recursive closures, pairs, lists, and an optional challenge extension that computes free variables to build minimal closures.
- Features
- Language Reference
- Challenge Extension
- Project Structure
- Getting Started
- Running Tests
- Examples
- Arithmetic – integer addition, subtraction, multiplication, integer division, and negation.
- Boolean logic – short-circuit
andalso/orelse, boolean negation, and equality testing. - Conditionals –
cnd,ifnzero,ifleq,ifneq,ifmunit. - First-class functions – named recursive lambdas (
lam) with lexical scoping via closures. - Pairs & linked lists –
apair,1st,2nd,munit(empty / unit value), plus conversion helpers between NUMEX lists and Racket lists. - Higher-order functions –
numex-filterandnumex-all-gtdefined entirely inside the NUMEX language. - Challenge extension –
compute-free-varsfor static free-variable analysis andeval-exp-cfor building minimal closures that capture only the variables they actually need.
| Constructor | Racket syntax | Description |
|---|---|---|
| Integer | (num 42) |
A whole-number constant. Floats are rejected. |
| Boolean | (bool #t) / (bool #f) |
A boolean constant. |
| Unit | (munit) |
The unit value; also used as the empty-list sentinel. |
| Closure | produced by lam evaluation |
A first-class function value. |
| Pair | (apair e1 e2) |
An evaluated pair of two values. |
| Expression | Description |
|---|---|
(plus e1 e2) |
e1 + e2 — both operands must be integers. |
(minus e1 e2) |
e1 - e2 — both operands must be integers. |
(mult e1 e2) |
e1 * e2 — both operands must be integers. |
(div e1 e2) |
Integer quotient e1 / e2; raises an error if e2 = 0. |
(neg e) |
Arithmetic negation when e is an integer; logical NOT when e is a boolean. |
| Expression | Description |
|---|---|
(andalso e1 e2) |
Short-circuit AND. Returns (bool #f) without evaluating e2 if e1 is false. |
(orelse e1 e2) |
Short-circuit OR. Returns (bool #t) without evaluating e2 if e1 is true. |
(iseq e1 e2) |
Deep equality check. Returns (bool #t) if both operands are equal integers or equal booleans; (bool #f) otherwise (no error on mixed types). |
| Expression | Description |
|---|---|
(cnd e1 e2 e3) |
If e1 evaluates to (bool #t), returns e2; else returns e3. e1 must be a boolean. |
(ifnzero e1 e2 e3) |
If integer e1 ≠ 0, returns e2; else e3. |
(ifleq e1 e2 e3 e4) |
If integer e1 ≤ e2, returns e3; else e4. |
(ifneq e1 e2 e3 e4) |
If e1 ≠ e2, returns e3; else e4. (Macro built on iseq.) |
(ifmunit e1 e2 e3) |
If e1 is (munit), returns e2; else e3. (Macro.) |
(ismunit e) |
Returns (bool #t) if e evaluates to (munit). |
(isnumexlist e) |
Returns (bool #t) if e is a proper NUMEX list (chain of apair ending in munit). |
| Expression | Description |
|---|---|
(var "x") |
Look up variable "x" in the current environment. Raises an error if unbound. |
(with "x" e1 e2) |
Bind "x" to the value of e1 and evaluate e2 in the extended environment. |
(with* bindings e) |
Sequential let*-style binding. bindings is a Racket list of (cons name expr) pairs. |
| Expression | Description |
|---|---|
(lam nameopt formal body) |
Creates a lambda. nameopt is either a string (enabling recursion by self-reference) or null (anonymous). formal is the parameter name string. Evaluates to a closure. |
(apply funexp actual) |
Apply the closure produced by funexp to the value of actual. |
| Expression / Helper | Description |
|---|---|
(apair e1 e2) |
Construct a pair from two expressions. |
(1st e) |
Extract the first element of a pair. |
(2nd e) |
Extract the second element of a pair. |
(racketlist->numexlist lst) |
Convert a Racket list to a NUMEX linked list (apair chain ending with munit). |
(numexlist->racketlist lst) |
Convert a NUMEX list back to a Racket list. |
These are NUMEX values defined in project.rkt using only NUMEX constructs.
numex-filter takes a unary function f and a NUMEX list, and returns a new list containing only those elements x for which (f x) is non-zero.
; Keep only elements greater than 5
(eval-exp
(apply (apply numex-all-gt (num 5))
(racketlist->numexlist (list (num 10) (num 4) (num 15)))))
; => (apair (num 10) (apair (num 15) (munit)))numex-all-gt takes a threshold integer i and a NUMEX list, and returns only the elements strictly greater than i. It is built on top of numex-filter.
(eval-exp
(apply (apply numex-all-gt (num 5))
(racketlist->numexlist (list (num 10) (num 4) (num 5) (num 15)))))
; => (apair (num 10) (apair (num 15) (munit)))Two additional features provide optimised closure creation:
(compute-free-vars e)Traverses a NUMEX expression e and transforms every lam into a fun-challenge struct annotated with the set of free variables in its body. This information is later used to build minimal closures.
(eval-exp-c e)An alternative evaluator that uses compute-free-vars to capture only the free variables of a function in its closure, rather than the entire ambient environment. This reduces memory use and makes closure environments predictable.
; Only "x" is free; "w" is not captured
(closure-env
(eval-exp-c
(with* (list (cons "w" (num 3)) (cons "x" (num 1)) (cons "y" (num 2)))
(lam "f" "y" (plus (var "x") (var "y"))))))
; => (list (cons "x" (num 1)))NUMEX-Lang/
├── project.rkt # Language implementation (structs, evaluator, stdlib)
├── TestCases.rkt # Standard test suite (131 tests, scored out of 100)
├── CtestCases.rkt # Challenge test suite (free-vars & eval-exp-c tests)
├── grade.txt # Score written by TestCases.rkt after a run
├── grade-Challenging.txt# Score written by CtestCases.rkt after a run
└── README.md
- Racket 7.x or later.
git clone https://github.com/mahi97/NUMEX-Lang.git
cd NUMEX-LangNo additional packages are required; the project uses only the standard Racket distribution.
Run the standard test suite (writes score to grade.txt):
racket TestCases.rktRun the challenge test suite (writes score to grade-Challenging.txt):
racket CtestCases.rktBoth test runners print a summary to the terminal and write a detailed log to their respective output files.
#lang racket
(require "project.rkt")
;; Arithmetic
(eval-exp (plus (num 3) (mult (num 4) (num 5)))) ; => (num 23)
(eval-exp (div (num 10) (num 3))) ; => (num 3) (integer division)
;; Booleans & logic
(eval-exp (andalso (bool #t) (bool #f))) ; => (bool #f)
(eval-exp (neg (bool #t))) ; => (bool #f)
;; Variables & binding
(eval-exp (with "x" (num 7) (plus (var "x") (num 1)))) ; => (num 8)
;; Recursive factorial
(eval-exp
(apply
(lam "fact" "n"
(cnd (iseq (var "n") (num 0))
(num 1)
(mult (var "n") (apply (var "fact") (minus (var "n") (num 1))))))
(num 5)))
; => (num 120)
;; Pairs & lists
(eval-exp (1st (apair (num 42) (bool #t)))) ; => (num 42)
(numexlist->racketlist
(eval-exp
(apply (apply numex-all-gt (num 3))
(racketlist->numexlist (list (num 1) (num 4) (num 2) (num 5))))))
; => (list (num 4) (num 5))