Stack-clj is a stack-based Domain Specific Language built in Clojure.
The entry point to using stack-clj is a (possibly anonymous) stackfn. Each stackfn has it’s own stack (clj list) of intermediate results and a stack of symbol tables (each represented as clj maps). The symbol table stack always maintains at least one map at the bottom of the stack, representing the “global” symbol table for the enclosing stackfn. The first map on the symbol table stack is always the most “local” symbol table, thus if a local scope is defined within a stackfn, this receives its own symbol table which is pushed onto the top of the symbol table stack.
Stack-clj supports 4 constant expressions, all with the same syntax as Clojure: strings, booleans (true|false
, nil
isn’t supported as a constant expression), keywords, and numbers.
Variables in stack-clj are symbols prefixed with !
. It is advised that variables do not end with +
as this is used to indicate assignment, though doing so is valid (in which case a variable !C+
is assigned to with !C++
).
A new stackfn
may be declared one of two ways, either globally with
(defstackfn name-of-new-stackfn [& !args] ...)
or locally with
(stackfn name? [& !args] ...)
. Either form may be declared as multi-arity and may take any number of arguments (including 0 or variadic). The syntax for declaring parameters is the same as Clojure’s syntax for doing so, except that each parameter name must begin with !
(as they are stack variables). New local stackfn
can be declared as standalone stack expressions, thus stackfns can take stackfns as arguments and return stackfns. Stackfns may call other stackfns or themselves (recursively). Stackfns return the top of their stacks (or nil if the stack is empty) upon completion.
To declare a local scope inside a stackfn, the local
keyword is used much like the let
keyword in Clojure, though with restricted bindings. The symbol (left-hand side of a binding pair) must be a valid stack variable, and may not be destructured. The right-hand side of a binding pair must either be a stack constant or a stackfn.
Clojure expressions are valid in the aforementioned locations. Clojure functions are also valid as the first argument to invoke>
. invoke>
takes a Clojure function, or a predefined stack variable as its first argument (symbols generally work here since symbols are invokable in Clojure, thus it is important that the user predefines stack variables intended to be invoked as functions in order to avoid potentially confusing errors or behavior). invoke>
takes either a non-negative number n
as its second argument or one of the keywords, :top
or :all
and pops n
elements from the stack to be used as arguments in the former case. If :top
is specified then the top of the stack is popped and whatever value is specified at the top of the stack is then taken as n
. If :all
is specified (count stack)
is taken as n
.
((stackfn [] 1 2 3 (invoke> list 3))) ;; => (3 2 1)
((stackfn [] 1 2 3 (invoke> list :all))) ;; => (3 2 1)
((stackfn [] 1 2 3 3 (invoke> list :top))) ;; => (3 2 1)
If the stack has fewer elements than specified by n
, then invoke>
will throw an IllegalArgumentException
.
Branching is handled via (if> ... else>? ...)
. This expression pops the top of the stack t
and executes all expressions that come after if>
and before either else>
or the end of if>
(specified by the closing right parenthesis) iff t
is truthy (i.e. not false or nil), otherwise all expressions after else>
are evaluated (if an else>
branch is specified as an if>
expression may have either 0 or 1 associated else>
branches).
Stack-clj supports 8 types of loop, while>
, until>
, foreach>
, doseq>
, and the <do ...
forms of each of the first four (e.g. <do ... while>
or <do ... foreach>
). continue
may be used to short-circuit evaluation of a particular loop iteration, while break
may be used to do the same and also exit the loop without any further evaluation of the loop.
while>
comes in two forms:
while> pred body
and
while> [new-top pred] body
pred
is the predicate function to be tested at the beginning of each iteration. This can be any clojure function or can be a predefined stackfn variable, it cannot be a stackfn or use the #(...)
form, however (though the more verbose (fn [...] ...)
anonymous function syntax is fine). Once pred
evaluates to false
, the loop exits. In the second form new-top
is a constant value to be pushed onto the stack before the first time pred
is evaluated. body
is a list of stackfn
exprs to be evaluated each iteration.
(while> [3 (comp not zero?)]
(invoke> dec 1))
;; 3 is pushed onto stack, then (not (zero? 3)) => true, so
;; (invoke> dec 1) is evaluated, popping 3 and pushing 2 onto the stack,
;; then (not (zero? 2)) => true
;; then (not (zero? 1)) => true
;; finally (not (zero? 0)) => false, so loop exits with 0 on top of the
;; stack
(while> (partial not= "q")
"Enter 'q' to quit: "
(invoke> prn 1)
<pop> ;; Pop returns nil from prn call
input>)
;; Loop until user inputs 'q' (leaving intermediate responses and
;; final response on stack)
until>
is equivalent to while>
except that the loop exits when pred
evaluates to true
.
(until> ["str" (fn [s] > (count s) 11)]
<dup>
(invoke> str 2))
;; Concatenate "str" with itself until its length is 12 or more
10
(invoke> (comp str rand-int) 1)
!num+
false
!guessed?+
(until> !guessed?
"Guess a number between 0 and 9"
(invoke> println 1)
<pop>
!num
input>
(invoke> = 2)
!guessed?+)
;; Prompts user to guess random single-digit number until they guess
;; correctly
foreach>
takes a collection literal of constants or (non-nested) predefined variables (e.g. [1 "two" !three]
) or an expression that evaluates to a collection (e.g. (range 10)
) and iterates through each element, pushing the current element onto the stack, then evaluating the body of the loop.
(foreach> (range 10)
(invoke> identity 1))
;; push 0-9 onto stack in reverse order
(foreach> (map inc (filter odd? (range 35)))
(invoke> str 1))
;; push twice the value of every odd number between 0-34 onto stack in reverse order, as strings
3
!three+
(invoke> (fn [] {}) 0) ;; Return empty map
(foreach> [0 "1" 2 !three :four true]
<dup>
(invoke> (comp keyword str) 1)
(invoke> hash-map 2)
!map
(invoke> conj 2))
;; Iteratively build a map based on the vector of constants/stack-variables
;; passed as the coll arg to foreach>
doseq>
is equivalent to foreach>
except that it automatically pops the final result of the body of expressions in the loop each iteration.
(doseq> (range 10)
(invoke> prn 1))
;; Prints 0-9 without leaving results on stack
(foreach> (range 10)
(invoke> prn 1))
;; Prints 0-9, leaving 10 nils on top of the stack
(doseq> (range 10)
<dup>
(invoke> inc 1)
(invoke> prn 1))
;; Prints 1-10, leaving 0-9 on top of the stack
Generally speaking, I/O can be used via the first argument to invoke>
, much in the same way as in Clojure, though the expression input>
is available as syntactic sugar for (invoke> read-line 0)
.
Much like Clojure includes Java Interop, so does stack-clj. Java methods may be invoked via one of three stack-clj expressions:
(.static> class-name method-name & args?)
takes a class-name, (including forms like (new java.util.Date)
, java.util.Calendar
, or (java.util.GregorianCalendar.)
), a method-name, and any number of arguments to be applied to the method.
(.var> var method-name & args?)
takes a predefined stack variable that maps to a Java Object, a method-name, and any number of arguments, which may also be specified as predefined stack variables.
Other built-in stack-clj expressions include
<dup>
pushes the top of the stack onto the top of the stack, effectively duplicating the top of the stack<prn-state>
prints the current stack and symbol tables to the console<pop>
pops the top of the stack. Throws an error when evaluated on an empty stack.
Example stack-clj programs (specified as global stackfns) are provided in test/dsl/core_test.clj
. For convenience, a sample of these are copied here.
(defstackfn tic-tac-toe
[]
(local [!prn-board (stackfn [!board]
(doseq> !board
" "
(invoke> #(apply str (repeat 8 %)) 1)
(invoke> println 2)))
!construct-init-board (stackfn []
(foreach>
(reverse (partition 3 (range 10)))
(invoke> (partial apply vector) 1))
(invoke> vector 3))
!get-available-spaces (stackfn [!board]
!board
(invoke> (comp
(partial filter
number?)
flatten) 1))
!update-game-board (stackfn [!board !x? !idx]
!x?
(if>
"x"
else>
"o")
!idx
!board
(invoke> (fn [board idx mark]
(assoc-in board
[(quot idx 3) (mod idx 3)]
mark)) 3))
!ai-turn (stackfn [!available-spaces !board]
!available-spaces
(invoke> count 1)
(invoke> rand-int 1)
!available-spaces
(invoke> nth 2)
false
!board
(invoke> !update-game-board 3))
!column? (stackfn [!n0 !n1 !n2]
!n2 !n1 !n0
(invoke> (fn [n0 n1 n2]
(apply = (map #(mod % 3)
(list n0 n1 n2))))
3))
!row? (stackfn [!n0 !n1 !n2]
(invoke> (fn [] (list 0 1 2)) 0)
(invoke> (fn [] (list 3 4 5)) 0)
(invoke> (fn [] (list 6 7 8)) 0)
!n2 !n1 !n0
(invoke> list 3)
(invoke> sort 1)
(invoke> (fn [ns case0 case1 case2]
(or (= ns case0)
(= ns case1)
(= ns case2)))
4))
!diagonal? (stackfn [!n0 !n1 !n2]
(invoke> (fn [] (list 0 4 8)) 0)
(invoke> (fn [] (list 2 4 6)) 0)
!n2 !n1 !n0
(invoke> list 3)
(invoke> sort 1)
(invoke> (fn [ns case0 case1]
(or (= ns case0)
(= ns case1))) 3))
!win? (stackfn [!ns]
;; Unpack each element of !ns onto stack
(foreach> !ns
(invoke> identity 1))
(invoke> !diagonal? 3)
(if>
true
else>
(foreach> !ns
(invoke> identity 1))
(invoke> !row? 3)
(if>
true
else>
(foreach> !ns
(invoke> identity 1))
(invoke> !column? 3))))
;; Return indices of xs placed on gameboard
!get-xs (stackfn [!board]
!board
(invoke> flatten 1)
(invoke> (fn [board]
(keep-indexed #(if (= %2 "x") %1)
board)) 1))
;; Return indices of os placed on gameboard
!get-os (stackfn [!board]
!board
(invoke> flatten 1)
(invoke> (fn [board]
(keep-indexed #(if (= %2 "o") %1)
board)) 1))
;; Get a cartesian product
!cart (stackfn [!ns]
!ns
(invoke> (fn [ns]
(into #{}
(filter some?
(for [i ns
j ns
k ns]
(if (distinct? i j k)
#{i j k}))))) 1))
;; Returns "x" if x won, "o" if o won, and nil if game not yet
;; finished
!get-winner (stackfn [!board]
!board
(invoke> !get-xs 1)
(invoke> !cart 1)
!cart-xs+
(foreach> !cart-xs
<dup>
(invoke> set? 1)
(if>
(invoke> !win? 1)
!x-won?+
!x-won?
(if> "x" break)))
(invoke> (partial = "x") 1)
(if>
"x"
else>
!board
(invoke> !get-os 1)
(invoke> !cart 1)
!cart-os+
(foreach> !cart-os
<dup>
(invoke> set? 1)
(if>
(invoke> !win? 1)
!o-won?+
!o-won?
(if>
"o"
break)))
(invoke> (partial = "o") 1)
(if> "o" else> false)))
!prompt-user (stackfn prompt-user [!board]
!board
(invoke> !get-available-spaces 1)
<dup>
"Input a number"
"for your next move: "
(invoke> #(println %2 %3 %1) 3)
<pop>
!board
(invoke> !prn-board 1)
<pop>
input>
(invoke> (fn str->int [s]
(if (re-matches #"\d+" s)
(read-string
(re-matches #"\d+" s)))) 1)
!in+
(invoke> #(some #{%1} %2) 2)
<dup>
(if>
true
!board
(invoke> !update-game-board 3)
!board+
<dup>
(invoke> !get-available-spaces 1)
;; If a draw happens, it's always after the
;; player's move and before the AI's move
<dup>
(invoke> empty? 1)
(if>
;; Check if x won
!board
(invoke> !get-winner 1)
<dup>
(invoke> string? 1)
(if>
"WINNER IS:"
"!!!!!!!!!!!!!!!"
(invoke> #(println %2 %3 %1) 3)
!board
(invoke> !prn-board 1)
else>
"Draw!"
(invoke> println 1))
else>
(invoke> !ai-turn 2)
"AI's move: "
(invoke> println 1)
<pop>
!board+
(invoke> !prn-board 1)
!board
(invoke> !get-winner 1)
<dup>
(invoke> string? 1)
(if>
"WINNER IS:"
"!!!!!!!!!!!!!!!"
(invoke> #(println %2 %3 %1) 3)
!board
(invoke> !prn-board 1)
else>
<pop>
!board))
else> ;; Check if space unavailable
!in
(invoke> number? 1)
(if>
"Invalid space entered!"
(invoke> println 1)
!board
(invoke> prompt-user 1))))]
"Welcome to Tic-Tac-Toe!"
(invoke> println 1)
<pop>
(invoke> !construct-init-board 0)
(<do
(invoke> !prompt-user 1)
while> vector?)))
(defstackfn fizzbuzz
[]
(local [!fizz-buzz (stackfn [!in]
!in
(invoke> (fn [x]
(let [no-rem?
(comp zero? (partial mod x))]
(cond-> ""
(no-rem? 3) (str "fizz")
(no-rem? 5) (str "buzz"))))
1))]
(while> [0 int?]
(invoke>
(fn []
(print
"Enter a non-negative integer (enter anything else to exit): "))
0)
<pop>
(invoke> read-line 0)
(invoke> (fn str->int [s]
(if (re-matches #"\d+" s)
(read-string (re-matches #"\d+" s))))
1)
<dup>
(if>
(invoke> !fizz-buzz 1)
(invoke> println 1))
<pop>)))
(defstackfn fib
[!n]
!n
(invoke> #(> % 1) 1)
(if>
!n
(invoke> dec 1)
(invoke> fib 1)
!n
(invoke> (comp dec dec) 1)
(invoke> fib 1)
(invoke> + 2)
else>
!n))