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) first)) (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 ";") b (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 + * . 165 cr 3 2 1 + * . cr 9 : sq dup * ; 2 sq . 4 bye nil forth=>