|
|
- ;;; parseclj-ast.el --- Clojure parser/unparser -*- lexical-binding: t; -*-
-
- ;; Copyright (C) 2017-2018 Arne Brasseur
-
- ;; Author: Arne Brasseur <arne@arnebrasseur.net>
-
- ;; This file is not part of GNU Emacs.
-
- ;; This file is free software; you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation; either version 3, or (at your option)
- ;; any later version.
-
- ;; This file is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
-
- ;; You should have received a copy of the GNU General Public License
- ;; along with GNU Emacs; see the file COPYING. If not, write to
- ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- ;; Boston, MA 02110-1301, USA.
-
- ;;; Commentary:
-
- ;; Parse Clojure code to an AST, and unparse back to code.
-
- ;;; Code:
-
- (require 'a)
- (require 'seq)
- (require 'subr-x)
- (require 'parseclj-lex)
-
- ;; AST helper functions
-
- (defun parseclj-ast-node (type position &rest attributes)
- "Create an AST node with given TYPE and POSITION.
- Other ATTRIBUTES can be given as a flat list of key-value pairs."
- (apply 'a-list :node-type type :position position attributes))
-
- (defun parseclj-ast-node-p (node)
- "Return t if the given NODE is a Clojure AST node."
- (and (consp node)
- (consp (car node))
- (eq :node-type (caar node))))
-
- (defun parseclj-ast-node-attr (node attr)
- "Return NODE's ATTR, or nil."
- (a-get node attr))
-
- (defun parseclj-ast-node-type (node)
- "Return the type of the AST node NODE."
- (a-get node :node-type))
-
- (defun parseclj-ast-children (node)
- "Return children for the AST NODE."
- (a-get node :children))
-
- (defun parseclj-ast-value (node)
- "Return the value of NODE as another AST node."
- (a-get node :value))
-
- (defun parseclj-ast-leaf-node-p (node)
- "Return t if the given ast NODE is a leaf node."
- (member (parseclj-ast-node-type node) parseclj-lex--leaf-tokens))
-
- (defun parseclj-ast-branch-node-p (node)
- "Return t if the given AST NODE is a branch node."
- (not (parseclj-ast-leaf-node-p node)))
-
- ;; Parse/reduce strategy functions
-
- (defun parseclj-ast--reduce-leaf (stack token &optional _options)
- "Put into the STACK an AST leaf node based on TOKEN.
- Ignores white spaces and comments.
-
- OPTIONS is an association list. See `parseclj-parse' for more information
- on available options."
- (if (member (parseclj-lex-token-type token) '(:whitespace :comment))
- stack
- (cons
- (parseclj-ast-node (parseclj-lex-token-type token)
- (a-get token :pos)
- :form (a-get token :form)
- :value (parseclj-lex--leaf-token-value token))
- stack)))
-
- (defun parseclj-ast--reduce-leaf-with-lexical-preservation (stack token options)
- "Put into STACK an AST leaf node based on TOKEN.
- This function is very similar to `parseclj-ast--reduce-leaf', but unlike
- it, takes into account tokens representing white space or comments and
- saves them into the STACK. Nodes produced by this function have a
- `:lexical-preservation' key set to t.
-
- OPTIONS is an association list. See `parseclj-parse' for more information
- on available options."
- (let ((token-type (parseclj-lex-token-type token))
- (top (car stack)))
- (if (member token-type '(:whitespace :comment))
- ;; merge consecutive whitespace or comment tokens
- (if (eq token-type (a-get top :node-type))
- (cons (a-update top :form #'concat (a-get token :form))
- (cdr stack))
- (cons (parseclj-ast-node (parseclj-lex-token-type token)
- (a-get token :pos)
- :form (a-get token :form))
- stack))
- (parseclj-ast--reduce-leaf stack token options))))
-
- (defun parseclj-ast--reduce-branch (stack opening-token children _options)
- "Reduce STACK with an AST branch node representing a collection of elements.
- Ignores discard tokens.
-
- OPENING-TOKEN is a lex token representing an opening paren, bracket or
- brace.
- CHILDREN is the collection of nodes to be reduced into the AST branch node.
- OPTIONS is an association list. See `parseclj-parse' for more information
- on available options."
- (let* ((pos (a-get opening-token :pos))
- (type (parseclj-lex-token-type opening-token))
- (type (cl-case type
- (:lparen :list)
- (:lbracket :vector)
- (:lbrace :map)
- (t type))))
- (cl-case type
- (:root (cons (parseclj-ast-node :root pos :children children) stack))
- (:discard stack)
- (:tag (cons (parseclj-ast-node :tag
- pos
- :tag (intern (substring (a-get opening-token :form) 1))
- :children children)
- stack))
- (:metadata (cons (parseclj-ast-node :with-meta
- pos
- :children children)
- stack))
- (:map-prefix (cons (a-assoc (car children)
- :map-prefix opening-token)
- stack))
- (t (cons
- (parseclj-ast-node type pos :children children)
- stack)))))
-
- (defun parseclj-ast--reduce-branch-with-lexical-preservation (stack opening-token children options)
- "Reduce STACK with an AST branch node representing a collection of elements.
- Similar to `parseclj-ast--reduce-branch', but reduces discard tokens as
- well. Nodes produced by this function have a `:lexical-preservation'
- key set to t.
-
- OPENING-TOKEN is a lex token representing an opening paren, bracket or
- brace.
- CHILDREN is the collection of tokens to be reduced into the AST branch
- node.
- OPTIONS is an association list. See `parseclj-parse' for more information
- on available options."
- (if (eq :discard (parseclj-lex-token-type opening-token))
- (cons (parseclj-ast-node :discard (a-get opening-token :pos) :children children) stack)
- (let* ((stack (funcall #'parseclj-ast--reduce-branch stack opening-token children options))
- (top (car stack)))
- (if (parseclj-ast-node-p top)
- (cons (cl-list* (car top) ;; make sure :node-type remains the first element in the list
- '(:lexical-preservation . t)
- (cdr top))
- (cdr stack))
- stack))))
-
-
- ;; Unparse functions
-
- (declare-function parseclj-unparse-clojure "parseclj")
-
- (defun parseclj-ast--unparse-collection (node)
- "Insert a string representation of the given AST branch NODE into buffer."
- (let* ((token-type (parseclj-ast-node-type node))
- (delimiters (cl-case token-type
- (:root (cons "" ""))
- (:list (cons "(" ")"))
- (:vector (cons "[" "]"))
- (:set (cons "#{" "}"))
- (:map (cons "{" "}")))))
- (insert (car delimiters))
- (let ((nodes (alist-get ':children node)))
- (when-let (node (car nodes))
- (parseclj-unparse-clojure node))
- (seq-doseq (child (cdr nodes))
- (when (not (a-get node :lexical-preservation))
- (insert " "))
- (parseclj-unparse-clojure child)))
- (insert (cdr delimiters))))
-
- (defun parseclj-ast--unparse-tag (node)
- "Insert a string representation of the given AST tag NODE into buffer."
- (progn
- (insert "#")
- (insert (symbol-name (a-get node :tag)))
- (insert " ")
- (parseclj-unparse-clojure (car (a-get node :children)))))
-
- (provide 'parseclj-ast)
-
- ;;; parseclj-ast.el ends here
|