       (eval body (cons (zipmap argnames args) scope)))))
 
 (defn iffn [node scope]
-  (let [[_ test then else] node]
+  (let [[_ predicate then else] node]
     (cond
-      (eval test scope) (eval then scope)
+      (eval predicate scope) (eval then scope)
       :else (eval else scope))))
+
+(defn letfn [node scope]
+  (let [[_ assignments body] node]
+    (if (empty? assignments)
+      (eval body scope)
+      (let [[avar aval & remaining] assignments]
+        (eval `((~'fn ~[avar]
+                  (~'let ~(vec remaining) ~body)) ~aval) scope)))))
 
 (def specials
   {'fn lambdafn
    'quote (fn [n s] (second n))
-   'if iffn })
+   'if iffn
+   'let letfn})
 
 (def specialfns (set (vals specials)))
 

(ns cavm.query.expression
  (:require [clojure.edn :as edn])
  (:require [honeysql.types :as hsqltypes])
  (:refer-clojure :exclude  [eval]))

(def eval)


(defn eval [node scope]
  (cond
    (symbol? node) ((first (filter #(contains? % node) scope)) node)
    (seq? node) (fn-node node scope)
    (instance? Number node) (double node)
    (keyword? node) node
    (string? node) node
    ; due to weird (empty <clojure.lang.MapEntry>) behavior, we have
    ; to handle map? separate from coll?
    (map? node) (into {} (mapv #(eval (vec %) scope) node))
    (coll? node) (into (empty node) (mapv #(eval % scope) node))))

(defn expression [exp & globals]
  (eval (edn/read-string {:readers {'sql/call hsqltypes/read-sql-call}} exp) (cons specials globals)))
