Skip to content

Commit 0816aa0

Browse files
committed
Add binding / bind
trace-macroexpand also tries to handle bad tracing states much harder.
1 parent ab07289 commit 0816aa0

File tree

4 files changed

+328
-6
lines changed

4 files changed

+328
-6
lines changed

README.md

+85-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ This repo contains a collection of small Common Lisp hacks I've written over the
33

44
## General
55
### Modules
6-
Each of these hacks is an independent module: it lives in its own little package and is loadable standalone: if you just want one of them then you don't need to drag all the unrelated ones into your environment. If you put them in the right place, then [`require-module`](https://github.com/tfeb/tfeb-lisp-tools#requiring-modules-with-searching-require-module "require-module") will find and load them for you: this is how I use them.
6+
Almost all of these hacks are independent modules: they live in their own little packages and are loadable standalone: if you just want one of them then you don't need to drag all the unrelated ones into your environment. If you put them in the right place, then [`require-module`](https://github.com/tfeb/tfeb-lisp-tools#requiring-modules-with-searching-require-module "require-module") will find and load them for you: this is how I use them. Exceptions are:
7+
8+
- `binding`, which depends on `collecting` and `iterate`, and will try to use `require-module` to load them if they're not already known when it's compiled or loaded.
79

810
The system itself provides `:org.tfeb.hax`: there is no `org.tfeb.hax` package however: each component lives in its own package with names like `org.tfeb.hax.*`.
911

@@ -885,6 +887,88 @@ counter
885887
### Package, module
886888
`define-functions` lives in `org.tfeb.hax.define-functions` and provides `:org.tfeb.hax.define-functions`.
887889

890+
## Local bindings: `binding`
891+
Different languages take different approaches to declaring – *binding* variables and functions locally in code.
892+
893+
- CL requires `let`, `labels` &c, which is clear but involves extra indentation;
894+
- Scheme allows local use of `define` which does not involve indentation, but does not allow it everywhere;
895+
- Python allows local bindings anywhere but the scope is insane (bindings have function scope and are thus visible before they appear textually) and variable binding is conflated with assignment which is just a horrible idea:
896+
- some C compilers may allow variable declarations almost anywhere with their scope starting from the point of declaration and running to the end of the block – I am not sure what the standard supports however;
897+
- Racket allows `define` in many more places than Scheme with their scope running to the end of the appropriate block.
898+
899+
Racket is pretty clear how what it does works:
900+
901+
```lisp
902+
...
903+
(define foo ...)
904+
...
905+
```
906+
907+
turns into
908+
909+
```lisp
910+
...
911+
(letrec ([foo ...])
912+
...)
913+
```
914+
915+
I thought it would be fun to implement a form which does this for CL, and that's what `binding` does.
916+
917+
**`binding`** is a form, directly within whose body several special binding forms are available. These forms are:
918+
919+
- `bind` will bind local variables or functions, corresponding to `let*` or `labels` respectively;
920+
- `bind/values` will bind multiple values, corresponding to `multiple-value-bind`;
921+
- `bind/destructuring` corresponds to `destructuring-bind`.
922+
923+
For `bind` the two cases are:
924+
925+
- `(bind var val)` will bind `var` to `val`using `let*`;
926+
- `(bind (f ...) ...)` will create a local function `f` using `labels` (the function definition form is like Scheme's `(define (f ...) ...)` syntax).
927+
928+
For `bind/values` there are also two cases:
929+
930+
- `(bind/values (...) form)` corresponds to `(multiple-value-bind (...) form ...)`
931+
- `(bind-values (...) form ...)` corresponds to `(multiple-value-bind (...) (values form ...) ...)`.
932+
933+
`bind/destructuring` doesn't have any variants.
934+
935+
All of these forms are coalesced to create the minimum number of binding constructs in the generated code (this is why `bind` corresponds to `let*`), so:
936+
937+
```lisp
938+
(binding
939+
(print 1)
940+
(bind x 1)
941+
(bind y 2)
942+
(print 2)
943+
(bind (f1 v)
944+
(+ x v))
945+
(bind (f2 v)
946+
(+ y (f1 v)))
947+
(f2 1))
948+
```
949+
950+
corresponds to
951+
952+
```lisp
953+
(progn
954+
(print 1)
955+
(let* ((x 1) (y 2))
956+
(print 2)
957+
(labels ((f1 (v)
958+
(+ x v))
959+
(f2 (v)
960+
(+ y (f1 v))))
961+
(f2 1))))
962+
```
963+
964+
and so on. `bind/values` and `bind/destructuring` are not coalesced as it makes no sense to do so.
965+
966+
### Notes
967+
`bind` &c work *only* directly within `binding`: there is no code walker, intentionally so. There are top-level definitions of `bind` &c as macros which signal errors at macroexpansion time.
968+
969+
### Package, module, dependencies
970+
`binding` lives in `org.tfeb.hax.binding`and provides `:org.tfeb.hax.binding`. `binding` depends on `collecting` and `iterate` at compile and run time. If you load it as a module then, if you have [`require-module`](https://github.com/tfeb/tfeb-lisp-tools#requiring-modules-with-searching-require-module "require-module"), it will use that to try and load them if they're not there. If it can't do that and they're not there you'll get a compile-time error.
971+
888972
----
889973

890974
[^1]: The initial documentation for these hacks was finished on 20210120 at 18:26 UTC: one hour and twenty-six minutes after Joe Biden became president of the United States.

binding.lisp

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
;;;; Local definitions (like Racket)
2+
;;;
3+
;;; Like Racket (rather than Scheme) bindings can occur anywhere in a
4+
;;; BINDING form and an appropriately nested structure results.
5+
;;;
6+
;;; Bindings are only considered in the immediate children of BINDING
7+
;;; to avoid needing a code-walker.
8+
;;;
9+
;;; Within BINDING
10+
;;; - (bind var val) binds a variable;
11+
;;; - (bind (f ...) ...) binds a function (punning syntax like Scheme);
12+
;;; - (bind/values (...) form ...) binds multiple values -- if there
13+
;;; is a single form it should return as many values as there are
14+
;;; variables, otherwise there should be as many forms as values;
15+
;;; - (bind/destructuring dsll form) binds with destructuring;
16+
;;;
17+
;;; Successive bindings of the same kind (for BIND, not the other two)
18+
;;; are coalesced.
19+
;;;
20+
21+
;;; Try to make this work as a module
22+
;;;
23+
(eval-when (:compile-toplevel :load-toplevel :execute)
24+
(unless (and (member "ORG.TFEB.HAX.COLLECTING" *modules* :test #'string=)
25+
(member "ORG.TFEB.HAX.ITERATE" *modules* :test #'string=))
26+
#+org.tfeb.tools.require-module
27+
(org.tfeb.tools.require-module:require-modules
28+
'(:org.tfeb.hax.collecting
29+
:org.tfeb.hax.iterate))
30+
#-org.tfeb.tools.require-module
31+
(error "doomed")))
32+
33+
(defpackage :org.tfeb.hax.binding
34+
(:use :cl
35+
:org.tfeb.hax.collecting
36+
:org.tfeb.hax.iterate)
37+
(:export
38+
#:binding
39+
#:bind
40+
#:bind/values
41+
#:bind/destructuring))
42+
43+
(in-package :org.tfeb.hax.binding)
44+
45+
(provide :org.tfeb.hax.binding)
46+
47+
;;; At top-level all these should be errors
48+
;;;
49+
50+
(defmacro bind (name &body forms)
51+
(declare (ignore forms))
52+
(error "Trying to bind ~S outside binding" name))
53+
54+
(defmacro bind/values (vars &body forms)
55+
(declare (ignore forms))
56+
(error "Trying to bind/values ~S outside binding" vars))
57+
58+
(defmacro bind/destructuring (dsll form)
59+
(declare (ignore form))
60+
(error "Tryng to bind/destructuring ~A outside binding" dsll))
61+
62+
(defun parse-binding-form (form)
63+
;; Return what sort of binding form this is, or NIL, and the
64+
;; corresponding binding, or NIL
65+
(if (consp form)
66+
(case (first form)
67+
((bind)
68+
(unless (>= (length form) 2)
69+
(error "hopless bind form ~S" form))
70+
(typecase (first (rest form))
71+
(symbol
72+
(unless (<= (length form) 3)
73+
(error "too many expressions in ~S" form))
74+
(values 'variable (rest form)))
75+
(cons
76+
(values 'function `(,(first (first (rest form)))
77+
,(rest (first (rest form)))
78+
,@(rest (rest form)))))
79+
(t
80+
(error "mutant bind form ~S" form))))
81+
((bind/values)
82+
(unless (>= (length form) 2)
83+
(error "hopeless bind/values form ~S" form))
84+
(let ((vars (first (rest form)))
85+
(forms (rest (rest form))))
86+
(unless (and (listp vars)
87+
(every #'symbolp vars))
88+
(error "not all variables are in ~S" form))
89+
(values 'values
90+
(if (= (length form) 3)
91+
(rest form)
92+
`(,vars (values ,@forms))))))
93+
((bind/destructuring)
94+
(unless (= (length form) 3)
95+
(error "hopeless bind/destructing form ~S" form))
96+
(let ((dsll (first (rest form))))
97+
(unless (listp dsll)
98+
(error "destructuring lambda list isn't in ~S" form))
99+
(values 'destructuring (rest form))))
100+
(otherwise
101+
(values nil nil)))
102+
(values nil nil)))
103+
104+
(defun walk-binding-body (body)
105+
;; Walk the body of a BINDING form. This is just unavoidably hairy.
106+
(collecting
107+
(iterate wbb ((tail body)
108+
(variable-bindings '())
109+
(function-bindings '()))
110+
(cond
111+
(tail
112+
(destructuring-bind (this . rest) tail
113+
(multiple-value-bind (what binding) (parse-binding-form this)
114+
(ecase what
115+
((variable)
116+
(cond
117+
(variable-bindings ;been collecting vars
118+
(wbb rest (cons binding variable-bindings)
119+
function-bindings))
120+
(function-bindings ;been collecting fns
121+
(collect `(labels ,(reverse function-bindings)
122+
,@(walk-binding-body tail))))
123+
(t ;not collecting
124+
(wbb rest (cons binding variable-bindings) '()))))
125+
((function)
126+
(cond
127+
(function-bindings
128+
(wbb rest '() (cons binding function-bindings)))
129+
(variable-bindings
130+
(collect `(let* ,(reverse variable-bindings)
131+
,@(walk-binding-body tail))))
132+
(t
133+
(wbb rest '() (cons binding function-bindings)))))
134+
((values)
135+
(cond
136+
(variable-bindings
137+
(collect
138+
`(let* ,(reverse variable-bindings)
139+
(multiple-value-bind ,(first binding) ,(second binding)
140+
,@(walk-binding-body rest)))))
141+
(function-bindings
142+
(collect
143+
`(labels ,(reverse function-bindings)
144+
(multiple-value-bind ,(first binding) ,(second binding)
145+
,@(walk-binding-body rest)))))
146+
(t
147+
(collect
148+
`(multiple-value-bind ,(first binding) ,(second binding)
149+
,@(walk-binding-body rest))))))
150+
((destructuring)
151+
(cond
152+
(variable-bindings
153+
(collect
154+
`(let* ,(reverse variable-bindings)
155+
(destructuring-bind ,(first binding) ,(second binding)
156+
,@(walk-binding-body rest)))))
157+
(function-bindings
158+
(collect
159+
`(labels ,(reverse function-bindings)
160+
(destructuring-bind ,(first binding) ,(second binding)
161+
,@(walk-binding-body rest)))))
162+
(t
163+
(collect
164+
`(destructuring-bind ,(first binding) ,(second binding)
165+
,@(walk-binding-body rest))))))
166+
((nil)
167+
(cond
168+
(variable-bindings
169+
(collect
170+
`(let* ,(reverse variable-bindings)
171+
,this
172+
,@(walk-binding-body rest))))
173+
(function-bindings
174+
(collect
175+
`(labels ,(reverse function-bindings)
176+
,this
177+
,@(walk-binding-body rest))))
178+
(t
179+
(collect this)
180+
(wbb rest '() '()))))))))
181+
(variable-bindings
182+
;; hit end of body with pending variables: this only matters
183+
;; for side-effect
184+
(collect `(let* ,(reverse variable-bindings))))
185+
(function-bindings
186+
;; Pending functions, matters even less
187+
(collect `(labels ,(reverse function-bindings))))))))
188+
189+
(defmacro binding (&body forms)
190+
;; The macro itself
191+
(let ((expanded (walk-binding-body forms)))
192+
(if (= (length expanded) 1)
193+
(first expanded)
194+
`(progn ,@expanded))))
195+
196+
;;; Rudimentary sanity tests
197+
;;;
198+
(dolist (form/expansion
199+
'(((binding
200+
(bind a 1)
201+
(bind b 2)
202+
(values a b))
203+
(let* ((a 1) (b 2))
204+
(values a b)))
205+
((binding
206+
1
207+
(bind b 2)
208+
b)
209+
(progn
210+
1
211+
(let* ((b 2))
212+
b)))
213+
((binding
214+
(bind (f &rest args) args)
215+
(f 1 3))
216+
(labels ((f (&rest args) args))
217+
(f 1 3)))
218+
((binding
219+
(bind/values (a b) (values 1 2))
220+
(values a b))
221+
(multiple-value-bind (a b) (values 1 2)
222+
(values a b)))
223+
((binding
224+
(bind/values (a b) 1 2)
225+
(values a b))
226+
(multiple-value-bind (a b) (values 1 2)
227+
(values a b)))
228+
((binding
229+
(bind/destructuring (a &rest b) (list 1 2))
230+
(values a b))
231+
(destructuring-bind (a &rest b) (list 1 2)
232+
(values a b)))))
233+
(destructuring-bind (form expansion) form/expansion
234+
(unless (equal (macroexpand-1 form) expansion)
235+
(warn "~S expanded to ~S, not ~S"
236+
form (macroexpand-1 form) expansion))))

org.tfeb.hax.asd

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
(defsystem "org.tfeb.hax"
77
:description "TFEB hax"
8-
:version "1.0.0"
8+
:version "1.1.0"
99
:author "Tim Bradshaw"
1010
:license "MIT"
1111
:homepage "https://github.com/tfeb/tfeb-lisp-hax"
@@ -23,6 +23,8 @@
2323
(:file "comment-form")
2424
(:file "define-functions")
2525
(:file "trace-macroexpand")
26+
(:file "binding"
27+
:depends-on ("collecting" "iterate"))
2628
(:file "hax-cometh"
2729
:depends-on ("collecting" "wrapping-standard"
2830
"iterate" "dynamic-state" "memoize"

trace-macroexpand.lisp

+4-4
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ The return value is ignored.")
176176
:format-control "No wrapped *MACROEXPAND-HOOK*?")
177177
(continue ()
178178
:report "Install FUNCALL as the wrapped hook"
179-
(setq *wrapped-macroexpand-hook* 'funcall))
179+
(setf *wrapped-macroexpand-hook* 'funcall))
180180
(store-value (v)
181181
:report "Set the wrapped hook to a value"
182182
:interactive (lambda ()
183183
(format *query-io* "~&Value for wrapped hook: ")
184184
(list (read *query-io*)))
185-
(setq *wrapped-macroexpand-hook* v))))
185+
(setf *wrapped-macroexpand-hook* v))))
186186
(if (trace-macroexpand-trace-p macro-function macro-form environment)
187187
(let ((expanded-form (funcall *wrapped-macroexpand-hook*
188188
macro-function macro-form environment)))
@@ -213,14 +213,14 @@ The return value is ignored.")
213213
:format-control "Tracing on but no wrapped *MACROEXPAND-HOOK*?") ;
214214
(continue ()
215215
:report "Install FUNCALL as the wrapped hook"
216-
(setq *wrapped-macroexpand-hook* 'funcall)
216+
(setf *wrapped-macroexpand-hook* 'funcall)
217217
(values nil t))
218218
(store-value (v)
219219
:report "Set the wrapped hook to a value"
220220
:interactive (lambda ()
221221
(format *query-io* "~&Value for wrapped hook: ")
222222
(list (read *query-io*)))
223-
(setq *wrapped-macroexpand-hook* v)
223+
(setf *wrapped-macroexpand-hook* v)
224224
(values nil t))))
225225
(when *wrapped-macroexpand-hook*
226226
(restart-case

0 commit comments

Comments
 (0)