A tiny functional programming language implemented in TypeScript. MiniML uses S-expression syntax, a tree-walking interpreter, and lexical closures. It is built in two phases: a core interpreter that handles arithmetic, comparisons, conditionals, and let bindings, followed by first-class functions, a standard library, and an interactive REPL.
This project grew out of a set of standalone TypeScript problems covering cumulative sums, linear search, Peano naturals, sequence generation, expression trees, environment-based evaluation, higher-order functions, and tree traversal. The problems were part of a set for a programming languages class. Each problem teaches an isolated concept, but real software rarely uses concepts in isolation.
A language interpreter is one of the few projects where all of these ideas have a natural home:
| Problem | Concept | Role in MiniML |
|---|---|---|
| 1 - Cumulative Sum | Array accumulation | cumsum and sum built-ins |
| 2 - Linear Search | Indexed scan | Environment lookup algorithm |
| 3 - Peano Naturals | Recursive data, base/recursive cases | nat+, nat->int, int->nat built-ins |
| 4 - Sequence Generator | Loop-based generation | range built-in |
| 6a - Expression Types | Class hierarchies, instanceof |
The AST node classes |
| 7a - Variable Expressions | Extending a type hierarchy | Var node for variable references |
| 8 - Filter Map | Higher-order functions, generics | filtermap, map, filter built-ins |
| 10 - Max Tree Depth | Recursive tree traversal | depth built-in |
Problems 5 (string append map), 6b (expression evaluator), 7b (environment evaluator), and 9 (closures/makesay) informed the design but are not included as standalone functions. Their concepts are demonstrated directly by the interpreter itself: the evaluator generalizes Problems 6b and 7b, and lambda evaluation generalizes the closure pattern from Problem 9.
Both phases are implemented. Phase 1 provides the core interpreter (arithmetic, comparisons, conditionals, let). Phase 2 adds first-class functions, a standard library, and an interactive REPL with file loading.
- ✅ Tokenizer -- breaks source strings into tokens, handles whitespace, comments, negative literals
- ✅ Recursive-descent parser -- builds an AST from all supported forms
- ✅ Tree-walking evaluator -- integer literals, arithmetic (
+,-,*), comparisons (<,=,>),ifconditionals,letbindings with lexical scoping - ✅ Environment-based variable resolution with proper shadowing
- ✅
lambdaexpressions and lexical closures - ✅ Function application (
Appnode) with automatic partial application for built-ins - ✅
definefor top-level bindings - ✅
closure,builtin, andlistvalue types - ✅ Standard library built-ins (
sum,cumsum,range,filtermap,map,filter,find,length,depth,nat+,nat->int,int->nat) - ✅ Pretty printing of closures, built-ins, and lists
- ✅ Interactive REPL with
:loadand:quitcommands - ✅
.mmlfile loading from the command line or from the REPL - ✅ Example programs in
examples/
npm install
npx tsc
node dist/miniml.js # start the REPL
node dist/miniml.js examples/closures.mml # load a file then drop into the REPLIn the REPL:
miniml> (+ 1 (* 2 3))
7
miniml> (define add (lambda (x) (lambda (y) (+ x y))))
defined add
miniml> ((add 3) 4)
7
miniml> (sum (range 1 1 100))
5050
miniml> :load examples/closures.mml
miniml> :quit
MiniML supports:
- Integer literals and arithmetic (
+,-,*) - Comparisons (
<,=,>) producing booleans ifconditionals with then/else branchesletbindings with lexical scoping and shadowinglambdaexpressions with single parameters (multi-argument via currying)- Function application with automatic currying for built-ins
definefor top-level bindings in the REPL and files- Comments starting with
;
; Basic arithmetic
(+ 1 (* 2 3)) ; => 7
; Let bindings with variable shadowing
(let x 10
(let y (+ x 3)
(if (< y 15) (* y 2) 0))) ; => 26
; Curried addition
(define add (lambda (x) (lambda (y) (+ x y))))
((add 3) 4) ; => 7
; Built-in over a range
(sum (range 1 1 100)) ; => 5050
; Higher-order functions on lists
(filtermap (lambda (x) (< x 5))
(lambda (x) (* x x))
(range 1 1 10)) ; => (1 4 9 16)
; Partial application of a built-in
(define myrange (range 1))
(myrange 1 10) ; => (1 2 3 4 5 6 7 8 9 10)The entire interpreter lives in a single file, miniml.ts, organized into sections that depend only on sections above them.
Source string --> tokenize() --> Token[] --> parse() --> Expr --> evaluateExpr() --> Value --> printValue() --> string
-
Type Definitions - Peano naturals (
Succ,Nat) and binary trees (TLeaf,TNode,Tree) used by helper functions and built-ins. -
AST Node Definitions - A class hierarchy rooted at
Expr. Leaf nodes (Literal,Var), binary operators (Plus,Minus,Mul,LessThan,Equals,GreaterThan), control flow (If,Let), and functions (Lambda,App,Define). -
Value and Environment Types -
Valueis a tagged union withnum,bool,closure,builtin, andlistvariants.Envis an association list ([string, Value][]) where lookup scans front-to-back so newer bindings shadow older ones. -
Helper Functions - Problem-set functions that back the standard library:
cumulative_sum,search,add/to_int/from_int,sequence,filtermap,maxdepth. -
Tokenizer - Converts a source string into a flat array of tokens (
lparen,rparen,number,symbol). Strips whitespace and;comments. -
Parser - Recursive-descent parser that converts tokens into an AST. Special forms (
+,-,*,<,=,>,if,let,lambda,define) are recognized by keyword; any other head position is treated as function application. -
Evaluator - Tree-walking evaluator (
evaluateExpr) that recurses over the AST, threading an environment throughlet,lambda, andAppforms. -
Apply helper - Dispatches closures vs. built-ins. Built-ins collect arguments one at a time and invoke when arity is satisfied (automatic partial application).
-
Pretty printer - Prints values: numbers, booleans (
#t/#f),<closure>,<builtin:name[k/n]>, and parenthesized list notation. -
Standard library - Wraps the helper functions as callable MiniML built-ins and seeds them into the initial environment.
-
REPL and file loader - Interactive prompt with
:load <file>and:quitcommands. Files passed on the command line are loaded in order, then the REPL starts with those definitions in scope.
S-expressions keep the tokenizer under 80 lines and the parser under 150. An infix parser with operator precedence would be several times larger for identical semantics.
Tagged unions for values make the evaluator's type checks explicit. Each branch of the instanceof chain handles one case, mirroring pattern matching in functional languages.
Association-list environments are the simplest scoping representation. Prepend to shadow, scan to look up. This makes the scoping model transparent and easy to debug.
Single-argument lambdas avoid multi-argument parsing complexity. The tradeoff is verbosity, but it makes closure semantics maximally clear: each application extends the environment by exactly one binding.
Automatic partial application for built-ins lets (range 1) return a new built-in still expecting two arguments, matching the currying model used by user-defined lambdas.
- Node.js (v14 or later)
- npm
npm install
npx tsc
node dist/miniml.js| Built-in | Arity | Description |
|---|---|---|
sum |
1 | Sum of a number list |
cumsum |
1 | Running totals of a number list |
find |
2 | Index of first occurrence, or -1 |
range |
3 | Arithmetic progression (spacing, low, high) |
filtermap |
3 | Filter then map in one pass |
map |
2 | Apply function to each element |
filter |
2 | Keep elements passing a predicate |
length |
1 | Number of elements in a list |
nat+ |
2 | Addition via Peano naturals |
nat->int |
1 | Peano natural to integer |
int->nat |
1 | Integer to Peano natural |
depth |
1 | Max depth of a nested list as a tree |
true |
- | Boolean constant |
false |
- | Boolean constant |
All multi-argument built-ins support partial application. (range 1) returns a function that still needs two arguments.
Project4/
miniml.ts -- single-file implementation
tsconfig.json -- TypeScript compiler configuration
package.json -- project metadata and dependencies
examples/ -- example .mml programs
MiniML reports errors at two levels:
- Parse errors identify the token position where parsing failed (missing parentheses, unexpected tokens, malformed special forms).
- Runtime errors identify the specific type violation (arithmetic on non-numbers, unbound variables, applying a non-function,
ifcondition not a boolean).
Errors in the REPL are caught and printed without terminating the session. When loading a file, a failing expression is reported but subsequent expressions still run.
- Single-argument lambdas only. Multi-argument functions must be written as nested lambdas (currying). This is a deliberate simplification that keeps closure semantics explicit.
- No strings. MiniML has integers and booleans. There is no string type or string operations.
- No mutation. All bindings are immutable. There are no assignment operators or mutable references.
- No recursion without workarounds. There is no
letrecor named recursion. Top-leveldefinedoes not self-reference at definition time, so recursive functions require a fixpoint combinator. - No tail-call optimization. Deeply recursive programs will overflow the JavaScript call stack.
- No static type checking. Type errors (such as
(+ true 3)) are caught at runtime, not at parse time. The AST accepts anyExprin any position. - Linear environment lookup. Variable resolution scans the association list front-to-back. This is O(n) per lookup, which is fine for small programs but would not scale to thousands of bindings.
- Lists are produced only by built-ins. There is no literal list syntax in MiniML source; lists come from
range,cumsum,map,filter, andfiltermap.