/
erl.el
651 lines (547 loc) · 21.5 KB
/
erl.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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
;;; erl.el --- Erlang-style process runtime system.
;;; Commentary:
;;
;; This module provides an Erlang-like runtime system in
;; Emacs. Processes are Emacs buffers with local variables containing
;; the pid, mailbox, etc.
;;
;; When a process is spawned it gets assigned a pid and a new buffer,
;; and its initial function is called in that buffer. This function
;; can do some initial processing, and then call (erl-continue K),
;; where K is a continuation function to called the next time the
;; process is scheduled. Usually the process won't be scheduled until
;; it receives a new message, which it can then retreive from its
;; mailbox. If the process returns without setting a new continuation,
;; it terminates with 'normal' status.
(eval-when-compile (require 'cl))
(require 'mcase)
(eval-and-compile
(or (fboundp 'defvar-local)
(defmacro defvar-local (var val &optional docstring)
(declare (debug defvar) (doc-string 3))
(list 'progn (list 'defvar var val docstring)
(list 'make-variable-buffer-local (list 'quote var))))))
(autoload 'erl-dist-send "derl")
(autoload 'erl-dist-reg-send "derl")
(autoload 'erl-dist-exit "derl")
(defgroup erl nil
"Erlang-style process runtime system."
;; Would prefer 'erlang should erlang.el define it.
:group 'languages)
;; Process ID structure.
;;
;; Exactly matches the [ERL-TAG erl-pid NODE ID SERIAL CREATION] vector used
;; in the `erlext' mapping, so don't change it!
(defvar erl-pid-counter) (defvar erl-node-name) ; defined below
(defstruct (erl-pid
(:type vector)
:named
(:initial-offset 1) ; make room for erl-tag (TYPE)
(:constructor nil) ; no default constructor
(:constructor %make-erl-local-pid (&optional (id (incf erl-pid-counter))
(node erl-node-name)
(serial 0)
(creation 0))))
node id serial creation)
(defun make-erl-local-pid (&optional id)
"Make a node-local pid."
(let ((pid (if id
(%make-erl-local-pid id)
(%make-erl-local-pid))))
;; Tag the first element of the pid
(setf (elt pid 0) erl-tag)
pid))
;; Global book keeping state
(defun erl-determine-hostname ()
"Figure out the short-names hostname."
(let ((fqdn (system-name)))
(if (string-match "\\.local$" fqdn)
fqdn
(if (string-match "[^\\.]+" fqdn)
(match-string 0 fqdn)
(error "erl: Can't determine hostname.")))))
(defvar erl-node-name
(intern (format "distel_%S@%s" (emacs-pid) (erl-determine-hostname)))
"Node name for Emacs.")
(defconst erl-null-pid (make-erl-local-pid 0)
"\"Null process\", the /dev/null of erl processes.
Any messages sent to this process are quietly discarded. When code
isn't running in the buffer of a particular process, it's running as
the null process.")
(defvar erl-pid-counter 0
"Counter for PIDs.")
(defvar erl-process-buffer-alist nil
"Automatically-maintained association list of (PID-ID . BUFFER)
mappings for local processes.")
(defvar erl-schedulable-processes nil
"List of processes which can be scheduled to run.")
(defvar erl-in-scheduler-loop nil
"True when the scheduler loop is on the call stack, i.e. when
schedulable processes are guaranteed to be executed before control is
passed back to Emacs.")
(defvar erl-default-group-leader erl-null-pid
;; Initialized to a real process further down
"Default group_leader for new processes.
Processes spawned by other processes will inherit their GL, but
\"brand new\" ones will use this.")
(defvar erl-popup-on-output t
"Popup *erl-output* when new output arrives.")
(defvar erl-stop-on-error nil
"*When non-nil, prevents the scheduler from catching Elisp errors.
Such errors are allowed to propagate, so you can debug them using
`debug-on-error'.
Because this interrupts the scheduling of processes, you must use the
command `erl-schedule' to continue.")
(defvar erl-ref-counter 0
"Unique reference id counter.")
(defvar erl-nodes nil
"List of nodes that we are currently connected to.")
;; Process-local variables
(eval-when-compile
(defmacro defprocvar (symbol &optional initvalue docstring)
"Define SYMBOL as a buffer-local process variable."
`(prog1 (defvar ,symbol ,initvalue ,docstring)
(make-variable-buffer-local ',symbol)
;; stop major modes' `kill-all-local-variables' from rubbing
;; out the process state.
(put ',symbol 'permanent-local t))))
(defprocvar erl-self erl-null-pid
"Current process' pid.
Always bound in a process' buffer, but in
other buffers defaults to `erl-null-pid'.")
(defprocvar erl-mailbox nil
"Process mailbox.
Contains messages for the process, which it's supposed to process and
remove. Messages are ordered from oldest to newest.")
(defprocvar erl-group-leader nil
"Group leader process.")
(defprocvar erl-continuation nil
"Function for the scheduler to call to run the process.")
(defprocvar erl-continuation-args nil
"Arguments for continuation function.")
(defprocvar erl-links nil
"Process links.")
(defprocvar erl-trap-exit nil
"True when trapping exits from linked processes.")
(defprocvar erl-exit-reason nil
"Exit reason, or nil if the process is alive.")
(defprocvar erl-reductions 0
"Number of \"reductions\".
Actually the number of times the process has been run invoked,
typically by the scheduler.")
(defprocvar erl-process-name nil
"Name of the process, a string.
This can be set and then later used to help with debugging.
An example name would be \"Process list for z@cockatoo\"")
(defmacro with-erl-process (pid &rest body)
"Execute BODY in PID's buffer. This is a full context-switch."
`(with-current-buffer (erl-pid->buffer ,pid)
,@body))
;; Bindings capture helpers
(defmacro capture-bindings (&rest vars)
"Create a data structure representing the bidings of VARS.
These bindings can be restored by `with-bindings' or
`call-with-bindings'."
`(list ',vars (list ,@vars)))
(defmacro with-bindings (bindings &rest body)
"Run BODY with BINDINGS restored.
BINDINGS is created by `capture-bindings'"
`(call-with-bindings ,bindings (lambda () ,@body)))
(defun call-with-bindings (bindings fn)
"Call FN with BINDINGS restored.
BINDINGS is created by `capture-bindings'"
(let ((vars (car bindings))
(vals (cadr bindings)))
(eval `(apply (lambda ,vars (funcall ,fn)) ',vals))))
;; Process API functions
(defmacro erl-spawn-async (&rest body)
"Spawn a new erl process and have it execute BODY.
Since the process may be scheduled for later, BODY is not guaranteed
to run in the current dynamic environment; see `erl-spawn' for an
alternative."
`(erl-spawn-fun (lambda () ,@body)))
(defmacro erl-spawn (&rest body)
"Spawn a new process and run it in the current dynamic environment."
`(erl-spawn-fun (lambda () ,@body) t))
(defmacro erl-spawn-link-async (&rest body)
"Same as `erl-spawn-async', but links with the process."
`(erl-spawn-fun (lambda () ,@body) nil t))
(defmacro erl-spawn-link (&rest body)
"Same as `erl-spawn', but links with the process."
`(erl-spawn-fun (lambda () ,@body) t t))
(defun erl-link (pid)
"Link the current process with PID."
(unless (equal pid erl-self)
(erl-add-link erl-self pid)
(erl-add-link pid erl-self)))
(defun erl-unlink (pid)
"Unlink the current process from PID."
(erl-remove-link erl-self pid)
(erl-remove-link pid erl-self))
(defun erl-send (who message)
"Send the term MESSAGE to the process WHO.
WHO can be a pid, a registered name (symbol), or a tuple of
\[REGISTERED-NAME NODE]."
(cond ((erl-null-pid-p who)
(erl-lose-msg message))
((erl-local-pid-p who)
(when (erl-local-pid-alive-p who)
(erl-deliver-message who message)))
((erl-remote-pid-p who)
(erl-dist-send who message))
((symbolp who)
(let ((proc (erl-whereis who)))
(if proc
(erl-send proc message)
(erl-exit (tuple 'badarg (tuple 'not-registered who))))))
((tuplep who) ; [tuple NAME NODE]
(erl-dist-reg-send (tuple-elt who 2) (tuple-elt who 1) message))
(t
(error "Bad pid: %S" who))))
(defun erl-exit (why &optional who)
"Exit the current process.
Like the erlang BIF exit/1."
(if who
(erl-send-exit erl-self who why)
(signal 'erl-exit-signal (list why))))
(defun erl-exit/2 (who why)
"Exit a process.
Like the erlang BIF exit/2."
(erl-exit why who))
(defun erl-continue (k &rest args)
"Yield control and arrange for K to be called with ARGS the next
time this process is scheduled. Note that the process is not
\"scheduled out\" automatically, and the caller should return
normally."
(setq erl-continuation k)
(setq erl-continuation-args args))
(defun erl-reschedule ()
"Return immedaitely (via `throw') to the scheduler.
Also makes the current process immediately reschedulable."
;; the scheduler loop will catch this and know what to do
(throw 'schedule-out 'reschedule))
(defun erl-make-ref ()
"Make a unique reference object."
(vector erl-tag 'erl-ref erl-node-name (incf erl-ref-counter) 0))
(defun erl-binaryp (x)
(and (eq (elt x 0) erl-tag) (eq (elt x 1) 'erl-binary)))
(defun erl-binary-string (x)
(elt x 2))
(defun erl-binary (string)
(vector erl-tag 'erl-binary string))
;; receive
(defmacro erl-receive (vars clauses &rest after)
"Receive a message, matched by pattern.
If the mailbox contains a matching message, the pattern's body is
executed immediately. Otherwise, `erl-continue' is used to make the
process continue matching when new messages arrive. The crucial
difference from Erlang's receive is that erl-receive returns
immediately when nothing is matched, but will automatically resume
after a new message arrives and the process is rescheduled.
Since the the process may return and be rescheduled before the
matching message is received, the clause's body might not be executed
in the original dynamic environment. Consequently, any local variable
bindings that need to be preserved should be named in VARS.
After a pattern has been matched and executed, the AFTER forms are
then executed.
The overall syntax for receive is:
(erl-receive (VAR-NAME ...)
((PATTERN . BODY)
...)
. AFTER)
The pattern syntax is the same as `mcase-let'."
`(erl-start-receive (capture-bindings ,@vars)
,(mcase-parse-clauses clauses)
(lambda () ,@after)))
(defun erl-start-receive (bs clauses after)
;; Setup a continuation and immediately return to the scheduler
;; loop, which will call us back.
(when (equal erl-self erl-null-pid)
(error "No process context for erl-receive"))
(erl-continue #'erl-receive* bs clauses after)
(erl-reschedule))
(defun erl-receive* (bs clauses after)
(erl-receive-loop bs clauses after erl-mailbox))
(defun erl-receive-loop (bs clauses after msgs &optional acc)
(if (null msgs)
(erl-continue #'erl-receive* bs clauses after)
(let ((action
;; We restore the bindings incase they are referred to in patterns
(with-bindings bs
(mcase-choose (car msgs) clauses))))
(if (null action)
(erl-receive-loop bs clauses after (cdr msgs) (cons (car msgs) acc))
(setq erl-mailbox (append (reverse acc) (cdr msgs)))
(with-bindings bs
(funcall action)
(funcall after))))))
(defun erl-register (name &optional process)
"Register PROCESS with NAME."
(if (get-buffer (erl-regname-to-bufname name))
(erl-exit (tuple 'badarg (tuple 'already-registered name)))
(with-erl-process (or process erl-self)
(rename-buffer (erl-regname-to-bufname name)))))
(defun erl-whereis (name)
"Get the PID of the process registered with NAME, or nil if the name
is unregistered."
(let ((buf (get-buffer (erl-regname-to-bufname name))))
(if buf
(with-current-buffer buf erl-self))))
(defun erl-regname-to-bufname (name)
(format "*reg %S*" name))
(defalias 'erl-term-to-binary #'erlext-term-to-binary)
(defalias 'erl-binary-to-term #'erlext-binary-to-term)
(defun erl-group-leader ()
(or erl-group-leader erl-default-group-leader))
;; Guts
;;
;; The scheduler works without ever being explicitly called. It runs
;; when a message arrives from a remote node or when a new process is
;; spawned, and it continues to schedule processes until none are
;; runnable.
;;
;; The %ugly-variable-names are to avoid shadowing any existing
;; dynamic bindings between the caller and the process being invoked -
;; that's a bastard to debug.
(put 'erl-exit-signal
'error-conditions
'(error erl-exit-signal))
(defun erl-spawn-fun (%init-function &optional %run-now-p %link)
"Spawn a new erl process to call INIT-FUNCTION.
If RUN-NOW-P is true, the process is called immediately with the
current dynamic environment/bindings. Otherwise, the process is made
schedulable. The scheduler loop is entered if we aren't being called
by it already.
If LINK is true, the process is linked before being run."
(let* ((%pid (make-erl-local-pid))
(%buffer (get-buffer-create (erl-pid-buffer-name %pid)))
(%gl (or erl-group-leader erl-default-group-leader)))
(with-current-buffer %buffer
(setq erl-self %pid)
(setq erl-group-leader %gl)
(setq erl-continuation %init-function)
(setq erl-continuation-args nil)
(erl-enroll-process))
(when %link (erl-link %pid))
(if %run-now-p
(let ((erl-in-scheduler-loop t))
(erl-run-process %pid))
(erl-make-schedulable %pid))
(erl-maybe-schedule)
%pid))
(defun erl-idle ()
(erl-receive ()
()))
(defun erl-deliver-message (pid message)
"Deliver MESSAGE to the mailbox of the local process PID.
Invokes the scheduler if necessary."
(with-erl-process pid
(setq erl-mailbox (append erl-mailbox (list message))))
(erl-make-schedulable pid)
(erl-maybe-schedule))
(defun erl-schedule ()
"Enter scheduler loop until no process is runnable."
(interactive)
(let ((erl-in-scheduler-loop t))
(while (erl-schedule-once)))
;; post-condition
(assert (null erl-schedulable-processes)))
(defun erl-schedule-once ()
"Schedule the next process to run. Returns true if a process was scheduled."
(when erl-schedulable-processes
(erl-schedule-process (pop erl-schedulable-processes))
t))
(defun erl-maybe-schedule ()
"Schedule processes, unless the scheduler is already running."
(unless erl-in-scheduler-loop
(erl-schedule)))
(defun erl-schedule-process (%pid)
(cond ((not (erl-local-pid-alive-p %pid))
(message "STRANGE: %S scheduled but dead; removing" %pid)
(erl-make-unschedulable %pid))
((with-erl-process %pid (null erl-continuation))
(message "STRANGE: %S is a zombie! killing" %pid)
(with-erl-process %pid (erl-terminate 'zombie)))
(t
(while (eq 'reschedule (erl-run-process %pid))))))
(defun erl-run-process (%pid)
"Run a process.
Calls the current continuation from within the process' buffer."
(with-erl-process %pid
(incf erl-reductions)
;; The %ugly-names are to avoid shadowing the caller's dynamic
;; bindings.
(let ((%k erl-continuation)
(%kargs erl-continuation-args)
(%buffer (current-buffer)))
(setq erl-continuation nil)
(setq erl-continuation-args nil)
(if erl-stop-on-error
(erl-invoke %k %kargs)
(condition-case data
(erl-invoke %k %kargs)
(error (erl-terminate `[emacs-error ,(format "%S" data)])))))))
(defun erl-invoke (%k %kargs)
(condition-case data
(prog1 (catch 'schedule-out
(prog1 nil (save-current-buffer (apply %k %kargs))))
(unless erl-continuation
(erl-terminate 'normal)))
(erl-exit-signal (erl-terminate (cadr data)))))
(defun erl-make-schedulable (pid)
"Add PID to the list of runnable processes, so that it will execute
during the next `erl-schedule'."
(unless (member pid erl-schedulable-processes)
(setq erl-schedulable-processes
(append erl-schedulable-processes (list pid)))))
(defun erl-make-unschedulable (pid)
"Remove PID from the list of schedulable processes."
(setq erl-schedulable-processes
(remove pid erl-schedulable-processes)))
(defun erl-terminate (why)
"Exit the current process."
(unless (eq why 'normal)
(message "EXIT: %s %S %s" (erl-pid-to-string erl-self) why
(if erl-process-name
(concat "\n Process name: " erl-process-name)
"")))
(setq erl-exit-reason why)
(erl-make-unschedulable erl-self)
(kill-buffer (erl-pid->buffer erl-self)))
(defun erl-add-link (from to)
"Unidirectionally add a link."
(unless (erl-null-pid-p from)
(with-erl-process from
(add-to-list 'erl-links to))))
(defun erl-remove-link (from to)
"Unidirectionally remove a link."
(unless (erl-null-pid-p from)
(with-erl-process from
(setq erl-links (remove to erl-links)))))
(defun erl-set-name (fmt &rest args)
"Set `erl-process-name' to (apply 'format (FMT . ARGS))."
(setq erl-process-name (apply 'format (cons fmt args))))
;; PID utilities
(defun erl-pid-buffer-name (pid)
(unless (equal (erl-pid-node pid) erl-node-name)
(error "Not a local pid: %S" pid))
(format "*pid <%S.%S.%S>*"
0
(erl-pid-id pid)
(erl-pid-serial pid)))
(defun erl-pid->buffer (pid)
"Get PID's buffer."
(or (cdr (assoc (erl-pid-id pid) erl-process-buffer-alist))
(error "No buffer for pid %S" pid)))
(defun erl-null-pid-p (p)
(equal p erl-null-pid))
(defun erl-local-pid-alive-p (pid)
"Is PID a live local process?"
(when (erl-local-pid-p pid)
(let ((buffer (cdr (assoc (erl-pid-id pid) erl-process-buffer-alist))))
(and buffer
(buffer-live-p buffer)
(with-erl-process pid
(null erl-exit-reason))))))
(defun erl-local-pid-p (x)
"True iff X is the pid of a local process."
(and (erl-pid-p x)
(equal (erl-pid-node x) erl-node-name)))
(defun erl-remote-pid-p (x)
"True iff X is the pid of a remote process."
(and (erl-pid-p x)
(not (erl-local-pid-p x))))
(defun erl-pid-to-string (pid)
;; FIXME: number nodes
(let ((n (if (eq (erl-pid-node pid) erl-node-name)
"0" ; local
"?")))
(format "<%s.%S.%S>" n (erl-pid-id pid) (erl-pid-serial pid))))
(defun erl-lose-msg (msg)
"Log and discard a message sent to the null process."
(with-current-buffer (get-buffer-create "*erl-lost-msgs*")
(goto-char (point-max))
(insert (format "%S\n" msg))))
(defun erl-enroll-process ()
"Setup pid->buffer mapping state for the current process."
(push (cons (erl-pid-id erl-self) (current-buffer))
erl-process-buffer-alist)
(add-hook 'kill-buffer-hook #'erl-unenroll-process nil t)
(add-hook 'kill-buffer-hook #'erl-propagate-exit nil t))
(defun erl-remove-if (predicate list)
"Return a copy of LIST with all items satisfying PREDICATE removed."
(let (out)
(while list
(unless (funcall predicate (car list))
(push (car list) out))
(setq list (cdr list)))
(nreverse out)))
(defun erl-unenroll-process ()
(setq erl-process-buffer-alist
(erl-remove-if #'(lambda (x) (eq (erl-pid-id erl-self) (car x)))
erl-process-buffer-alist)))
(defun erl-propagate-exit ()
(when (null erl-exit-reason)
(setq erl-exit-reason 'killed))
(unless (eq erl-exit-reason 'normal)
(mapc #'(lambda (proc) (erl-send-exit erl-self proc erl-exit-reason))
erl-links)))
(defun erl-send-exit (from to rsn)
(cond ((erl-local-pid-alive-p to)
(erl-deliver-exit from to rsn))
((erl-remote-pid-p to)
(erl-dist-exit from to rsn))))
(defun erl-deliver-exit (from to rsn)
(with-erl-process to
(cond (erl-exit-reason ; already terminated?
t)
(erl-trap-exit
(erl-deliver-message to (tuple 'EXIT from rsn))
(erl-unlink from))
(t
(erl-terminate rsn)))))
(defun erl-nodedown-exit (local remote)
"Send an exit to LOCAL from REMOTE caused by a communications failure."
(when (erl-local-pid-alive-p local)
(with-erl-process local
(setq erl-links (remove remote erl-links))
(erl-deliver-exit remote local 'noconnection))))
;; Initialisation
(defun erl-nodeup (node proc)
(pushnew node erl-nodes)
(message "nodeup: %S" node))
(defun erl-nodedown (node)
(setq erl-nodes (remove node erl-nodes))
(message "nodedown: %S" node))
;; These hooks are defined in derl.el
(add-hook 'erl-nodeup-hook 'erl-nodeup)
(add-hook 'erl-nodedown-hook 'erl-nodedown)
;; Emacs indentation
(put 'with-erl-process 'lisp-indent-function 1)
(put 'erl-spawn 'lisp-indent-function 'defun)
(put 'erl-spawn-async 'lisp-indent-function 'defun)
(put 'erl-receive 'lisp-indent-function 2)
(put 'with-bindings 'lisp-indent-function 1)
;; Initial processes
(defun &erl-group-leader-loop ()
(erl-receive ()
((['put_chars s]
(if (eq s nil)
nil
(condition-case err
(save-excursion
(with-current-buffer (get-buffer-create "*erl-output*")
(goto-char (point-max))
(insert s)
(when erl-popup-on-output
(set-window-point (display-buffer (current-buffer))
(point-max)))))
(error (message "Error in group leader: %S" err))))))
(&erl-group-leader-loop)))
(when (null erl-group-leader)
(setq erl-default-group-leader
(erl-spawn
(erl-register 'group-leader)
(&erl-group-leader-loop))))
(provide 'erl)
;;; erl.el ends here