A Simple Forth Interpreter in Clojure

Just for fun I sat down and started writing a Forth interpreter in Clojure. This implementation only does some simple arithmetic, it has dup and "." (print), but lacks things like control structures.

Our Forth environment has two key components,

  • Dictionary
  • Stack

Dictionary holds both primitive words, those that are implemented in Clojure and user defined words, it is a Clojure map which uses a word as its key and a Clojure function as its value. Stack is implemented using a list, words operate on stack by poping some values operating on them and pushing the result back to stack.

(ns forth
  (:refer-clojure :exclude [pop!]))

(declare forth-eval)

(defn pop! [stack]
  (let [first (first @stack)]
    (swap! stack pop)

(defn push! [stack item]
  (swap! stack conj item))

(defn next-token [stream]
  (if (. stream hasNextBigInteger)
    (. stream nextBigInteger)
    (. stream next)))

(defn init-env []
  (let [stream (java.util.Scanner. System/in)
        stack (atom '())
        dict (atom {})
        prim (fn [id f] (swap! dict assoc id f))]
    (prim ".s" #(do (println "---")
                    (doseq [s @stack] (println s))
                    (println "---")))
    (prim "cr" #(println))
    (prim "+" #(push! stack (+ (pop! stack) (pop! stack))))
    (prim "*" #(push! stack (* (pop! stack) (pop! stack))))
    (prim "/" #(let [a (pop! stack)
                     b (pop! stack)]
                 (push! stack (/ b a))))
    (prim "-" #(let [a (pop! stack)
                     b (pop! stack)]
                 (push! stack (- b a))))
    (prim "dup" #(push! stack (first @stack)))
    (prim "." #(println (pop! stack)))
    (prim ":" #(let [name (next-token stream)
                     block (loop [b [] n (next-token stream)]
                             (if (= n ";")
                               (recur (conj b n) (next-token stream))))]
                 (prim name (fn [] (doseq [w block]
                                    (forth-eval dict stack w))))))
    [dict stack stream]))

(defn forth-eval [dict stack token]
  (cond (contains? @dict token) ((@dict token))
        (number? token) (push! stack token)
        :default (println token "??")))

(defn repl [env]
  (let [[dict stack stream] env
        token (next-token stream)]
    (when (not= token "bye")
      (forth-eval dict stack token)
      (repl env))))

Forth has no explicit grammar which makes it extremely easy to parse, we tokenize the input using whitespace as a delimiter, each token is then sent to forth-eval which first checks, if the token is in the dictionary if it is, corresponding function for the word is executed if the token is not a word, we check if it is a valid number, if it is we push it to the stack, if it is neither a word or a number an error message is printed, this is repeated until the token bye is read in which case we exit.

forth=> (repl (init-env))
5 6 + 7 8 + *
3 2 1 + *
. cr
: sq dup * ;
2 sq