├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── AUTHORS.md
├── LICENSE
├── README.md
├── demo.rkt
├── parendown-doc
├── info.rkt
└── scribblings
│ └── parendown.scrbl
├── parendown-lib
├── info.rkt
├── lang
│ └── reader.rkt
├── main.rkt
└── slash
│ └── lang
│ └── reader.rkt
├── parendown-test
├── info.rkt
├── tests.rkt
└── tests
│ └── slash.rkt
└── parendown
└── info.rkt
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | vars:
7 | runs-on: ubuntu-latest
8 | outputs:
9 | package: parendown
10 | steps:
11 | - name: Initialize variables
12 | run: "true"
13 | test:
14 | needs: vars
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | racket-variant: ["BC", "CS"]
19 | racket-version: ["8.3", "stable", "current"]
20 | racket-catalogs-id: ["pkgs"]
21 | racket-catalogs: [""]
22 | include:
23 | # We build once against racksnaps (https://racksnaps.defn.io/)
24 | # so that it's easier to track down working dependency
25 | # versions. This is essentially our package-lock.json or
26 | # Cargo.lock information.
27 | #
28 | - racket-variant: "CS"
29 | racket-version: "8.3"
30 | racket-catalogs-id: "racksnaps"
31 | racket-catalogs: |
32 | https://download.racket-lang.org/releases/8.3/catalog/,
33 | https://racksnaps.defn.io/built-snapshots/2022/01/23/catalog/
34 | steps:
35 | - name: Checkout
36 | uses: actions/checkout@v4
37 |
38 | - name: Install Racket '${{ matrix.racket-version }}'
39 | uses: Bogdanp/setup-racket@v1.11
40 | with:
41 | architecture: x64
42 | distribution: full
43 | variant: ${{ matrix.racket-variant }}
44 | version: ${{ matrix.racket-version }}
45 | catalogs: ${{ matrix.racket-catalogs }}
46 |
47 | # This is based on
48 | # https://github.com/soegaard/sketching/blob/bc24517203d5cae9019ee18491bb076b7299bbef/.github/workflows/push.yml
49 | # and we're using it so that it's possible to set the
50 | # catalogs without getting a permission error.
51 | dest: '"${HOME}/racketdist"'
52 | sudo: never
53 |
54 | # This is based on
55 | # https://github.com/Bogdanp/setup-racket-cache-example
56 | - name: Record the Racket version
57 | run: racket --version | tee .racket-version
58 | - name: Obtain cached Racket packages, if available
59 | uses: actions/cache@v4
60 | with:
61 | path: |
62 | ~/.cache/racket
63 | ~/.local/share/racket
64 | key: "\
65 | ${{ runner.os }}-\
66 | ${{ hashFiles('.racket-version') }}-\
67 | ${{ matrix.racket-catalogs-id }}"
68 |
69 | # We uninstall the packages if they're already installed. This
70 | # can happen if the GitHub Actions cache is already populated with
71 | # them.
72 | - name: Uninstall `${{ needs.vars.outputs.package }}`
73 | run: raco pkg remove --no-docs --batch ${{ needs.vars.outputs.package }} || true
74 | - name: Uninstall `${{ needs.vars.outputs.package }}-test`
75 | run: raco pkg remove --no-docs --batch ${{ needs.vars.outputs.package }}-test || true
76 | - name: Uninstall `${{ needs.vars.outputs.package }}-doc`
77 | run: raco pkg remove --no-docs --batch ${{ needs.vars.outputs.package }}-doc || true
78 | - name: Uninstall `${{ needs.vars.outputs.package }}-lib`
79 | run: raco pkg remove --no-docs --batch ${{ needs.vars.outputs.package }}-lib || true
80 |
81 | # We install each package directory as a linked package, and we
82 | # automatically fetch all the dependencies. We don't build the
83 | # docs yet; we'll do that later when we're recompiling the
84 | # project to check its dependencies.
85 | #
86 | # The order in which we install these packages matters; if we
87 | # install a package before one it depends on, the command will
88 | # fetch a stale copy of the dependency from the Racket package
89 | # index.
90 | #
91 | - name: Install `${{ matrix.package }}-lib` and its dependencies
92 | run: raco pkg install --auto --no-docs --batch --link "./${{ needs.vars.outputs.package }}-lib/"
93 | - name: Install `${{ needs.vars.outputs.package }}-doc` and its dependencies
94 | run: raco pkg install --auto --no-docs --batch --link "./${{ needs.vars.outputs.package }}-doc/"
95 | - name: Install `${{ needs.vars.outputs.package }}-test` and its dependencies
96 | run: raco pkg install --auto --no-docs --batch --link "./${{ needs.vars.outputs.package }}-test/"
97 | - name: Install `${{ needs.vars.outputs.package }}` and its dependencies
98 | run: raco pkg install --auto --no-docs --batch --link "./${{ needs.vars.outputs.package }}/"
99 |
100 | # We recompile the collection (the single collection which all
101 | # these packages populate) and check that the package
102 | # dependencies declared in each info.rkt are correct.
103 | - name: Recompile to check dependencies, and build documentation
104 | run: raco setup --check-pkg-deps --unused-pkg-deps "${{ needs.vars.outputs.package }}"
105 |
106 | # We run tests according to the way the DrDr continuous testing
107 | # system does. This imitates the settings used by the Racket
108 | # package index at .
109 | - name: Test `${{ needs.vars.outputs.package }}-lib`
110 | run: raco test --drdr --package "${{ needs.vars.outputs.package }}-lib"
111 | - name: Test `${{ needs.vars.outputs.package }}-doc`
112 | run: raco test --drdr --package "${{ needs.vars.outputs.package }}-doc"
113 | - name: Test `${{ needs.vars.outputs.package }}-test`
114 | run: raco test --drdr --package "${{ needs.vars.outputs.package }}-test"
115 | - name: Test `${{ needs.vars.outputs.package }}`
116 | run: raco test --drdr --package "${{ needs.vars.outputs.package }}"
117 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Added by raco when building the package.
2 | compiled/
3 | doc/
4 |
5 | # Added by DrRacket.
6 | *.bak
7 |
--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
1 | Parendown for Racket is authored by:
2 |
3 | * Ross Angle
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Parendown
2 |
3 | [](https://github.com/lathe/parendown-for-racket/actions/workflows/ci.yml)
4 |
5 | Parendown adds *weak opening parentheses* to Racket. It's a syntax sugar, and it's as simple as this:
6 |
7 | ```
8 | (a b #/c d) becomes (a b (c d))
9 | ```
10 |
11 | It has some pretty straightforward uses in terms of saving closing parentheses. When we write `(not #/equal? a b)`, it's as though we had an operation called "`not #/equal?`" already; we wouldn't get much benefit from defining `unequal?` except to shave a few characters off the name. If Racket didn't supply `(last x)`, we could still write `(car #/reverse x)` a few times before we got around to defining it ourselves.
12 |
13 | Those simple use cases were possible in Arc using its `a:b` syntax; they could be written as `(no:iso a b)` and `(car:rev x)` respectively. Parendown was inspired by experience using Arc, but its syntax is generalized to allow more (or less) than one list element before the `#/`. This generalization leads to several benefits.
14 |
15 | In Racket, Parendown is a language extension which adds a `#/` reader syntax. You can use it with `#lang parendown `, like so:
16 |
17 | ```
18 | #lang parendown racket/base
19 |
20 | (displayln #/string-append "Hello, " "world!")
21 | ```
22 |
23 | The `#/` reader syntax is designed to behave as much like the standard opening paren `(` as possible, but doesn't consume the closing paren `)`. It leaves that paren in the stream. Since only a strong opening paren `(` will consume a closing paren, this means a single closing paren `)` will tend to match up with zero or more weak opening parens `#/` *on its way* to matching up with the opening paren `(`.
24 |
25 | Although it doesn't usually make a difference in Racket code, the weak opening paren `#/` will also match up with square and curly closing brackets as though it were an occurrence of the appropriate square or curly opening bracket:
26 |
27 | ```
28 | [a b #/c d] becomes [a b [c d]]
29 | {a b #/c d} becomes {a b {c d}}
30 | ```
31 |
32 | As simple as Parendown is as a syntax sugar, its influence casts ripples over a whole language design. It singlehandedly leads to several different quality-of-life improvements throughout the use of the Racket language. It solves several things at once that have usually been solved with multiple specialized sugars, DSLs, or even runtime features.
33 |
34 |
35 | ## Parendown and higher-order functions vs. macros
36 |
37 | For instance, a common pattern in Lisp-based languages is that for every higher-order function, there tends to be a macro to make that function easier to use. Racket has some macros (aka syntax transformers) like `for/list` for iterating over sequences:
38 |
39 | ```
40 | (for/list ([a (in-list (list 1 2 3))]
41 | [b (in-list (list 10 20 30))])
42 | (* a b))
43 | ```
44 |
45 | Using Parendown, and using some `list-bind` and `list-map` operations from [Lathe Comforts](https://github.com/lathe/lathe-comforts-for-racket) (a library which is designed for use with Parendown), the way to write this using higher-order functions becomes just about as concise as the macro version:
46 |
47 | ```
48 | (list-bind (list 1 2 3) #/lambda (a)
49 | #/list-map (list 10 20 30) #/lambda (b)
50 | (* a b))
51 | ```
52 |
53 | Since this code takes up the same number of lines and the same amount of indentation, it usually has the same impact on the large-scale brevity of the codebase.
54 |
55 | The `for/list` syntax is an example of something languages like Haskell use monadic style to achieve, including the monadic `do` DSL:
56 |
57 | ```
58 | -- Haskell, using monadic `do` notation:
59 | do a <- [1, 2, 3]
60 | b <- [10, 20, 30]
61 | return (a * b)
62 |
63 | -- A more manual monadic style in Haskell using lambdas (\var -> body)
64 | [1, 2, 3] >>= \a ->
65 | [10, 20, 30] >>= \b ->
66 | return (a * b)
67 | ```
68 |
69 | Monadic style in general acts as a way to build continuation-passing style programs. A value of a monadic type (at least in a higher-order language) is something which can have continuations passed to it; the property of being monadic tells us little more than that a type supports a well-behaved operation (called "bind" or `>>=`) for taking a value of that type and passing a continuation to it. (Lists are like very simple computations which consist of many possible "results," which is why they're a well-behaved monadic type.)
70 |
71 | Using Parendown, we've already seen how to write code that's roughly in parity with Haskell's monadic style. We've only seen this technique applied to list construction, but it does come in handy in other places we use continuation-passing style as well. However, those situations actually don't come up all that much in Racket, since Racket has first-class continuations, so we'll stick to the list example.
72 |
73 | Continuation-passing style can be annoying to deal with for several reasons; it causes code to become very nested, full of intermediate variables, and sequentialized. Parendown helps specifically with the nesting. If we write the `list-bind` and `list-map` without Parendown, we can see a pyramid forming that pushes our code to the right as we go along:
74 |
75 | ```
76 | (list-bind (list 1 2 3)
77 | (lambda (a)
78 | (list-map (list 10 20 30)
79 | (lambda (b)
80 | (* a b)))))
81 | ```
82 |
83 | In a more traditional Lispy style, the pyramid might be even more voluminous and unruly. Here we use a common Lisp indentation style and use Racket's own `append-map` and `map` operations rather than using Lathe Comforts:
84 |
85 | ```
86 | (append-map (lambda (a)
87 | (map (lambda (b)
88 | (* a b))
89 | (list 10 20 30)))
90 | (list 1 2 3))
91 | ```
92 |
93 | The sparse style isn't all bad. It gives the code some distinct visual landmarks, and the generous indentation makes it easy to spot all the arguments to `append-map` at a glance. But the arguments in the above `list-bind` example are easy to spot at a glance thanks to a different feature: That all but one of them fits on a single line. In more complex situations, where the layers of nesting are deep, the `list-bind` style -- especially with Parendown in there to flatten the indentation -- remains just as readable, whereas the `append-map` call ends up having its second argument stranded on a later screenful of code, out of sight, making it no longer easy to associate it with `append-map` at a single glance.
94 |
95 | The `for/list` macro isn't the only example of a Racket sugar that's made somewhat redundant by the Parendown sugar. Here are a few more in the same vein as `for/list`, where the Parendown sugar makes it easy to pass in a callback or write multiple layers of functionality without introducing an extra layer of indentation:
96 |
97 | ```
98 | ; Without Parendown:
99 | (let/cc k
100 | ...)
101 |
102 | ; Without `let/cc`, but with Parendown:
103 | (call/cc #/lambda (k)
104 | ...)
105 |
106 |
107 | ; Without Parendown:
108 | (let* ([a ...]
109 | [b ...])
110 | ...)
111 |
112 | ; Without `let*`, but with Parendown:
113 | (let ([a ...])
114 | #/let ([b ...])
115 | ...)
116 |
117 |
118 | ; Without Parendown:
119 | (cond
120 | [(list? x) (length x)]
121 | [(integer? x) x]
122 | [else 0])
123 |
124 | ; Without `cond`, but with Parendown:
125 | (if (list? x) (length x)
126 | #/if (integer? x) x
127 | 0)
128 | ```
129 |
130 | The `let` and `if` synergies are especially nice. Code that uses Parendown can very easily set up intermediate variables with `let` or early exit conditions with `if` without introducing a single indentation level. These are conveniences which a Racket programmer might otherwise consider achieving using locally scoped `(define ...)` forms or escape continuations, but Parendown makes it unnecessary to bring in those complex techniques.
131 |
132 |
133 | ## Parendown's other uses
134 |
135 | Parendown has a few other uses, although these start to be less compelling.
136 |
137 | Parendown can occasionally have advantages similar to infix syntax:
138 |
139 | ```
140 | ; In an infix lang, we may refactor like this, making a small edit:
141 | position + width
142 | position + 0.5 * width
143 |
144 | ; With Parendown, we may refactor like this, making a small edit:
145 | (+ position width)
146 | (+ position #/* 0.5 width)
147 | ```
148 |
149 | If there's a useful variable-arity operation, Parendown can sometimes help us tinker around with possibilities for it before we realize what its design should be:
150 |
151 | ```
152 | ; Without Parendown:
153 | (* a b c d)
154 |
155 | ; Without variable-arity `*` but with Parendown:
156 | (* a #/* b #/* c d)
157 |
158 |
159 | ; Without Parendown:
160 | (- a b c d)
161 |
162 | ; Without variable-arity `-` but with Parendown:
163 | (- a #/+ b c d)
164 |
165 |
166 | ; Without Parendown:
167 | (list a b c)
168 |
169 | ; If for some reason we didn't have `list` but had Parendown:
170 | (cons a #/cons b #/cons c null)
171 | ```
172 |
173 | In fact, once we have `list` in the language, it has such synergy with Parendwn that we might neglect to define any other variable-arity functions for a while:
174 |
175 | ```
176 | ; Without Parendown:
177 | (append
178 | a
179 | b
180 | c)
181 |
182 | ; Without `append`, but with Parendown:
183 | (append* #/list
184 | a
185 | b
186 | c)
187 | ```
188 |
189 | Lisp syntax is known for having very uniform notation, but an exception is made in almost every Lisp dialect for quotation. With Parendown, quoted lists could use roughly the same amount of code lines and indentation as ever, without the need for a specialized notation:
190 |
191 | ```
192 | ; Without Parendown:
193 | '(/
194 | (+ (- b) (sqrt (- (expt b 2) (* 4 a c))))
195 | (* 2 a))
196 |
197 | ; Without the quotation syntax, but with Parendown:
198 | (quote #/ /
199 | (+ (- b) (sqrt (- (expt b 2) (* 4 a c))))
200 | (* 2 a))
201 | ```
202 |
203 | One of the hallmark syntax sugars/DSLs of Clojure is its suite of *threading macros*, which allow long sequences of functional transformations to be written in a step-by-step way. (Alexis King has written a Clojure-inspired [`threading` package for Racket](https://github.com/lexi-lambda/threading), which we'll use for this example.) One of the possible benefits of this step-by-step juxtaposition is to avoid an indentation pyramid, so what comes naturally in Parendown isn't far off from the Clojure threaded style:
204 |
205 | ```
206 | ; Without Parendown, with Clojure-like `~>>` from package `threading`:
207 | (~>> users
208 | (append-map user-friends)
209 | (filter (lambda (user) (not (user-banned? user))))
210 | (map user-name)
211 | string->immutable-string
212 | (foldl
213 | (lambda (name result) (hash-update result name add1 0))
214 | (hash)))
215 |
216 | ; Without `~>>` but with Parendown (writing steps from last to first):
217 | (foldl
218 | (lambda (name result) (hash-update result name add1 0))
219 | (hash)
220 | #/string->immutable-string
221 | #/map user-name
222 | #/filter (lambda (user) #/not #/user-banned? user)
223 | #/append-map user-friends
224 | users)
225 | ```
226 |
227 | When a flat sequence of steps doesn't emerge on its own, or when we really want the steps to be arranged from first to last like they are in Clojure, it's not hard to approximate that style even without using Parendown:
228 |
229 | ```
230 | ; Without `~>>` but with `let*` (writing steps from first to last):
231 | (let* ([- users]
232 | [- (append-map user-friends -)]
233 | [- (filter (lambda (user) (not (user-banned? user))) -)]
234 | [- (map user-name -)]
235 | [- (string->immutable-string -)]
236 | [- (foldl
237 | (lambda (name result) (hash-update result name add1 0))
238 | (hash)
239 | -)])
240 | -)
241 | ```
242 |
243 |
244 | ## Commentary on Parendown and variable shadowing
245 |
246 | The last example of how to emulate Clojure threading makes use of variable shadowing; it doesn't rely on Parendown at all. Nevertheless, the two features have some interesting overlaps, rooted in their similarities at a syntactic level: The syntactic pattern "variable binding ... shadowing variable binding ... variable usage site" is similar to the pattern `( ... #/ ... )`.
247 |
248 | For both variable shadowing and Parendown, we have a kind of *lexical* state update going on. The stateful entity here is not part of the program's run time operation, but part of the operation of the codebase itself as a maintainable system. A simple and local *edit to the code* can immediately change a valid use of one variable binding into a valid use of a different one (in the case of variable shadowing) or change one well-matched system of parentheses into another (in the case of Parendown's weak opening parens). What makes this in some sense stateful is that there's an entity that has an unchanging identity (the variable name, or the closing paren occurrence), and it has a changing state (the expression or parameter the variable is bound to, or the set of weak opening parens that match up with that closing paren).
249 |
250 | In this way, Parendown and variable shadowing are techniques that should be adopted or avoided on the basis of how the code is edited. One parts of the code may undergo edits in such a way where Parendown's ability to approximate infix syntax comes in handy. Another part may involve two nested variable bindings which could easily use the same name, but for which we expect it to be a mistake if a future maintainer switches one for the other, so it's best for their names to be distinct until further notice. Of course, since a programmer can come in and refactor a variable name or substitute a strong opening paren for a weak one at any time, this kind of decision is always reversible.
251 |
252 |
253 | ## The `parendown/slash` language
254 |
255 | We've chosen `#/` so that Parendown appears seamless with Racket. For many cases, using the syntax `/` is a little nicer. For that purpose, we define `#lang parendown/slash`:
256 |
257 | ```
258 | #lang parendown/slash racket/base
259 |
260 | (displayln /string-append "Hello, " "world!")
261 | ```
262 |
263 | This acts as a non-symbol-terminating readtable extension, so symbols like `syntax/parse` and `any/c` will be usable in the usual way. In order to make sure a `/` weak opening paren isn't treated as part of the preceding symbol, it may be necessary to use whitespace in between.
264 |
265 | Symbols beginning with `/`, such as the division operator `/`, may be more difficult to use with this extension in place. However, they can still be referred to using the alternative notations `\/...` and `|/...|`. In the case of division, that means writing `\/` or `|/|`.
266 |
267 |
268 | ## Installation and use
269 |
270 | This is a library for Racket. To install it from the Racket package index, run `raco pkg install parendown`. Then you can change the `#lang` line of your Racket modules to `#lang parendown `, where `#lang ` is the line you were using before. Since Parendown is sugar for parentheses, it'll be a handy extension to just about any Racket language where parentheses have their usual Racket behavior.
271 |
272 | To install it from source, run `raco pkg install --deps search-auto` from the `parendown-lib/` directory.
273 |
274 | [Documentation for Parendown for Racket](http://docs.racket-lang.org/parendown/index.html) is available at the Racket documentation website, and it's maintained in the `parendown-doc/` directory.
275 |
276 | If you're writing your own reader extensions, you can add Parendown functionality to your readtable like so:
277 |
278 | ```
279 | (require (only-in parendown parendown-readtable-handler))
280 |
281 | (make-readtable (current-readtable) #\/ 'dispatch-macro
282 | parendown-readtable-handler)
283 | ```
284 |
285 | This gives you the opportunity to use a syntax other than `#/` or `/` if you prefer.
286 |
287 | In certain circumstances, it's inconvenient to change the reader. Most of the advantages of Parendown are also available in the form of the `pd` syntax transformer:
288 |
289 | ```
290 | #lang racket/base
291 |
292 | (require (only-in parendown pd))
293 |
294 | (pd / begin
295 | (displayln / string-append "Hello, " "world!")
296 | (displayln / string-append "I can't use " "division!"))
297 | ```
298 |
299 | The `(pd / ...)` form surrounds some code and processes all occurrences of the symbol `/` it encounters. It also lets you switch to something like `(pd % ...)` if you want it to process occurrences of `%` instead, although typically you could just `(define div /)` outside the `pd` form in any case where you need to use division.
300 |
301 | You can use `pd` any number of times, but typically it's sufficient to surround a chunk of code with `(pd / begin ...)`.
302 |
303 | The `pd` form also expands calls of the form `(pd (a b c))` simply to `(a b c)`. This ensures that if the code contains nested calls like `(pd / - / + 1 (pd / add1 / add1 0))`, everything continues to work.
304 |
305 |
306 | ## Related work
307 |
308 | It turns out Hendrik Boom developed the same syntax some time ago (even down to the choice of the character `/`) in an unreleased language described [here](http://topoi.pooq.com/hendrik/ComputerProjects/index.html). Hendrik Boom even used the same indentation style, calling it [tail-indentation](https://groups.google.com/d/msg/racket-users/oLR_7L-g9zc/fZXaMkfQCAAJ) in analogy to tail calls.
309 |
310 | In terms of direct influences, the Parendown syntaxes take primary inspiration from the Arc language's abbreviation of `(a (b c))` as `(a:b c)` (which only worked when `a` and `b` were symbols), as well as a `(scope let a 1 @ let b 2 @ + a b)` syntax [posted by Yuval Lando on Arc Forum](http://arclanguage.org/item?id=11934). Ross Angle (rocketnia) developed some languages (including what's become [Era's Cene language](https://github.com/era-platform/cene-for-racket)) which renamed this `:` to `/` and generalized it. Parendown (another project started by that author) brings that generalized syntax to Racket.
311 |
312 | At some point, Pauan's Nulan project may have used a syntax like this as well.
313 |
314 | The Haskell operator `$` predates at least the Arc syntax, and it has the very similar effect of allowing `(a b $ c d)` instead of `(a b (c d))` for function calls in that language. In fact, the benefits of this sugar in continuation-passing style were known at least as far back as the Haskell 1.2 report from 1992 (page 85):
315 |
316 | ```
317 | -- right-associating infix application operator (useful in continuation-
318 | -- passing style)
319 | ($) :: (a -> b) -> a -> b
320 | f $ x = f x
321 | ```
322 |
323 | As early as 1974, [Interlisp](http://bitsavers.trailing-edge.com/pdf/xerox/interlisp/Interlisp_Reference_Manual_1974.pdf) had a similar behavior. It called `[` and `]` "super-parentheses," and the combination of `[`, `(`, and `]` in Interlisp worked roughly like the combination of `(`, `#/`, and `)` does in a `#lang parendown racket` program:
324 |
325 | ```
326 | The INTERLISP read program treats square brackets as 'super-parentheses': a
327 | right square bracket automatically supplies enough right parentheses to match
328 | back to the last left square bracket (in the expression being read), or if none
329 | has appeared, to match the first left parentheses,
330 | e.g., (A (B (C]=(A (B (C))),
331 | (A [B (C (D] E)=(A (B (C (D))) E).
332 | ```
333 |
334 | [A 2006 paper by Anssi Yli-Jyrä](http://www.linguistics.fi/julkaisut/SKY2006_1/2.6.9.%20YLI-JYRA.pdf) reviews a few different designs, including the Interlisp design. That author ultimately favors the following approach, where this time `[`, `〈`, and `]` serve the same purposes as `(`, `#/`, and `)` serve with Parendown:
335 |
336 | > Krauwer and des Tombe (1981) proposed _condensed labelled bracketing_ that can be defined as follows. Special brackets (here we use angle brackets) mark those initial and final branches that allow an omission of a bracket on one side in their realized markup. The omission is possible on the side where a normal bracket (square bracket) indicates, as a side-effect, the boundary of the phrase covered by the branch. For example, bracketing "[[A B] [C [D]]]" can be replaced with "[A B〉 〈C 〈D]" using this approach.
337 |
--------------------------------------------------------------------------------
/demo.rkt:
--------------------------------------------------------------------------------
1 | #lang parendown racket
2 |
3 | ; demo.rkt
4 | ;
5 | ; A demonstration of the weak opening paren functionality of the
6 | ; Parendown language extension.
7 |
8 | ; Copyright 2017-2018 The Lathe Authors
9 | ;
10 | ; Licensed under the Apache License, Version 2.0 (the "License");
11 | ; you may not use this file except in compliance with the License.
12 | ; You may obtain a copy of the License at
13 | ;
14 | ; http://www.apache.org/licenses/LICENSE-2.0
15 | ;
16 | ; Unless required by applicable law or agreed to in writing,
17 | ; software distributed under the License is distributed on an
18 | ; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
19 | ; either express or implied. See the License for the specific
20 | ; language governing permissions and limitations under the License.
21 |
22 |
23 | (displayln "Hello, world!")
24 | (writeln '(1 2 [7 (3 4) #/4 3 5]))
25 | (displayln #/string-append "Hello, " "world!")
26 |
--------------------------------------------------------------------------------
/parendown-doc/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define collection "parendown")
4 |
5 | (define deps (list "base"))
6 | (define build-deps (list "parendown-lib" "racket-doc" "scribble-lib"))
7 |
8 | (define scribblings
9 | (list (list "scribblings/parendown.scrbl" (list))))
10 |
--------------------------------------------------------------------------------
/parendown-doc/scribblings/parendown.scrbl:
--------------------------------------------------------------------------------
1 | #lang parendown scribble/manual
2 |
3 | @; parendown/scribblings/parendown.scrbl
4 | @;
5 | @; Weak opening paren functionality in the form of a language
6 | @; extension and a library.
7 |
8 | @; Copyright 2018, 2021 The Lathe Authors
9 | @;
10 | @; Licensed under the Apache License, Version 2.0 (the "License");
11 | @; you may not use this file except in compliance with the License.
12 | @; You may obtain a copy of the License at
13 | @;
14 | @; http://www.apache.org/licenses/LICENSE-2.0
15 | @;
16 | @; Unless required by applicable law or agreed to in writing,
17 | @; software distributed under the License is distributed on an
18 | @; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
19 | @; either express or implied. See the License for the specific
20 | @; language governing permissions and limitations under the License.
21 |
22 |
23 | @(require #/for-label racket/base)
24 | @(require #/for-label #/only-in racket/contract/base and/c any any/c)
25 |
26 | @(require #/for-label parendown)
27 |
28 |
29 | @title{Parendown}
30 |
31 | Parendown adds @emph{weak opening parentheses} to Racket in the form of a language extension. A more extensive overview of Parendown's uses can be found in @hyperlink["https://github.com/lathe/parendown-for-racket"]{the README at GitHub}.
32 |
33 | @; TODO: Copy at least a little more of the readme into this blurb.
34 |
35 |
36 |
37 | @table-of-contents[]
38 |
39 |
40 |
41 | @section[#:tag "language-extension"]{The Parendown Language Extension}
42 |
43 | @defmodulelang[parendown]
44 |
45 | The @tt{parendown} language is a language extension. To use it, specify another language after @tt{parendown} on the @hash-lang[] line. That language will have its readtable extended with a @tt{#/} syntax that behaves according to @racket[parendown-readtable-handler].
46 |
47 | @codeblock{
48 | #lang parendown racket/base
49 |
50 | (displayln #/string-append "Hello, " "world!")
51 | }
52 |
53 |
54 | @section[#:tag "parendown/slash"]{The @racketmodname[parendown/slash] Language Extension}
55 |
56 | @defmodulelang[parendown/slash]
57 |
58 | The @tt{parendown/slash} language is a language extension like @tt{parendown}, but with a more streamlined syntax. To use it, specify another language after @tt{parendown/slash} on the @hash-lang[] line. That language will have its readtable extended with a @tt{/} syntax that behaves according to @racket[parendown-readtable-handler].
59 |
60 | This acts as a non-symbol-terminating readtable extension, so symbols like @racketmodname[syntax/parse] and @racket[any/c] will be usable in the usual way. In order to make sure a @tt{/} weak opening paren isn't treated as part of the preceding symbol, it may be necessary to use whitespace in between.
61 |
62 | @codeblock{
63 | #lang parendown/slash racket/base
64 |
65 | (displayln /string-append "Hello, " "world!")
66 | }
67 |
68 | Symbols beginning with @tt{/}, such as the division operator @racket[/], may be more difficult to use with this extension in place. However, they can still be referred to using the alternative notations @tt{\/...} and @tt{|/...|}. In the case of division, that means writing @code{\/} or @code{|/|}.
69 |
70 |
71 | @section[#:tag "parendown-library"]{Parendown as a Library}
72 |
73 | @defmodule[parendown #:link-target? #f]
74 |
75 | There is also a @tt{parendown} module which lets Racket code use some features of Parendown even when they aren't using the @hash-lang[] directly.
76 |
77 | @defform[(pd slash-symbol stx ...)]{
78 | Expands to @racket[(stx ...)], where the lists of each @racket[stx] have been recursively traversed to transform any tails that begin with the symbol @racket[slash-symbol] into tails consisting of a single element, where that element is the list resulting from transforming the rest of that tail.
79 |
80 | For instance, the form
81 |
82 | @racketblock[
83 | (pd _/ begin
84 | (displayln _/ string-append "Hello, " "world!")
85 | (displayln _/ string-append "I can't use " "division!"))
86 | ]
87 |
88 | expands to this:
89 |
90 | @racketblock[
91 | (begin
92 | (displayln (string-append "Hello, " "world!"))
93 | (displayln (string-append "I can't use " "division!")))
94 | ]
95 | }
96 |
97 | @defform[
98 | #:link-target? #f
99 | (pd (stx ...))
100 | ]{
101 | Simply expands to @racket[(stx ...)].
102 |
103 | This is usually the result of the other case of @racket[pd]. For instance, the form
104 |
105 | @racketblock[
106 | (pd _/ begin
107 | (displayln _/ string-append "Hello, " "world!")
108 | (pd _/ displayln _/ string-append "I can't use " "division!"))
109 | ]
110 |
111 | expands to this:
112 |
113 | @racketblock[
114 | (begin
115 | (displayln (string-append "Hello, " "world!"))
116 | (pd (displayln (string-append "I can't use " "division!"))))
117 | ]
118 |
119 | This contains another occurrence of @tt{pd}, and this time, the code
120 |
121 | @racketblock[
122 | (pd (displayln (string-append "I can't use " "division!")))
123 | ]
124 |
125 | expands to this:
126 |
127 | @racketblock[
128 | (displayln (string-append "I can't use " "division!"))
129 | ]
130 |
131 | This behavior makes it so occurrences of the @tt{pd} form can be generously added wherever they're suspected to be needed, without causing conflicts with each other.
132 | }
133 |
134 | @defproc*[(
135 | [(parendown-readtable-handler [name char?] [in input-port?]) any/c]
136 | [
137 | (parendown-readtable-handler
138 | [name char?]
139 | [in input-port?]
140 | [src any/c]
141 | [line (or/c #f exact-positive-integer?)]
142 | [col (or/c #f exact-nonnegative-integer?)]
143 | [pos (or/c #f exact-positive-integer?)])
144 | any/c]
145 | )]{
146 | A readtable handler procedure suitable for use with @racket[make-readtable]. This handler implements a syntax very similar to (if not necessarily in full parity with) the default read behavior for the characters @tt{(}, @tt{[}, and @tt{@"{"}, except that it doesn't consume the terminating @tt{)}, @tt{]}, or @tt{@"}"}.
147 |
148 | When the terminating character is @tt{]} or @tt{@"}"}, the resulting list's @tt{paren-shape} syntax property is set to @racket[#\[] or @racket[#\{], respectively.
149 |
150 | This readtable handler is sensitive to the @racket[read-accept-dot] and @racket[read-accept-infix-dot] parameters at the time the handler is invoked. This functionality of Parendown should be considered unstable, since it isn't quite the same as what @tt{(}, @tt{[}, and @tt{@"{"} do on contemporary versions of Racket. Those characters' default handlers are sensitive to the values of those parameters at the time the read is @emph{originally started}, not the time they are encountered during the read. For instance, in contemprary versions of Racket, if @racket[(read-accept-dot)] is @racket[#t] at the time @racket[read] is first called and then a custom reader syntax causes it to be set to @racket[#f], a subsequent occurrence of @tt{(} in the same read will be processed as though @racket[(read-accept-dot)] were still @racket[#t].
151 | }
152 |
153 | @defproc[
154 | (parendown-color-lexer
155 | [weak-open-paren (and/c string? immutable?)]
156 | [original-get-info (-> any/c any/c any)])
157 | procedure?
158 | ]{
159 | Given the syntax of a weak opening paren as a string (e.g., @racket["#/"] or @racket["/"]), and given a language's @racket[_get-info] procedure (like one returned by @racket[read-language]), returns a procedure that a @racket[_get-info] procedure can return in response to a request for @racket['color-lexer]. This lexer implements syntax highlighting in nearly the same way @racket[original-get-info] does, but it recognizes @racket[weak-open-paren] as a parenthesis.
160 | }
161 |
--------------------------------------------------------------------------------
/parendown-lib/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define collection "parendown")
4 |
5 | (define deps (list "base"))
6 |
--------------------------------------------------------------------------------
/parendown-lib/lang/reader.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | ; parendown/lang/reader
4 | ;
5 | ; Parendown's weak opening paren functionality in the form of a
6 | ; language extension.
7 |
8 | ; Copyright 2017-2018, 2021 The Lathe Authors
9 | ;
10 | ; Licensed under the Apache License, Version 2.0 (the "License");
11 | ; you may not use this file except in compliance with the License.
12 | ; You may obtain a copy of the License at
13 | ;
14 | ; http://www.apache.org/licenses/LICENSE-2.0
15 | ;
16 | ; Unless required by applicable law or agreed to in writing,
17 | ; software distributed under the License is distributed on an
18 | ; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
19 | ; either express or implied. See the License for the specific
20 | ; language governing permissions and limitations under the License.
21 |
22 |
23 | (require
24 | (only-in syntax/module-reader
25 | make-meta-reader
26 | lang-reader-module-paths)
27 | (only-in parendown
28 | parendown-color-lexer parendown-readtable-handler))
29 |
30 | (provide
31 | (rename-out
32 | [-read read]
33 | [-read-syntax read-syntax]
34 | [-get-info get-info]))
35 |
36 | (define (wrap-reader -read)
37 | (lambda args
38 | (parameterize
39 | (
40 | [
41 | current-readtable
42 |
43 | ; NOTE: There are many syntaxes we could have used for this,
44 | ; but we're using `#/`. Using `/` like Cene does would be
45 | ; riskier, because many symbols in Racket conain `/` in
46 | ; their names. Nevertheless, we the commented-out code in
47 | ; the alternative language `#lang parendown/slash`. It
48 | ; requires us to put whitespace between a Parendown weak
49 | ; opening paren `/` and any preceding symbol, but we've been
50 | ; using whitespace like that anyway.
51 | ;
52 | ; A change to this code should coincide with a change to the
53 | ; hardcoded `"#/"` string in the `color lexer` case below.
54 | ;
55 | (make-readtable (current-readtable)
56 | #\/ 'dispatch-macro parendown-readtable-handler)])
57 | ; #\/ 'non-terminating-macro parendown-readtable-handler)])
58 |
59 | (apply -read args))))
60 |
61 | (define-values (-read -read-syntax -get-info)
62 | (make-meta-reader
63 | 'parendown
64 | "language path"
65 | lang-reader-module-paths
66 | wrap-reader
67 | wrap-reader
68 | (lambda (-get-info)
69 | (lambda (key default-value)
70 | (define (fallback) (-get-info key default-value))
71 | (case key
72 | [(color-lexer) (parendown-color-lexer "#/" -get-info)]
73 |
74 | ; TODO: Consider having `#lang parendown` and
75 | ; `#lang parendown/slash` provide behavior for the following
76 | ; other extension points:
77 | ;
78 | ; drracket:indentation
79 | ; - Determining the number of spaces to indent a new
80 | ; line by. For Parendown, it would be nice to indent
81 | ; however the base language indents, but counting the
82 | ; weak opening paren as an opening parenthesis (so
83 | ; that the new line ends up indented further than a
84 | ; preceding weak opening paren).
85 | ;
86 | ; drracket:keystrokes
87 | ; - Determining actions to take in response to
88 | ; keystrokes. For Parendown, it might be nice to make
89 | ; it so that when a weak opening paren is typed at the
90 | ; beginning of a line (with some amount of
91 | ; indentation), the line is reindented to be flush
92 | ; with a preceding normal or weak opening paren).
93 | ;
94 | ; configure-runtime
95 | ; - Initializing the Racket runtime for executing a
96 | ; Parendown-language module directly or interacting
97 | ; with it at a REPL. For Parendown, it might be nice
98 | ; to let the weak opening paren be used at the REPL.
99 | ; Then again, will that modify the current readtable
100 | ; in a way people don't expect when they run a module
101 | ; directly? Also, for this to work, we need to have
102 | ; Parendown attach a `'module-language` syntax
103 | ; property to the module's syntax somewhere. Is it
104 | ; possible to do that while also passing through the
105 | ; base language's `'module-language` declaration?
106 | ;
107 | ; drracket:submit-predicate
108 | ; - Determining whether a REPL input is complete. For
109 | ; Parendown, if we're supporting weak opening parens
110 | ; at the REPL, we should just make sure inputs with
111 | ; weak opening parens are treated as we expect. We
112 | ; might not need to extend this.
113 | ;
114 | ; module-language
115 | ; - Is this the right place to look for this key? It's a
116 | ; key to the `#:info` specification for
117 | ; `#lang syntax/module-reader`, but maybe that's not
118 | ; related. Other places in the documentation that talk
119 | ; about `'module-language` are referring to a syntax
120 | ; property.
121 |
122 | [else (fallback)])))))
123 |
--------------------------------------------------------------------------------
/parendown-lib/main.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | ; parendown
4 | ;
5 | ; Parendown's weak opening paren functionality in the form of a
6 | ; library rather than as a language extension. (The language extension
7 | ; is implemented in terms of this.)
8 |
9 | ; Copyright 2017-2018, 2021 The Lathe Authors
10 | ;
11 | ; Licensed under the Apache License, Version 2.0 (the "License");
12 | ; you may not use this file except in compliance with the License.
13 | ; You may obtain a copy of the License at
14 | ;
15 | ; http://www.apache.org/licenses/LICENSE-2.0
16 | ;
17 | ; Unless required by applicable law or agreed to in writing,
18 | ; software distributed under the License is distributed on an
19 | ; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
20 | ; either express or implied. See the License for the specific
21 | ; language governing permissions and limitations under the License.
22 |
23 |
24 | (require
25 | (for-syntax
26 | racket/base
27 | (only-in racket/match match)
28 | (only-in syntax/parse id syntax-parse))
29 | (only-in racket/contract/base
30 | -> and/c any any/c case-> contract-out or/c)
31 | (only-in racket/undefined undefined)
32 | (only-in syntax/readerr raise-read-error))
33 |
34 | (provide
35 | pd
36 | (contract-out
37 | [
38 | parendown-readtable-handler
39 | (case->
40 | (-> char? input-port? any/c)
41 | (->
42 | char?
43 | input-port?
44 | any/c
45 | (or/c #f exact-positive-integer?)
46 | (or/c #f exact-nonnegative-integer?)
47 | (or/c #f exact-positive-integer?)
48 | any/c))]
49 | [
50 | parendown-color-lexer
51 | (-> (and/c string? immutable?) (-> any/c any/c any)
52 | procedure?)]))
53 |
54 |
55 |
56 | ; ===== Weak opening brackets from a syntax transformer ==============
57 |
58 | (define-syntax (pd stx)
59 | (syntax-parse stx
60 |
61 | ; If the input appears to have already been processed by a
62 | ; surrounding `pd` form, that's fine. In that case `pd` behaves
63 | ; like an identity operation, having no effect.
64 | [(_ (rest ...)) #'(rest ...)]
65 |
66 | [ (_ sample:id rest ...)
67 | (define (add-to-syntax-property-list prop-name elem stx)
68 | (syntax-property stx prop-name
69 | (cons elem (or (syntax-property stx prop-name) (list)))))
70 | (define uses (list))
71 | (define processed
72 | (let loop ([stx #'(rest ...)])
73 | (syntax-parse stx
74 | [ (first . rest)
75 | (if
76 | (and
77 | (identifier? #'first)
78 | (bound-identifier=? #'sample #'first))
79 | (begin
80 | (set! uses (cons #'first uses))
81 | #`(#,(loop #'rest)))
82 | #`(#,(loop #'first) . #,(loop #'rest)))]
83 | [_ stx])))
84 | #`(begin
85 | ; We generate fake binding and usage sites just so the
86 | ; Check Syntax binding arrows look good in DrRacket.
87 | (let ()
88 | (define-syntax (sample stx) #'(void))
89 | #,@uses
90 | (void))
91 | #,processed)]))
92 |
93 |
94 |
95 | ; ===== Weak opening brackets from a reader extension ================
96 |
97 | (define (until-fn condition body)
98 | (unless (condition)
99 | (body)
100 | (until-fn condition body)))
101 |
102 | (define-syntax-rule (until condition body ...)
103 | (until-fn (lambda () condition) (lambda () body ...)))
104 |
105 | ; Racket's `peek-char` lets you skip a number of *bytes*, but not a
106 | ; number of characters. This one lets you skip a number of characters.
107 | (define (peek-char-skipping-chars in skip-chars-amt)
108 | (let ([peeked-string (peek-string (add1 skip-chars-amt) 0 in)])
109 | (string-ref peeked-string skip-chars-amt)))
110 |
111 | (define (non-terminating-char? readtable x)
112 | (and (char? x)
113 | (let ()
114 | (define-values
115 | (char-terminating char-entry char-dispatch-entry)
116 | (readtable-mapping readtable x))
117 | (or (eq? 'non-terminating-macro char-terminating)
118 | (and (char? char-terminating)
119 | (parameterize ([current-readtable #f])
120 | (define symbol-name (string #\a char-terminating))
121 | (not
122 | (eq? 'a (read (open-input-string symbol-name))))))))))
123 |
124 |
125 | (define parendown-readtable-handler
126 | (case-lambda
127 | [ (name in)
128 | (define-values (line col pos) (port-next-location in))
129 | (define src (object-name in))
130 | (read-list name in src line col pos #f)]
131 | [ (name in src line col pos)
132 | (read-list name in src line col pos #t)]))
133 |
134 | (define (read-list name in src line col pos should-read-syntax)
135 | (define span 1)
136 | (define (read-as-we-should [in in])
137 | (if should-read-syntax
138 | (read-syntax/recursive src in)
139 | (read/recursive in)))
140 | (define (read-skipping-comments)
141 | (define result (read-as-we-should))
142 | (until (not (special-comment? result))
143 | (set! result (read-as-we-should)))
144 | result)
145 | (define (skip-whitespace)
146 | (regexp-match #px"^\\s*" in))
147 | (define (like-default char . originals)
148 | (define-values (char-terminating char-entry char-dispatch-entry)
149 | (readtable-mapping (current-readtable) char))
150 | (memq char-terminating originals))
151 |
152 | ; NOTE: Racket doesn't provide any really elegant way to skip
153 | ; comments without reading the next non-comment expression... but it
154 | ; turns out the built-in list syntax doesn't *use* any really
155 | ; elegant way to do it either. So, the function
156 | ; `peek-after-whitespace-and-comments` here is full of ad hoc,
157 | ; unhygienic behavior, but almost all of it is meant to simulate
158 | ; specific behaviors of the built-in list syntax.
159 | (define (peek-after-whitespace-and-comments)
160 | (skip-whitespace)
161 | (define next-char (peek-char in))
162 |
163 | (cond
164 | [(eof-object? next-char) next-char]
165 | [ (like-default next-char #\;)
166 | (read-as-we-should)
167 | (peek-after-whitespace-and-comments)]
168 | [(not (like-default next-char #\#)) next-char]
169 | [#t
170 | (define-values (dispatch-line dispatch-col dispatch-pos)
171 | (port-next-location in))
172 | (define dispatch-span 2)
173 | (define hash-char next-char)
174 | (define fake-dispatch-string (peek-string 2 0 in))
175 | (cond
176 | [(< (string-length fake-dispatch-string) 2) hash-char]
177 | [ (like-default (string-ref fake-dispatch-string 1) #\;)
178 | (read-string 2 in)
179 | ; NOTE: Inside of the built-in list syntax, a #;(...)
180 | ; comment works like this even if ; has been bound to a
181 | ; custom `dispatch-macro` in the current readtable. Inside
182 | ; the expression, the readtable seems to be the same as
183 | ; it is on the outside, so the built-in list syntax does
184 | ; *not* parameterize the readtable with a default
185 | ; implementation of #; to do the read here.
186 | (read-skipping-comments)
187 | (peek-after-whitespace-and-comments)]
188 | [ (like-default (string-ref fake-dispatch-string 1) #\|)
189 | (read-string 2 in)
190 |
191 | ; NOTE: Inside of the built-in list syntax, a #|...|#
192 | ; comment isn't picky about which exact characters are
193 | ; used to close it, as long as they have the appropriate
194 | ; `readtable-mapping`. Outside of a list, a #|...|#
195 | ; comment is pickier than that.
196 | (define (read-rest-of-fake-nested-comment)
197 | (define first (read-char in))
198 | (define second (peek-char in))
199 | (when (or (eof-object? first) (eof-object? second))
200 | (raise-read-error "read: end of file in #| comment"
201 | src
202 | dispatch-line dispatch-col dispatch-pos
203 | dispatch-span))
204 |
205 | (cond
206 | [ (like-default first #\#)
207 | (if (like-default second #\|)
208 | (begin
209 | (read-char in)
210 | (read-rest-of-fake-nested-comment)
211 | (read-rest-of-fake-nested-comment))
212 | (read-rest-of-fake-nested-comment))]
213 | [ (like-default first #\|)
214 | (if (like-default second #\#)
215 | (read-char in)
216 | (read-rest-of-fake-nested-comment))]
217 | [#t (read-rest-of-fake-nested-comment)]))
218 |
219 | (read-rest-of-fake-nested-comment)
220 | (peek-after-whitespace-and-comments)]
221 | [#t hash-char])]))
222 |
223 | (define (closing? char)
224 | (like-default char #\) #\] #\}))
225 |
226 | ; TODO: The built-in list syntax actually seems to obtain the value
227 | ; of these parameters at the time the *original* read procedure was
228 | ; called, before any recursive reads. For instance, the expression
229 | ; (1 2 * (3 . 4) 5), where * is a custom reader macro that sets
230 | ; `read-accept-dot` to #f, reads as (1 2 (3 . 4) 5) if
231 | ; `read-accept-dot` was true before the read. See if there's a way
232 | ; we can simulate that.
233 | (define accept-dot (read-accept-dot))
234 | (define accept-infix-dot (read-accept-infix-dot))
235 |
236 | ; These variables are the state of the next loop.
237 | (define rev-elems (list))
238 | (define improper-tail (list))
239 | (define action-on-non-comment void)
240 | (define next-char undefined)
241 | (define listening-for-dots accept-dot)
242 | (until
243 | (begin
244 | (skip-whitespace)
245 | (set! next-char (peek-after-whitespace-and-comments))
246 |
247 | (when (eof-object? next-char)
248 | (raise-read-error
249 | (string-append
250 | "read: expected `)', `]', or `}' to close "
251 | "`" (string name) "'")
252 | src line col pos span))
253 |
254 | (if
255 | (and listening-for-dots
256 | (like-default next-char #\.)
257 | (not
258 | (non-terminating-char? (current-readtable)
259 | ; TODO: This is the only place we peek more than one
260 | ; character ahead. See if the official reader performs a
261 | ; read and then a peek here instead.
262 | (peek-char-skipping-chars in 1))))
263 | (let ()
264 | (define-values (dot-line dot-col dot-pos)
265 | (port-next-location in))
266 | (define dot-span 1)
267 | (define dot-char next-char)
268 | (define (dot-err [dot-char dot-char])
269 | (raise-read-error
270 | (string-append
271 | "read: illegal use of `" (string dot-char) "'")
272 | src dot-line dot-col dot-pos dot-span))
273 |
274 | (read-char in)
275 | (define elem (read-skipping-comments))
276 | (define possible-next-dot-or-closing
277 | (peek-after-whitespace-and-comments))
278 | (cond
279 | [(eof-object? possible-next-dot-or-closing) (dot-err)]
280 | [ (and accept-infix-dot
281 | (like-default possible-next-dot-or-closing #\.))
282 |
283 | (read-char in)
284 |
285 | ; NOTE: In the built-in list syntax, the syntax
286 | ; (1 2 . 3 . *), where * is a custom reader macro that
287 | ; returns a special comment, reads as (1 2). For parity,
288 | ; we don't add the infix operator to the list until we
289 | ; encounter the next non-comment list element. Note that
290 | ; comments like ; #; and #| are skipped a different way
291 | ; (emulated here by
292 | ; `peek-after-whitespace-and-comments`), so they don't
293 | ; exhibit the same behavior.
294 | (set! action-on-non-comment
295 | (lambda ()
296 | (set! action-on-non-comment void)
297 | (set! rev-elems (append rev-elems (list elem)))))
298 |
299 | (set! listening-for-dots #f)
300 | (define possible-closing
301 | (peek-after-whitespace-and-comments))
302 | (when (eof-object? possible-closing)
303 | ; TODO: This prints garbage to the console, but this
304 | ; has parity with Racket. See if this is a bug that
305 | ; needs to be fixed in Racket.
306 | (dot-err (integer->char #xFFFD)))
307 | (when (closing? possible-closing)
308 | (dot-err possible-closing))
309 |
310 | ; We've read the operator of an infix list, and there's
311 | ; still more to go after that, so we continue the loop.
312 | #f]
313 | [ (closing? possible-next-dot-or-closing)
314 | (set! improper-tail elem)
315 | (set! next-char possible-next-dot-or-closing)
316 |
317 | ; We've read the end of an improper list, so we exit the
318 | ; loop.
319 | #t]
320 | [ #t
321 |
322 | ; The usual error for (10 20 . 30 40) reads the "4" from
323 | ; the stream before it raises its error, so we do the
324 | ; same thing.
325 | (read-char in)
326 |
327 | (dot-err)]))
328 |
329 | ; If the next character is not a dot, or if we're not
330 | ; listening for dots, we exit the loop if we've reached a
331 | ; closing paren.
332 | (closing? next-char)))
333 | (define elem (read-as-we-should))
334 | (unless (special-comment? elem)
335 | (action-on-non-comment)
336 | (set! rev-elems (cons elem rev-elems))))
337 | (define result (append (reverse rev-elems) improper-tail))
338 | (define-values (stop-line stop-col stop-pos)
339 | (port-next-location in))
340 | (when should-read-syntax
341 | (set! result
342 | (datum->syntax #f result
343 | (and line (vector src line col pos (- stop-pos pos)))))
344 | (when (like-default next-char #\])
345 | (set! result (syntax-property result 'paren-shape #\[)))
346 | (when (like-default next-char #\})
347 | (set! result (syntax-property result 'paren-shape #\{))))
348 | result)
349 |
350 |
351 | ; Parendown's syntax highlighting recognizes the weak open paren as a
352 | ; `'parenthesis` token, and it passes all other processing through to
353 | ; the extended language's syntax highlighter.
354 | ;
355 | (define (parendown-color-lexer weak-open-paren -get-info)
356 | (define weak-open-paren-length (string-length weak-open-paren))
357 |
358 | ; TODO: Should we check for whether `-get-info` is false before
359 | ; calling it here? Other languages seem to do that, but the
360 | ; documented contract of `make-meta-reader` specifies that it will
361 | ; at least be a `procedure?`, not `(or/c #f procedure?)`.
362 | ;
363 | (define get-info-fallback-color-lexer (-get-info 'color-lexer #f))
364 |
365 | (define default-fallback-color-lexer
366 | (if (procedure? get-info-fallback-color-lexer)
367 | get-info-fallback-color-lexer
368 |
369 | ; TODO: Why are we using `dynamic-require` here? Other languages
370 | ; do it. Is that so they can keep their package dependencies
371 | ; small and only depend on DrRacket-related things if the user
372 | ; is definitely already using DrRacket?
373 | ;
374 | ; TODO: Some languages even guard against the possibility that
375 | ; the packages they `dynamic-require` don't exist. Should we do
376 | ; that here?
377 | ;
378 | (dynamic-require 'syntax-color/racket-lexer 'racket-lexer)))
379 |
380 | (define normalized-fallback-color-lexer
381 | (if (procedure-arity-includes? default-fallback-color-lexer 3)
382 | default-fallback-color-lexer
383 | (lambda (in offset mode)
384 | (define-values (text sym paren start stop)
385 | (default-fallback-color-lexer in))
386 | (define backup-distance 0)
387 | (define new-mode mode)
388 | (values text sym paren start stop backup-distance new-mode))))
389 |
390 | (lambda (in offset mode)
391 | (define peeked (peek-string weak-open-paren-length 0 in))
392 | (if (and (string? peeked) (string=? weak-open-paren peeked))
393 | (let ()
394 | (define-values (line col pos) (port-next-location in))
395 | (read-string weak-open-paren-length in)
396 | (define text weak-open-paren)
397 | (define sym 'parenthesis)
398 | (define paren #f)
399 |
400 | ; TODO: The documentation of `start-colorer` says the
401 | ; beginning and ending positions should be *relative* to the
402 | ; original `port-next-location` of "the input port passed to
403 | ; `get-token`" (called `in` here), but it raises an error if
404 | ; we use `(define start 0)`. Is that a documentation issue?
405 | ; Perhaps it should say "the input port passed to the first
406 | ; call to `get-token`."
407 | ;
408 | (define start pos)
409 | (define stop (+ start weak-open-paren-length))
410 |
411 | (define backup-distance 0)
412 |
413 | ; TODO: Does it always make sense to preserve the mode like
414 | ; this? Maybe some color lexers would want their mode updated
415 | ; in a different way here (not that we can do anything about
416 | ; it).
417 | ;
418 | (define new-mode mode)
419 |
420 | (values text sym paren start stop backup-distance new-mode))
421 | (normalized-fallback-color-lexer in offset mode))))
422 |
--------------------------------------------------------------------------------
/parendown-lib/slash/lang/reader.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | ; parendown/slash/lang/reader
4 | ;
5 | ; Parendown's weak opening paren functionality in the form of a
6 | ; language extension, using a non-symbol-terminating `/` reader macro
7 | ; instead of `#/`.
8 |
9 | ; Copyright 2017-2018, 2021 The Lathe Authors
10 | ;
11 | ; Licensed under the Apache License, Version 2.0 (the "License");
12 | ; you may not use this file except in compliance with the License.
13 | ; You may obtain a copy of the License at
14 | ;
15 | ; http://www.apache.org/licenses/LICENSE-2.0
16 | ;
17 | ; Unless required by applicable law or agreed to in writing,
18 | ; software distributed under the License is distributed on an
19 | ; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
20 | ; either express or implied. See the License for the specific
21 | ; language governing permissions and limitations under the License.
22 |
23 |
24 | (require
25 | (only-in syntax/module-reader
26 | make-meta-reader
27 | lang-reader-module-paths)
28 | (only-in parendown
29 | parendown-color-lexer parendown-readtable-handler))
30 |
31 | (provide
32 | (rename-out
33 | [-read read]
34 | [-read-syntax read-syntax]
35 | [-get-info get-info]))
36 |
37 | (define (wrap-reader -read)
38 | (lambda args
39 | (parameterize
40 | (
41 | [
42 | current-readtable
43 | (make-readtable (current-readtable)
44 | #\/ 'non-terminating-macro parendown-readtable-handler)])
45 |
46 | (apply -read args))))
47 |
48 | (define-values (-read -read-syntax -get-info)
49 | (make-meta-reader
50 | 'parendown/slash
51 | "language path"
52 | lang-reader-module-paths
53 | wrap-reader
54 | wrap-reader
55 | (lambda (-get-info)
56 | (lambda (key default-value)
57 | (define (fallback) (-get-info key default-value))
58 | (case key
59 | [(color-lexer) (parendown-color-lexer "/" -get-info)]
60 |
61 | ; TODO: Consider providing behavior for additional extension
62 | ; points. See the corresponding comment in the
63 | ; `#lang parendown` reader module.
64 |
65 | [else (fallback)])))))
66 |
--------------------------------------------------------------------------------
/parendown-test/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define collection "parendown")
4 |
5 | (define deps (list "base" "parendown-lib" "rackunit-lib"))
6 |
--------------------------------------------------------------------------------
/parendown-test/tests.rkt:
--------------------------------------------------------------------------------
1 | #lang parendown racket/base
2 |
3 | ; parendown/tests
4 | ;
5 | ; Unit tests.
6 |
7 | ; Copyright 2018, 2021 The Lathe Authors
8 | ;
9 | ; Licensed under the Apache License, Version 2.0 (the "License");
10 | ; you may not use this file except in compliance with the License.
11 | ; You may obtain a copy of the License at
12 | ;
13 | ; http://www.apache.org/licenses/LICENSE-2.0
14 | ;
15 | ; Unless required by applicable law or agreed to in writing,
16 | ; software distributed under the License is distributed on an
17 | ; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
18 | ; either express or implied. See the License for the specific
19 | ; language governing permissions and limitations under the License.
20 |
21 |
22 | (require #/only-in rackunit check-equal?)
23 |
24 | (require #/only-in parendown pd)
25 |
26 | ; (We provide nothing from this module.)
27 |
28 |
29 | (check-equal?
30 | '(a #/b c #/d . #/e . f . #/g)
31 | '(a (b c (d . (e . f . (g)))))
32 | "Using the `#/` reader syntax from `#lang parendown`")
33 |
34 | (check-equal?
35 | (pd / quote / a / b c / d f e / g)
36 | '(a (b c (d . (e . f . (g)))))
37 | "Using the `pd` macro")
38 |
--------------------------------------------------------------------------------
/parendown-test/tests/slash.rkt:
--------------------------------------------------------------------------------
1 | #lang parendown/slash racket/base
2 |
3 | ; parendown/tests/slash
4 | ;
5 | ; Unit tests for `#lang parendown/slash`.
6 |
7 | ; Copyright 2018, 2021 The Lathe Authors
8 | ;
9 | ; Licensed under the Apache License, Version 2.0 (the "License");
10 | ; you may not use this file except in compliance with the License.
11 | ; You may obtain a copy of the License at
12 | ;
13 | ; http://www.apache.org/licenses/LICENSE-2.0
14 | ;
15 | ; Unless required by applicable law or agreed to in writing,
16 | ; software distributed under the License is distributed on an
17 | ; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
18 | ; either express or implied. See the License for the specific
19 | ; language governing permissions and limitations under the License.
20 |
21 |
22 | (require /only-in rackunit check-equal?)
23 |
24 | ; (We provide nothing from this module.)
25 |
26 |
27 | (check-equal?
28 | '(a /b c /d . /e . f . /g)
29 | '(a (b c (d . (e . f . (g)))))
30 | "Using the `/` reader syntax from `#lang parendown/slash`")
31 |
32 | (check-equal?
33 | (\/ 4 2)
34 | 2
35 | "Denoting division as `\\/`")
36 |
37 | (check-equal?
38 | (|/| 4 2)
39 | 2
40 | "Denoting division as `|/|`")
41 |
--------------------------------------------------------------------------------
/parendown/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define collection 'multi)
4 |
5 | (define implies (list "parendown-doc" "parendown-lib"))
6 | (define deps implies)
7 |
--------------------------------------------------------------------------------