Fennel wiki: state-machine

;; Because Fennel has tail-call optimization, you can use direct function calls
;; to construct a state machine for parsing. This code constructs a parser for
;; strings that use # and {} as tags and delimiters to structure text. For
;; example, this text:

;; bing bang #start-entry{1} This is a #test entry with a ##flagged
;; #tag{that has many arguments to it} #end-entry

;; ... parses into this data:

;; -> ["bing bang" "#start-entry{1}" " This is a" "#test" "entry with a"
;;     "##flagged" "#tag{that has many arguments to it}" "#end-entry"]

;; For more about using tail-calls for state machines, see the book Programming
;; in Lua: https://www.lua.org/pil/6.3.html

;; because the state functions are mutually recursive, we have to put them in
;; a table, otherwise they won't all be in scope for each other.
(local parsers {})

(fn add-char [results char]
  (let [last (. results (# results))]
    (doto results
      (tset (# results) (.. last char)))))

(fn trim-trailing-space [results]
  (let [last (. results (# results))]
    (tset results (# results) (last:gsub "%s+$" ""))
    (when (= (. results (# results)) "")
      (table.remove results))))

(fn next-item [results]
  (doto results
    (trim-trailing-space)
    (table.insert "")))

(fn parsers.base [str i results]
  (match (string.sub str i i)
    "" results ; done!
    "#" (parsers.pound str (+ i 1) (-> results (next-item) (add-char "#")))
    "{" (error "Curly braces must come after pound")
    char (parsers.base str (+ i 1) (add-char results char))))

(fn parsers.pound [str i results]
  (match (string.sub str i i)
    "" results ; done!
    "{" (parsers.curly str (+ i 1) (add-char results "{"))
    " " (parsers.begin str (+ i 1) (next-item results))
    char (parsers.pound str (+ i 1) (add-char results char))))

(fn parsers.curly [str i results]
  (match (string.sub str i i)
    "}" (parsers.begin str (+ i 1) (-> results (add-char "}") (next-item)))
    "{" (error "Curly braces may not nest.")
    "" (error "Incomplete curly!")
    char (parsers.curly str (+ i 1) (add-char results char))))

(fn parsers.begin [str i results]
  (match (string.sub str i i)
    " " (parsers.begin str (+ i 1) results) ; consume all leading space
    char (parsers.base str i results))) ; then transition to base state

(local view (require :fennelview))
(print (view (parsers.begin (table.concat arg " ") 1 [""])))