├── .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 | [![CI](https://github.com/lathe/parendown-for-racket/actions/workflows/ci.yml/badge.svg)](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 | --------------------------------------------------------------------------------