-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathparseclj-ast.el
205 lines (168 loc) · 7.6 KB
/
parseclj-ast.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
;;; parseclj-ast.el --- Clojure parser/unparser -*- lexical-binding: t; -*-
;; Copyright (C) 2017-2018 Arne Brasseur
;; Author: Arne Brasseur <[email protected]>
;; 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