This repository has been archived by the owner on Jan 6, 2021. It is now read-only.
/
parser.clj
164 lines (138 loc) · 5.17 KB
/
parser.clj
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
(ns alda.parser
(:require [alda.lisp.score :as score]
[alda.parser
[aggregate-events :as agg]
[parse-events :as event]
[tokenize :as token]]
[clojure.core.async :refer [<!! >!! chan close! go-loop thread]]))
(defn print-stream
"Continuously reads from a channel and prints what is received, stopping once
the channel is closed."
[channel]
(thread
(loop []
(when-let [x (<!! channel)]
(prn x)
(recur)))))
(defn stream-seq
"Coerces a channel into a lazy sequence that can be lazily consumed or
realized at will."
[ch]
(lazy-seq (when-let [v (<!! ch)] (cons v (stream-seq ch)))))
(defn tokenize
"Asynchronously reads and tokenizes input, streaming the result into a
channel.
Returns a channel from which tokens can be read as they are parsed."
[input]
(let [chars-ch (chan)
tokens-ch (chan)]
; feed each character of input to chars-ch
(thread
(doseq [character input] (>!! chars-ch character))
(>!! chars-ch :EOF)
(close! chars-ch))
; parse tokens from chars-ch and feed them to tokens-ch
(thread
(loop [{:keys [line column] :as parser} (token/parser tokens-ch)]
(if-let [character (<!! chars-ch)]
(recur (token/read-character! parser character))
(do
(>!! tokens-ch [:EOF [line column]])
(close! tokens-ch)))))
tokens-ch))
(defn parse-events
"Asynchronously reads tokens from a channel, parsing events and streaming
them into a new channel.
Returns a channel from which events can be read as they are parsed.
If there is an error, the error is included in the stream."
[tokens-ch]
;; alda.lisp must be required and referred in order to use inline Clojure
;; expressions.
(when-not (resolve 'ALDA-LISP-LOADED)
(require '[alda.lisp :refer :all]))
(let [events-ch (chan)]
(thread
(loop [parser (event/parser events-ch)]
(if-let [token (<!! tokens-ch)]
(recur (event/read-token! parser token))
(do
(>!! events-ch :EOF)
(close! events-ch)))))
events-ch))
(defn aggregate-events
"Asynchronously reads events from a channel and aggregates certain types of events that need to be aggregated, e.g. notes in a chord.
Returns a channel on which the final events can be read.
If there is an error, the error is included in the stream."
[events-ch]
;; alda.lisp must be required and referred in order to use inline Clojure
;; expressions.
(when-not (resolve 'ALDA-LISP-LOADED)
(require '[alda.lisp :refer :all]))
(let [events-ch2 (chan)]
(thread
(loop [parser (agg/parser events-ch2)]
(if-let [event (<!! events-ch)]
(recur (agg/read-event! parser event))
(close! events-ch2))))
events-ch2))
(defn build-score
"Asynchronously reads events from a channel and applies them sequentially to a
new score.
Returns a channel from which the complete score can be taken.
If there was an error in the a previous part of the pipeline, it is thrown
here."
[events-ch2]
;; alda.lisp must be required and referred in order to use inline Clojure
;; expressions.
(when-not (resolve 'ALDA-LISP-LOADED)
(require '[alda.lisp :refer :all]))
(go-loop [score (score/score)]
(let [error (atom nil)
event (<!! events-ch2)]
(cond
(nil? event)
(if @error @error score)
(instance? Throwable event)
(swap! error #(or % event))
:else
(recur (try
(score/continue score event)
(catch Throwable e
(swap! error #(or % e)))))))))
(defn parse-input
"Given a string of Alda code, process it via the following asynchronous
pipeline:
- Tokenize it into a stream of recognized tokens.
- From the token stream, parse out a stream of events.
- Process the events sequentially to build a score.
If an :output key is supplied, the result will depend on the value of that
key:
:score => an Alda score map, ready to be performed by the sound engine. If
there is an error, the error is thrown.
:events => a lazy sequence of Alda events, which will produce a complete
score when applied sequentially to a new score. Note that the sequence may
contain an error object if there is any error parsing, and the error is not
thrown. If it is desirable to throw an error, use :events-or-error.
:events-or-error => equivalent to :events, but the sequence is fully realized
and an error is thrown in the event of a parse error.
The default :output is :score."
[input & {:keys [output] :or {output :score}}]
(case output
:score
(let [score (-> input
tokenize
parse-events
aggregate-events
build-score
<!!)]
(if (instance? Throwable score)
(throw score)
score))
:events
(-> input tokenize parse-events aggregate-events stream-seq)
:events-or-error
(let [events (parse-input input :output :events)]
(doseq [event events]
(if (instance? Throwable event)
(throw event)))
events)))