Skip to content

E-A-Griffin/stack-dsl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stack-clj

Stack-clj is a stack-based Domain Specific Language built in Clojure.

API

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.

Constants

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

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++).

Stackfn

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.

Local

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 Interop

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

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).

Loops

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>

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>

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>

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>

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

I/O

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).

Java Interop

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.

etc.

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.

Examples

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.

Tic-Tac-Toe

(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?)))

Fizzbuzz

(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>)))

Fibonacci

(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))

About

Stack-clj is a stack-based Domain Specific Language built in Clojure.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published