Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bootstrap syntax-case and quasiquote using racket #115

Closed
wants to merge 35 commits into from
Closed
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
348ce88
use racket to bootstrap dot-dot-dot.kl
gelisam Oct 9, 2020
6b2470d
pretty-print generated code
gelisam Oct 9, 2020
f6fc855
generate error message if no case matches
gelisam Oct 10, 2020
c9e0cb8
longer test
gelisam Oct 10, 2020
dfc756f
intermediate-quasiquote
gelisam Oct 10, 2020
4d00133
dot-dot-dot.golden
gelisam Oct 10, 2020
cbaa7a3
move {pair,append}-list-syntax to list-syntax.kl
gelisam Oct 10, 2020
de8ec1c
simplify my-macro using intermediate-quasiquote
gelisam Oct 10, 2020
5cfdc0e
split define-syntax into define-keywords and syntax-case
gelisam Oct 11, 2020
8a6eb69
racket-syntax-case matches on a racket-stx
gelisam Oct 11, 2020
f1d107a
no more raw-stx magic
gelisam Oct 11, 2020
8ed35e1
no more , magic
gelisam Oct 11, 2020
ba32433
don't use racket-syntax-case
gelisam Oct 11, 2020
81ca047
use the same syntax in generate-syntax-case and -quasiquote
gelisam Oct 11, 2020
4453fed
only compare identifiers with free-identifier=?
gelisam Oct 12, 2020
165fc3e
expose generate-quasiquote helper
gelisam Oct 12, 2020
68841e4
drop some raw- prefixes
gelisam Oct 12, 2020
bec8e4d
fancy-quasiquote
gelisam Oct 12, 2020
2b176d1
use the loc of the `(...)
gelisam Oct 12, 2020
6bf4c3b
simplify macros using fancy-quasiquote
gelisam Oct 12, 2020
bb50c7c
simplify fancy-quasiquote
gelisam Oct 12, 2020
80366b0
avoid capturing 'failure-cc'
gelisam Oct 13, 2020
1335aa5
don't use the (,@(list xs ...) x) pattern
gelisam Oct 13, 2020
2b09cd5
drop generate-quasiquote
gelisam Oct 15, 2020
6b438e5
rename generate-quasiquote-inside to generate-quasiquote
gelisam Oct 15, 2020
58ef435
auto-splice
gelisam Oct 15, 2020
d7b8fbb
fancy-syntax-case
gelisam Oct 17, 2020
16838d8
only evaluate syntax-case's input expression once
gelisam Oct 17, 2020
9b91b79
use the shorter names in error-messages
gelisam Oct 17, 2020
e18f263
simplify nested syntax-cases
gelisam Oct 18, 2020
19e933b
guards
gelisam Oct 18, 2020
0b52c86
simplify using guards
gelisam Oct 18, 2020
f2b3108
remove unsused generate-define-syntax
gelisam Oct 18, 2020
a6ea37f
inline barely-used generate-define-keywords
gelisam Oct 18, 2020
d2f9680
explain the new plan
gelisam Mar 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
476 changes: 476 additions & 0 deletions bootstrap.rkt

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions bootstrap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# bootstrapping

This file explains why we use Racket to generate some Klister code in order to
bootstrap our library of useful macros.


## the problem

The Klister `#kernel` provides a `syntax-case` macro, which I'll call
`raw-syntax-case` in this file. `raw-syntax-case` is minimalist: it only
supports shallow patterns. The idea is that deep patterns can and should be
implemented as a library, as a macro which I'll call `fancy-syntax-case`.

We must define `fancy-syntax-case` as a macro which expands to a number of calls
to `raw-syntax-case`. Because `fancy-syntax-case` is not yet defined, that macro
definition will need to pattern-match on its input using `raw-syntax-case`.
That's a bummer, because that makes the macro definition long and hard to read.

(define-syntax (fancy-syntax-case stx)
(raw-syntax-case stx
[(cons _ scrutinee-keywords-cases)
(raw-syntax-case scrutinee-keywords-cases
[(cons scrutinee keywords-cases)
(raw-syntax-case keywords-cases
[(cons keywords cases)
...etc...])])]))

I would prefer to write that macro definition using `fancy-syntax-case` itself!
That would make the code much shorter and more readable.

(define-syntax (fancy-syntax-case stx)
(fancy-syntax-case stx
[(_ ,scrutinee (,(keywords ... )) ,(cases ...))
...etc...]))

That sounds impossible, but there is a way!


## the solution

The trick is to write the short
readable definition that we want to write and to convert it into the long
unreadable definition. Writing this transformation using Klister would again
require using `raw-syntax-case`, so we use Racket instead.

We thus want to write a Racket program which expands `fancy-syntax-case` calls
into a number of calls to `raw-syntax-case`. But wait, we already have a program
which does that, it's the short readable definition we just wrote! Rather than
reimplement this Klister program in Racket, let's automatically translate this
program to Racket via a Racket `#lang`. Since the program assumes that
`fancy-syntax-case` already exists, this `#lang` must be a version of Klister in
which `fancy-syntax-case` is builtin. Thankfully, Racket's `syntax-case` (which
I will call `racket-syntax-case`) is already quite fancy, so we only need to
translate `fancy-syntax-case` calls into `racket-syntax-case` calls. We can do
this by writing a Racket macro.

The overall picture is that we used to have one daunting task:

1. Use `raw-syntax-case` to expand `fancy-syntax-case` into a number of calls
to `raw-syntax-case`.

And we now have two easier tasks:

1. Use `fancy-syntax-case` to expand `fancy-syntax-case` into a number of calls
to `raw-syntax-case`.
2. Use `racket-syntax-case` to expand `fancy-syntax-case` into a call to
`racket-syntax-case`.


# the scope

The above argument also applies to `quasiquote`, which is also very useful when
defining other macros. By defining both `fancy-syntax-case` and
`fancy-quasiquote` at the same time using the above technique, we get to use
both `fancy-syntax-case` and `fancy-quasiquote` when defining both macros. This
is thus a technique which becomes more useful as we define more macros
simulaneously. We may thus add more macros to the list in the future.
32 changes: 13 additions & 19 deletions examples/bool.kl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#lang "prelude.kl"

(import (shift "prelude.kl" 1))
(import (shift "quasiquote.kl" 1))
(import (shift "dot-dot-dot.kl" 1))

(define not
(lambda (b)
@@ -16,24 +16,18 @@
(if x (true) y)))

(define-macros
((and (lambda (stx)
(syntax-case stx
((cons _ args)
(syntax-case args
(()
(pure '(true)))
((cons x xs)
(pure `(binary-and ,x
,(cons-list-syntax 'and xs stx)))))))))
(or (lambda (stx)
(syntax-case stx
((cons _ args)
(syntax-case args
(()
(pure '(false)))
((cons x xs)
(pure `(binary-or ,x
,(cons-list-syntax 'or xs stx)))))))))))
([and (lambda (stx)
(syntax-case stx ()
[(_)
(pure '(true))]
[(_ ,x ,xs ...)
(pure `(binary-and ,x (and ,xs ...)))]))]
[or (lambda (stx)
(syntax-case stx ()
[(_)
(pure '(false))]
[(_ ,x ,xs ...)
(pure `(binary-or ,x (or ,xs ...)))]))]))

(example (binary-and (false) (false)))
(example (binary-and (false) (true)))
22 changes: 11 additions & 11 deletions examples/datatype-macro.kl
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
(import (shift "prelude.kl" 1))
(import (shift "lispy-do.kl" 1))
(import (shift "let.kl" 1))
(import (shift "quasiquote.kl" 1))
(import (shift "dot-dot-dot.kl" 1))
(import (shift (only "list-syntax.kl" map) 1))


@@ -24,13 +24,12 @@
(define-macros
([list
(lambda (stx)
(syntax-case stx
[(cons _ more)
(syntax-case more
[()
(pure (replace-loc more '(nil)))]
[(cons x xs)
(pure (quasiquote/loc more (:: ,x ,(cons-list-syntax 'list xs xs))))])]))]))
(syntax-case stx ()
[(_)
(pure (replace-loc stx '(nil)))]
[(_ ,x ,xs ...)
(pure (replace-loc stx
`(:: ,x (list ,xs ...))))]))]))

(example (reverse (:: 1 (:: 2 (:: 3 (nil))))))

@@ -42,9 +41,10 @@
(define-macros
([head
(lambda (stx)
(syntax-case stx
[(list (_ x))
(pure (quasiquote/loc stx (:: ,x xs)))]))]))
(syntax-case stx ()
[(_ ,x)
(pure (replace-loc stx
`(:: ,x xs)))]))]))

(example (case (reverse null) [null 'a]))

2 changes: 1 addition & 1 deletion examples/define-syntax-rule.golden
Original file line number Diff line number Diff line change
@@ -1 +1 @@
#[define-syntax-rule.kl:46.33-46.36]<bar> : Syntax
#[define-syntax-rule.kl:42.33-42.36]<bar> : Syntax
54 changes: 25 additions & 29 deletions examples/define-syntax-rule.kl
Original file line number Diff line number Diff line change
@@ -1,44 +1,40 @@
#lang "prelude.kl"

(import (rename (shift "prelude.kl" 1) [syntax-case raw-syntax-case]))
(import (shift "prelude.kl" 1))
(import (shift "do.kl" 1))
(import (shift "list-syntax.kl" 1))
(import (shift "quasiquote.kl" 1))
(import (shift "dot-dot-dot.kl" 1))
(import (shift "syntax.kl" 1))

(define-macros
((define-macro
([define-macro
(lambda (stx)
(syntax-case stx
((list (_ pattern body))
(syntax-case pattern
((cons macro-name args)
(pure `(define-macros
((,macro-name (lambda (stx)
(syntax-case stx
((list ,pattern)
,body)))))))))))))))
(syntax-case stx ()
[(_ (,macro-name ,args ...) ,body)
(pure `(define-macros
([,macro-name
(lambda (stx)
(raw-syntax-case stx
Copy link
Owner Author

@gelisam gelisam Oct 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still using raw-syntax-case here so that I don't have to generate a variant of args in which every argument is wrapped in an unquote. I guess that's one small downside of fancy-syntax-case's unorthodox syntax.

[(list (_ ,args ...))
,body]))])))]))]))

(define-macros
((define-syntax-rule
([define-syntax-rule
(lambda (stx)
(syntax-case stx
((list (_ pattern template))
(syntax-case pattern
((cons macro-name args)
(do (unquoted-template <- (foldlM (lambda (t arg)
(replace-identifier
arg
(list-syntax ('unquote arg) stx)
t))
template
args))
(quasiquoted-template <- (pure (list-syntax ('quasiquote unquoted-template) stx)))
(pure `(define-macros
((,macro-name (lambda (stx)
(syntax-case stx
((list ,pattern)
(pure ,quasiquoted-template)))))))))))))))))
(syntax-case stx ()
[(_ (,macro-name ,args ...) ,template)
(do (unquoted-template <- (foldlM (lambda (t arg)
(replace-identifier arg `(,'unquote ,arg) t))
template
args))
(quasiquoted-template <- (pure `(,'quasiquote ,unquoted-template)))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(pure ``,unquoted-template)

would have worked too, but only because I don't implement the R6RS spec for nested quotations, according to which the result of

(let ([x 'foo])
  ``,x)

should be

`,x

not

`foo

(pure `(define-macros
([,macro-name
(lambda (stx)
(raw-syntax-case stx
[(list (_ ,args ...))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, I am using raw-syntax-case in order to avoid generating a variant of args in which every argument is wrapped in an unquote. In this case, however, it wouldn't have been a big deal to do so, because I am already generating a variant of template in which every argument is wrapped in an unquote.

(pure ,quasiquoted-template)]))]))))]))]))

(define-syntax-rule (lambda2 x y body)
(lambda (x y) body))
Empty file added examples/do-keywords.golden
Empty file.
8 changes: 8 additions & 0 deletions examples/do-keywords.kl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#lang "prelude.kl"

(import (shift "prelude.kl" 1))

(define-macros
([<- (lambda (stx) (syntax-error (quote "<- used out of context") stx))]))

(export <-)
39 changes: 15 additions & 24 deletions examples/do.kl
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#lang "prelude.kl"

(import (shift "prelude.kl" 1))
(import (shift "quasiquote.kl" 1))
(import (shift "let.kl" 1))
(import (shift "dot-dot-dot.kl" 1))
(import (shift "do-keywords.kl" 1))
(import "do-keywords.kl")
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These last two imports are a symptom of something really sad about fancy-syntax-case: keywords must be visible both at the macro-definition phase and at the macro-caller's phase. This can either be done by defining them twice (once with meta) or by defining them in a separate module and them importing that module twice. Either way, it's really annoying.

The reason they need to be available in the macro-definition phase is because fancy-syntax-case uses free-identifier=? to check that every keyword used in the patterns have been listed in the keyword list. But if the keyword isn't in scope, then the occurrence of the keyword in the keyword list is not free-identifier=? to the occurrence in the pattern, resulting in spurious did you mean to add the symbol to the keyword list? errors.

The reason they need to be available in the macro-caller's phase is because the generated code uses free-identifier=? to compare the input syntax object against the various patterns, including against keywords. So keywords must be in scope in the phase in which the generated code runs or they won't be free-identifier=?.

One solution could be to drop that keyword list, and to trust that fancy-syntax-case's caller doesn't accidentally write [(_ cond then-expr else-expr) ...] instead of [(_ ,cond ,then-expr ,else-expr) ...]. But since this is an unorthodox syntax, I expect that mistake to be quite common!

My preferred solution would be to add a new primitive, define-keyword, which would bind an identifier at all phases. It would hardcode the value to which it is bound to a macro complaining that the keyword is used out of context, and thus we won't have to worry about things like whether the closure of the definition includes values which only exist at some phases. Would that make sense?


(define-macros
-- (do (x <- foo)
@@ -14,27 +15,17 @@
-- (>>= (bar x) (lambda (_)
-- (>>= (baz x) (lambda (y)
-- (quux x y)))))))
((<-
(lambda (stx)
(syntax-error (quote "<- used out of context") stx)))
(do (lambda (stx)
(syntax-case stx
((cons _ all-actions)
(syntax-case all-actions
((list (last-action))
(pure last-action))
((cons first-action actions)
(let ((otherwise (pure `(>>= ,first-action (lambda (_)
,(cons-list-syntax 'do actions stx))))))
(syntax-case first-action
((list (var <-? action))
(>>= (free-identifier=? '<- <-?)
(lambda (isArrow)
(if isArrow
(pure `(>>= ,action (lambda (,var)
,(cons-list-syntax 'do actions stx))))
otherwise))))
(_
otherwise)))))))))))
([do (lambda (stx)
(syntax-case stx (<-)
[(_ ,last-action)
(pure last-action)]
[(_ (,var <- ,action) ,actions ...)
(pure `(>>= ,action
(lambda (,var)
(do ,actions ...))))]
[(_ ,action ,actions ...)
(pure `(>>= ,action
(lambda (_)
(do ,actions ...))))]))]))

(export <- do)
Empty file.
8 changes: 8 additions & 0 deletions examples/dot-dot-dot-test-keywords.kl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#lang "prelude.kl"

(import (shift "prelude.kl" 1))

(define-macros
([keyword (lambda (stx) (syntax-error '"keyword used out of context" stx))]))

(export keyword)
5 changes: 5 additions & 0 deletions examples/dot-dot-dot-test.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[dot-dot-dot-test.kl:17.17-17.31]<(1 2 3 4)> : Syntax
#[dot-dot-dot-test.kl:20.17-20.75]
<(keyword-identifier-prefixed bar baz end-of-list)> : Syntax
#[dot-dot-dot-test.kl:22.17-22.58]<(keyword-prefixed 3 4 end-of-list)> : Syntax
#[dot-dot-dot-test.kl:24.17-24.52]<(ordinary-list foo bar baz end-of-list)> : Syntax
29 changes: 29 additions & 0 deletions examples/dot-dot-dot-test.kl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#lang "prelude.kl"

(import (rename (shift "prelude.kl" 1)
[syntax-case raw-syntax-case]
[unquote raw-unquote]
[... raw-...]))
(import "dot-dot-dot-test-keywords.kl")
(import (shift "dot-dot-dot-test-keywords.kl" 1))
(import (shift "dot-dot-dot.kl" 1))
(import (shift "identifier.kl" 1))

(define-macros
([my-macro
(lambda (stx)
(syntax-case stx (keyword)
[(_ ((,a ,b) (,c ,d)))
(pure `'(,a ,b ,c ,d))]
[(_ (keyword ,head ,tail ...))
(pure (identifier? head))
(pure `'(keyword-identifier-prefixed ,head ,tail ... end-of-list))]
[(_ (keyword ,tail ...))
(pure `'(keyword-prefixed ,tail ... end-of-list))]
[(_ (,e ...))
(pure `'(ordinary-list ,e ... end-of-list))]))]))

(example (my-macro ((1 2) (3 4))))
(example (my-macro (keyword bar baz)))
(example (my-macro (keyword 3 4)))
(example (my-macro (foo bar baz)))
Empty file added examples/dot-dot-dot.golden
Empty file.
Loading