Building a REPL
- Step 01
The Expression Type
A REPL evaluates expressions. We start with a tiny language: integer literals, addition, let-bindings, and variables. Four constructors is all we need.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringStep 02Values and Environments
Evaluation produces values. For now, just integers. An environment maps variable names to their values using a simple association list.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)Step 03The Evaluator
Pattern matching makes the evaluator beautifully direct. Each expression form maps to a straightforward computation. Let-bindings extend the environment for the body expression.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nameStep 04A Tiny Tokenizer
To read user input, we need a tokenizer. It splits a string into meaningful chunks: numbers, identifiers, operators, and parentheses. Whitespace is consumed but not produced.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nametype token =| TNum of int| TIdent of string| TPlus | TEqual| TLParen | TRParen| TLet | TInlet is_alpha c =(c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z')|| c = '_'let is_digit c = c >= '0' && c <= '9'let tokenize input =let len = String.length input inlet pos = ref 0 inlet tokens = ref [] inwhile !pos < len dolet c = input.[!pos] inif c = ' ' || c = '\t' || c = '\n' thenincr poselse if is_digit c then beginlet start = !pos inwhile !pos < len && is_digit input.[!pos] doincr pos done;let s = String.sub input start (!pos - start) intokens := TNum (int_of_string s) :: !tokensend else if is_alpha c then beginlet start = !pos inwhile !pos < len && is_alpha input.[!pos] doincr pos done;let s = String.sub input start (!pos - start) inlet tok = match s with| "let" -> TLet | "in" -> TIn| _ -> TIdent s intokens := tok :: !tokensend else beginlet tok = match c with| '+' -> TPlus | '=' -> TEqual| '(' -> TLParen | ')' -> TRParen| _ -> failwith "unexpected char" intokens := tok :: !tokens;incr posenddone;List.rev !tokensStep 05The Parser
A recursive descent parser turns tokens into our expression AST. It handles operator precedence naturally: addition is parsed as a left-associative chain of atoms.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nametype token =| TNum of int | TIdent of string| TPlus | TEqual| TLParen | TRParen| TLet | TInlet is_alpha c =(c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z') || c = '_'let is_digit c = c >= '0' && c <= '9'let tokenize input =let len = String.length input inlet pos = ref 0 inlet tokens = ref [] inwhile !pos < len dolet c = input.[!pos] inif c = ' ' || c = '\t' || c = '\n' thenincr poselse if is_digit c then beginlet start = !pos inwhile !pos < len && is_digit input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) intokens := TNum (int_of_string s) :: !tokensend else if is_alpha c then beginlet start = !pos inwhile !pos < len && is_alpha input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) inlet tok = match s with| "let" -> TLet | "in" -> TIn| _ -> TIdent s intokens := tok :: !tokensend else beginlet tok = match c with| '+' -> TPlus | '=' -> TEqual| '(' -> TLParen | ')' -> TRParen| _ -> failwith "unexpected char" intokens := tok :: !tokens; incr posenddone;List.rev !tokenslet parse tokens =let toks = ref tokens inlet next () =match !toks with| [] -> failwith "unexpected end"| t :: rest -> toks := rest; t inlet peek () =match !toks with [] -> None | t :: _ -> Some t inlet rec parse_expr () =let left = parse_atom () inparse_add leftand parse_add left =match peek () with| Some TPlus ->ignore (next ());let right = parse_atom () inparse_add (Add (left, right))| _ -> leftand parse_atom () =match next () with| TNum n -> Lit n| TIdent s -> Var s| TLParen ->let e = parse_expr () inignore (next ()); e| TLet ->let (TIdent name) = next () inignore (next ());let rhs = parse_expr () inignore (next ());let body = parse_expr () inLet (name, rhs, body)| _ -> failwith "unexpected token" inparse_expr ()Step 06The Read-Eval-Print Loop
Now we connect all the pieces. The REPL reads a line, tokenizes it, parses the tokens, evaluates the expression, and prints the result. A persistent environment accumulates bindings across interactions.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nametype token =| TNum of int | TIdent of string| TPlus | TEqual| TLParen | TRParen| TLet | TInlet is_alpha c =(c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z') || c = '_'let is_digit c = c >= '0' && c <= '9'let tokenize input =let len = String.length input inlet pos = ref 0 inlet tokens = ref [] inwhile !pos < len dolet c = input.[!pos] inif c = ' ' || c = '\t' || c = '\n' thenincr poselse if is_digit c then beginlet start = !pos inwhile !pos < len && is_digit input.[!pos]do incr pos done;tokens := TNum (int_of_string(String.sub input start(!pos - start))) :: !tokensend else if is_alpha c then beginlet start = !pos inwhile !pos < len && is_alpha input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) intokens := (match s with| "let" -> TLet | "in" -> TIn| _ -> TIdent s) :: !tokensend else begintokens := (match c with| '+' -> TPlus | '=' -> TEqual| '(' -> TLParen | ')' -> TRParen| _ -> failwith "unexpected") :: !tokens;incr posenddone; List.rev !tokenslet parse tokens =let toks = ref tokens inlet next () = match !toks with| [] -> failwith "end"| t :: r -> toks := r; t inlet peek () = match !toks with| [] -> None | t :: _ -> Some t inlet rec expr () =let l = atom () in add land add left = match peek () with| Some TPlus ->ignore (next ());add (Add (left, atom ()))| _ -> leftand atom () = match next () with| TNum n -> Lit n| TIdent s -> Var s| TLParen ->let e = expr () inignore (next ()); e| TLet ->let (TIdent name) = next () inignore (next ());let rhs = expr () inignore (next ());Let (name, rhs, expr ())| _ -> failwith "unexpected" inexpr ()let print_value = function| Int n -> Printf.printf "=> %d\n" nlet repl () =let env = ref empty_env intry while true doprint_string "> ";let line = input_line stdin inlet tokens = tokenize line inlet ast = parse tokens inlet result = eval !env ast inprint_value resultdone with End_of_file ->print_endline "Goodbye."let () = repl ()main.ml 1 / 6type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringStep 01The Expression Type
A REPL evaluates expressions. We start with a tiny language: integer literals, addition, let-bindings, and variables. Four constructors is all we need.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringStep 02Values and Environments
Evaluation produces values. For now, just integers. An environment maps variable names to their values using a simple association list.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)Step 03The Evaluator
Pattern matching makes the evaluator beautifully direct. Each expression form maps to a straightforward computation. Let-bindings extend the environment for the body expression.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nameStep 04A Tiny Tokenizer
To read user input, we need a tokenizer. It splits a string into meaningful chunks: numbers, identifiers, operators, and parentheses. Whitespace is consumed but not produced.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nametype token =| TNum of int| TIdent of string| TPlus | TEqual| TLParen | TRParen| TLet | TInlet is_alpha c =(c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z')|| c = '_'let is_digit c = c >= '0' && c <= '9'let tokenize input =let len = String.length input inlet pos = ref 0 inlet tokens = ref [] inwhile !pos < len dolet c = input.[!pos] inif c = ' ' || c = '\t' || c = '\n' thenincr poselse if is_digit c then beginlet start = !pos inwhile !pos < len && is_digit input.[!pos] doincr pos done;let s = String.sub input start (!pos - start) intokens := TNum (int_of_string s) :: !tokensend else if is_alpha c then beginlet start = !pos inwhile !pos < len && is_alpha input.[!pos] doincr pos done;let s = String.sub input start (!pos - start) inlet tok = match s with| "let" -> TLet | "in" -> TIn| _ -> TIdent s intokens := tok :: !tokensend else beginlet tok = match c with| '+' -> TPlus | '=' -> TEqual| '(' -> TLParen | ')' -> TRParen| _ -> failwith "unexpected char" intokens := tok :: !tokens;incr posenddone;List.rev !tokensStep 05The Parser
A recursive descent parser turns tokens into our expression AST. It handles operator precedence naturally: addition is parsed as a left-associative chain of atoms.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nametype token =| TNum of int| TIdent of string| TNum of int | TIdent of string| TPlus | TEqual| TLParen | TRParen| TLet | TInlet is_alpha c =(c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z')|| c = '_'|| (c >= 'A' && c <= 'Z') || c = '_'let is_digit c = c >= '0' && c <= '9'let tokenize input =let len = String.length input inlet pos = ref 0 inlet tokens = ref [] inwhile !pos < len dolet c = input.[!pos] inif c = ' ' || c = '\t' || c = '\n' thenincr poselse if is_digit c then beginlet start = !pos inwhile !pos < len && is_digit input.[!pos] doincr pos done;let s = String.sub input start (!pos - start) inwhile !pos < len && is_digit input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) intokens := TNum (int_of_string s) :: !tokensend else if is_alpha c then beginlet start = !pos inwhile !pos < len && is_alpha input.[!pos] doincr pos done;let s = String.sub input start (!pos - start) inwhile !pos < len && is_alpha input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) inlet tok = match s with| "let" -> TLet | "in" -> TIn| _ -> TIdent s intokens := tok :: !tokensend else beginlet tok = match c with| '+' -> TPlus | '=' -> TEqual| '(' -> TLParen | ')' -> TRParen| _ -> failwith "unexpected char" intokens := tok :: !tokens;incr postokens := tok :: !tokens; incr posenddone;List.rev !tokenslet parse tokens =let toks = ref tokens inlet next () =match !toks with| [] -> failwith "unexpected end"| t :: rest -> toks := rest; t inlet peek () =match !toks with [] -> None | t :: _ -> Some t inlet rec parse_expr () =let left = parse_atom () inparse_add leftand parse_add left =match peek () with| Some TPlus ->ignore (next ());let right = parse_atom () inparse_add (Add (left, right))| _ -> leftand parse_atom () =match next () with| TNum n -> Lit n| TIdent s -> Var s| TLParen ->let e = parse_expr () inignore (next ()); e| TLet ->let (TIdent name) = next () inignore (next ());let rhs = parse_expr () inignore (next ());let body = parse_expr () inLet (name, rhs, body)| _ -> failwith "unexpected token" inparse_expr ()Step 06The Read-Eval-Print Loop
Now we connect all the pieces. The REPL reads a line, tokenizes it, parses the tokens, evaluates the expression, and prints the result. A persistent environment accumulates bindings across interactions.
type expr =| Lit of int| Add of expr * expr| Let of string * expr * expr| Var of stringtype value = Int of inttype env = (string * value) listlet empty_env : env = []let extend env name v = (name, v) :: envlet lookup env name =match List.assoc_opt name env with| Some v -> v| None -> failwith ("unbound: " ^ name)let rec eval env = function| Lit n -> Int n| Add (a, b) ->let (Int x) = eval env a inlet (Int y) = eval env b inInt (x + y)| Let (name, rhs, body) ->let v = eval env rhs ineval (extend env name v) body| Var name -> lookup env nametype token =| TNum of int | TIdent of string| TPlus | TEqual| TLParen | TRParen| TLet | TInlet is_alpha c =(c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z') || c = '_'let is_digit c = c >= '0' && c <= '9'let tokenize input =let len = String.length input inlet pos = ref 0 inlet tokens = ref [] inwhile !pos < len dolet c = input.[!pos] inif c = ' ' || c = '\t' || c = '\n' thenincr poselse if is_digit c then beginlet start = !pos inwhile !pos < len && is_digit input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) intokens := TNum (int_of_string s) :: !tokenstokens := TNum (int_of_string(String.sub input start(!pos - start))) :: !tokensend else if is_alpha c then beginlet start = !pos inwhile !pos < len && is_alpha input.[!pos]do incr pos done;let s = String.sub input start(!pos - start) inlet tok = match s withtokens := (match s with| "let" -> TLet | "in" -> TIn| _ -> TIdent s intokens := tok :: !tokens| _ -> TIdent s) :: !tokensend else beginlet tok = match c withtokens := (match c with| '+' -> TPlus | '=' -> TEqual| '(' -> TLParen | ')' -> TRParen| _ -> failwith "unexpected char" intokens := tok :: !tokens; incr pos| _ -> failwith "unexpected") :: !tokens;incr posenddone;List.rev !tokensdone; List.rev !tokenslet parse tokens =let toks = ref tokens inlet next () =match !toks with| [] -> failwith "unexpected end"| t :: rest -> toks := rest; t inlet peek () =match !toks with [] -> None | t :: _ -> Some t inlet rec parse_expr () =let left = parse_atom () inparse_add leftand parse_add left =match peek () withlet next () = match !toks with| [] -> failwith "end"| t :: r -> toks := r; t inlet peek () = match !toks with| [] -> None | t :: _ -> Some t inlet rec expr () =let l = atom () in add land add left = match peek () with| Some TPlus ->ignore (next ());let right = parse_atom () inparse_add (Add (left, right))add (Add (left, atom ()))| _ -> leftand parse_atom () =match next () withand atom () = match next () with| TNum n -> Lit n| TIdent s -> Var s| TLParen ->let e = parse_expr () inlet e = expr () inignore (next ()); e| TLet ->let (TIdent name) = next () inignore (next ());let rhs = parse_expr () inlet rhs = expr () inignore (next ());let body = parse_expr () inLet (name, rhs, body)| _ -> failwith "unexpected token" inparse_expr ()Let (name, rhs, expr ())| _ -> failwith "unexpected" inexpr ()let print_value = function| Int n -> Printf.printf "=> %d\n" nlet repl () =let env = ref empty_env intry while true doprint_string "> ";let line = input_line stdin inlet tokens = tokenize line inlet ast = parse tokens inlet result = eval !env ast inprint_value resultdone with End_of_file ->print_endline "Goodbye."let () = repl ()Playground