├── .github └── workflows │ └── ci.yml ├── .gitignore ├── AUTHORS.md ├── LICENSE ├── README.md ├── fexpress-doc ├── info.rkt └── scribblings │ └── fexpress.scrbl ├── fexpress-lib ├── info.rkt ├── main.rkt └── proof-of-concept.rkt ├── fexpress-test ├── info.rkt ├── tests.rkt └── tests │ └── and.rkt ├── fexpress └── info.rkt └── notes ├── 20210813-prior-art-and-musings.md └── code-sketches └── fexpress.js /.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: fexpress 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 | Fexpress is authored by: 2 | 3 | * rocketnia 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 | # Fexpress 2 | 3 | [![CI](https://github.com/rocketnia/fexpress/actions/workflows/ci.yml/badge.svg)](https://github.com/rocketnia/fexpress/actions/workflows/ci.yml) 4 | 5 | Fexprs are well-known to make compilation difficult in the general case. If the behavior of an abstraction can depend on the combination of run-time information and the exact source code the abstraction was called with, then some of the source code must survive as-is until run time. 6 | 7 | *Some* of the source code. With Fexpress, we demonstrate that by adding cetain features to fexprs, we can compile code that doesn't depend on dynamic fexpr use: 8 | 9 | ```racket 10 | > (define test-env 11 | (env-of-specific-values 12 | (hash 'the fexpress-the 13 | 'ilambda fexpress-ilambda 14 | 'clambda fexpress-clambda 15 | 'funcall (lambda (f . args) (apply f args)) 16 | '+ + 17 | '* *))) 18 | > (define (logging body) 19 | (parameterize ([current-fexpress-logger pretty-print]) 20 | (body))) 21 | > (define (fexpress-eval-and-log expr) 22 | (logging (lambda () (fexpress-eval test-env expr)))) 23 | > (fexpress-eval-and-log 24 | '(funcall 25 | (clambda (square) 26 | (funcall 27 | (clambda (double) 28 | (funcall double 29 | (funcall double 30 | (+ (funcall square 3) (funcall square 4))))) 31 | (clambda (x) (+ x x)))) 32 | (clambda (x) (* x x)))) 33 | '("Evaluating Racket code:" 34 | (lambda (env -funcall -+) 35 | (lambda (-square) 36 | (#%app 37 | -funcall 38 | (lambda (-double) 39 | (#%app 40 | -funcall 41 | -double 42 | (#%app 43 | -funcall 44 | -double 45 | (#%app 46 | -+ 47 | (#%app -funcall -square (#%datum . 3)) 48 | (#%app -funcall -square (#%datum . 4)))))) 49 | (lambda (-x) (#%app -+ -x -x)))))) 50 | '("Evaluating Racket code:" (lambda (env -*) (lambda (-x) (#%app -* -x -x)))) 51 | 100 52 | ``` 53 | 54 | The above code only invoked global variables, and those global variables were procedures and a certain fexpr (`fexpr-clambda`) which implemented its own compilation support, so the result was fully compiled code. 55 | 56 | To invoke only global variables, that code uses a Lisp-2-style `funcall` operation. If it had invoked the local variables directly, Lisp-1 style, Fexpress's implementation wouldn't figure out that they weren't fexprs, so it would have generated code that called back into the interpreter for those calls. 57 | 58 | But Fexpress shows off another solution for that. Using rudimentary type annotations, even the Lisp-1 style can turn into efficient code: 59 | 60 | ```racket 61 | > (define my-compose 62 | (fexpress-eval-and-log 63 | `(the ,(->/t_ (list (non-fexpr-value/t+)) 64 | (->/t_ (list (non-fexpr-value/t+)) 65 | (any-value/t_))) 66 | (clambda (f) 67 | (clambda (g) 68 | (clambda (x) 69 | (f (g x)))))))) 70 | '("Evaluating Racket code:" 71 | (lambda (env) 72 | (lambda (-f) (lambda (-g) (lambda (-x) (#%app -f (#%app -g -x))))))) 73 | > (logging (lambda () (((my-compose sqrt) add1) 8))) 74 | 3 75 | ``` 76 | 77 | For more information on the way Fexpress is built, what extension points and building blocks it has available, and where it might go from here, [see the documentation](http://docs.racket-lang.org/fexpress/index.html). 78 | 79 | The current incarnation of Fexpress is merely a proof of concept. We've prioritized simple and direct implementation to demonstrate the concepts, and the API may be unstable as a result. This version will stick around and keep serving as a demonstration, but we also envision making a more full-featured and stable Fexpress alongside it. 80 | 81 | 82 | ## Installation and use 83 | 84 | This is a library for Racket. To install it from the Racket package index, run `raco pkg install fexpress`. Then you can put an import like `(require fexpress/proof-of-concept)` in your Racket program. 85 | 86 | To install it from source, run `raco pkg install` from the `fexpress-lib/` directory. 87 | 88 | [Documentation for Fexpress](http://docs.racket-lang.org/fexpress/index.html) is available at the Racket documentation website, and it's maintained in the `fexpress-doc/` directory. 89 | -------------------------------------------------------------------------------- /fexpress-doc/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "fexpress") 4 | 5 | (define deps (list "base")) 6 | (define build-deps (list "fexpress-lib" "racket-doc" "scribble-lib")) 7 | 8 | (define scribblings (list (list "scribblings/fexpress.scrbl" (list)))) 9 | -------------------------------------------------------------------------------- /fexpress-doc/scribblings/fexpress.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @; fexpress/scribblings/fexpress.scrbl 4 | @; 5 | @; Fexpress, a compilation-friendly fexpr language. 6 | 7 | @; Copyright 2021, 2023 The Fexpress 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 23 | (for-label 24 | (only-in racket/base 25 | + * add1 and apply boolean? define eval exact-integer? hash 26 | hash-ref identifier? lambda list map parameterize sqrt 27 | symbol?))) 28 | @(require 29 | (for-label 30 | (only-in racket/contract/base 31 | -> any any/c hash/c listof parameter/c))) 32 | @(require (for-label (only-in racket/pretty pretty-print))) 33 | @(require (for-label (only-in racket/math natural?))) 34 | 35 | @(require (for-label fexpress/proof-of-concept)) 36 | 37 | @(require (only-in scribble/example examples make-eval-factory)) 38 | 39 | @(define example-eval 40 | (make-eval-factory (list 'racket/base 41 | 'racket/pretty 42 | 'fexpress/proof-of-concept))) 43 | 44 | @(define persistent-example-eval (example-eval)) 45 | 46 | 47 | @title{Fexpress} 48 | 49 | Fexpress is a compilation-friendly @tech{fexpr} language. As far as feasible, it macroexpands expressions ahead of time instead of just interpreting everything. 50 | 51 | At some point, there may be two variants of Fexpress. 52 | 53 | The current variant---@racketmodname[fexpress/proof-of-concept]---is intended to help demonstrate the principles at work. For this reason, it has only a minimalistic set of features, it doesn't have deep library dependencies, and we haven't gone to any special effort to harden its API for future stability. If the concepts that need to be demonstrated change, we might add newly needed methods to some of the generic interfaces, might allow an @racket[env?] to be something more expressive or restrictive than a hash table, and so on. 54 | 55 | The other variant of Fexpress---potentially the @tt{fexpress} module proper, once it exists---could be a more full-fledged system for using fexprs in Racket programs. This variant could better preserve Racket syntax object metadata for error reporting and Racket-style hygiene, and it could introduce features like editor highlighting to show what subexpressions of a program are making unoptimized fexpr calls. We can test the limits of how seamless an addition they can be to the Racket language. 56 | 57 | However, there's a certain kind of seamlessness Fexpress won't attempt: Racket's @racket[and] can't be passed into Racket's @racket[map], and sometimes this surprises people who expect macros to act like functions. In languages with fexprs as the default abstraction, it tends to be easy to implement @racket[and] and @racket[map] in such a way that this interaction succeeds. However, that amounts to a much different design for these operations, and not a better one. If Racket's @racket[map] refuses to pass its internal code to an fexpr, that's good encapsulation of its implementation details. And Racket's @racket[and] is designed to operate on input that's an unevaluated syntax object (along with various macroexpansion-time parameters), so if the input it receives is actually a run-time collection of positional and keyword arguments, it's quite reasonable for it to reject that input as a likely mistake by the user. These would be good design choices even in a language that had fexprs in it, and we don't intend to circumvent them with Fexpress. 58 | 59 | Anyhow, the Fexpress that exists now is the simplified proof of concept. Our hope is to demonstrate that a viable strategy exists for mixing fexprs with compilation. Thanks to extension points like @racket[gen:fexpr], it could be put to some fun use, but keep in mind the instability of the API. 60 | 61 | (TODO: Currently, there isn't a dedicated operation for writing simple fexprs. Fexpress users can build one out of @racket[gen:fexpr], or just use @racket[makeshift-fexpr] in Fexpress code directly, but let's provide one at some point to ease the analogy between Fexpress and other fexpr-equipped languages.) 62 | 63 | 64 | 65 | @table-of-contents[] 66 | 67 | 68 | 69 | @section[#:tag "proof-of-concept"]{The Fexpress Proof of Concept} 70 | 71 | @defmodule[fexpress/proof-of-concept] 72 | 73 | This module provides an open-faced implementation of a minimalistic, experimental Fexpress language. Not all the contracts documented here are completely enforced, nor are they stable. 74 | 75 | The building blocks provided here make the language capable of doing simple lambda calculus, with more or less efficiency depending on the use of @racket[fexpress-ilambda] or @racket[fexpress-clambda]. This language can be extended by implementing more @racket[gen:fexpr] values in Racket, and possibly more @racket[gen:continuation-expr], @racket[gen:type+], and @racket[gen:type_] values for them to interact with. 76 | 77 | 78 | @subsection[#:tag "entrypoint"]{Entrypoint} 79 | 80 | @defproc[(fexpress-eval [env env?] [expr any/c]) any/c]{ 81 | Given an s-expression representing Fexpress code, return the result of evaluating it in the given @racket[env?]. 82 | 83 | 84 | @examples[ 85 | #:eval persistent-example-eval 86 | 87 | (define _test-env 88 | (env-of-specific-values 89 | (hash 'the fexpress-the 90 | 'ilambda fexpress-ilambda 91 | 'clambda fexpress-clambda 92 | 'funcall (lambda (_f . _args) (apply _f _args)) 93 | '+ + 94 | '* *))) 95 | 96 | (define (_logging _body) 97 | (parameterize ([current-fexpress-logger pretty-print]) 98 | (_body))) 99 | 100 | (define (_fexpress-eval-and-log _expr) 101 | (_logging (lambda () (fexpress-eval _test-env _expr)))) 102 | 103 | (_fexpress-eval-and-log 104 | '(+ 1 2)) 105 | (_fexpress-eval-and-log 106 | '((ilambda (x y) (+ x y 3)) 1 2)) 107 | (_fexpress-eval-and-log 108 | '((clambda (x y) (+ x y 3)) 1 2)) 109 | (_fexpress-eval-and-log 110 | '(funcall 111 | (clambda (square) 112 | (funcall 113 | (clambda (double) 114 | (funcall double 115 | (funcall double 116 | (+ (funcall square 3) (funcall square 4))))) 117 | (clambda (x) (+ x x)))) 118 | (clambda (x) (* x x)))) 119 | ] 120 | } 121 | 122 | @defproc[ 123 | (env-of-specific-values [specific-values (hash/c var? any/c)]) 124 | env? 125 | ]{ 126 | Creates an @racket[env?] from a hash that maps Fexpress variables to values. 127 | 128 | An @racket[env?] maps Fexpress variables to @tech{positive types} that compile to references to the same variables, so this wraps up the values in @racket[specific-value/t+] and sets up their compilation behavior with @racket[at-variable/t+]. 129 | } 130 | 131 | @defthing[current-fexpress-logger (parameter/c (-> any/c any))]{ 132 | A parameter holding a procedure that the Fexpress proof of concept uses to log diagnostic messages in s-expression form. Currently, we log two things: 133 | 134 | @itemlist[ 135 | @item{Evaluation of Racket code, so that we can inspect what kind of Racket code Fexpress produces.} 136 | @item{Moments where the compiled Racket code reenters the interpreter in order to call an fexpr. These moments are worth knowing about so we can optimize them away.} 137 | ] 138 | } 139 | 140 | 141 | @subsection[#:tag "fexpr"]{Fexprs} 142 | 143 | An @deftech{fexpr} (sometimes known as a @deftech{first-class macro}) is a kind of abstraction that's existed since the earliest implementations of Lisp. 144 | 145 | An fexpr is something in between a function and a macro. Like a function, it's a first-class value that can do its work at run time. Like a macro, it receives its arguments unevaluated, and---at least in the better incarnations---it also receives some kind of access to its caller's local scope with which to understand these arguments' intended semantics. 146 | 147 | This combination lets programmers express a few things that they can't express with functions and macros, since fexprs can compute their results based on a synthesis of run-time information and source code information. 148 | 149 | However, this combination generally means programs can't be compiled effectively, because certain expressions need to be preserved as-is until run time. If a programmer wants to @emph{express} a compilable program, fexprs usually get in the way of that, and the combination of macros and functions is arguably more expressive than fexprs for that task. 150 | 151 | The Fexpress proof of concept shows how to get around this limitation by giving fexprs even more information to work with. These fexprs receive a @tech{continuation expression} which contains a @tech{negative type} where they can find optimization hints to apply in their behavior. 152 | 153 | There are also @tech{positive type} values, which are types that can perform some fexpr-calling behavior on behalf of their potential values. Positive types are the tool the fexpr evaluator needs to proceed into binding forms like @racket[fexpress-clambda] and implement some of their behavior early, before the actual values of the variables are known. With careful programming, the remaining part of the behavior is compiled code, allowing Fexpress to express compilable programs. 154 | 155 | (TODO: How new are the things we're demonstrating here? Fexprs have been in active use in the newLISP, PicoLisp, and (arguably) R communities. There's been a lot of research on compiling reflective languages, as seen in "Collapsing Towers of Interpreters." There's also a potential connection to JIT in general, and possibly to the compilation of algebraic effect systems.) 156 | 157 | @defproc[(fexpr? [v any/c]) boolean?]{ 158 | Returns whether the given value is an Fexpress @tech{fexpr}. 159 | } 160 | 161 | @defthing[gen:fexpr any/c]{ 162 | A generic interface for Fexpress @tech{fexprs}, which must implement the method @racket[fexpr-apply/t+]. 163 | } 164 | 165 | @defproc[ 166 | (fexpr-apply/t+ [env env?] 167 | [cont continuation-expr?] 168 | [val/t+ type+?] 169 | [val fexpr?] 170 | [args any/c]) 171 | type+? 172 | ]{ 173 | (Makes @tech{fexpr} calls, namely to the given one.) Returns a @tech{positive type} for the potential values which result from transforming the given positive type and the given value (an @racket[fexpr?]) of that type according to an fexpr call with the given arguments followed by the series of steps and the target @tech{negative type} listed in the given @tech{continuation expression}. 174 | 175 | There are many @tt{...-continue-eval/t+} and @tt{...-apply/t+} operations in Fexpress, and this is the one to call when the actual @emph{value} of the original type is known and is definitely an fexpr that is definitely being invoked. 176 | 177 | The given @racket[val/t+] type should be a type which evaluates to the value @racket[val]. 178 | } 179 | 180 | @defproc[ 181 | (makeshift-fexpr [apply/t+ 182 | (-> env? continuation-expr? type+? any/c type+?)]) 183 | fexpr? 184 | ]{ 185 | Returns an @tech{fexpr} that has the given behavior for @racket[fexpr-apply/t+]. 186 | 187 | This may be more convenient than defining an instance of @racket[gen:fexpr]. 188 | } 189 | 190 | 191 | @subsubsection[#:tag "fexprs-for-lambda"]{ 192 | Fexprs for Lambda Operations 193 | } 194 | 195 | @defform[#:kind "fexpr" (fexpress-ilambda (arg-id ...) body-expr)]{ 196 | An @tech{fexpr} implementing an interpreted @racket[lambda] operation. This doesn't attempt to compile the body. The resulting function evaluates the body dynamically every time it's called. 197 | 198 | When calling this fexpr, the subforms should be parseable according to @racket[parse-lambda-args]. 199 | } 200 | 201 | @defform[#:kind "fexpr" (fexpress-clambda (arg-id ...) body-expr)]{ 202 | An @tech{fexpr} implementing a compiled @racket[lambda] operation. This attempts to compile the body. The resulting function is likely to be as fast as the equivalent Racket code unless it uses Fexpress features that inhibit compilation, in which case it falls back to interpreting the relevant Fexpress code. 203 | 204 | When calling this fexpr, the subforms should be parseable according to @racket[parse-lambda-args]. 205 | } 206 | 207 | @defproc[ 208 | (parse-lambda-args [err-name symbol?] [args any/c]) 209 | parsed-lambda-args? 210 | ]{ 211 | Asserts that the given subforms are in the format expected for an @racket[fexpress-ilambda] or @racket[fexpress-clambda] form---namely, a list of two elements, the first of which is a list of mutually unique variables and the second of which, the body, is any value. (The body is usually an s-expression representing an Fexpress expression.) If the subforms do fit this format, returns a @racket[parsed-lambda-args] struct carrying the number of arguments, the argument variable names, and the body. If they don't, an error attributed to the operation name given by @racket[err-name] will be raised. 212 | } 213 | 214 | @defstruct*[ 215 | parsed-lambda-args 216 | ([n natural?] [arg-vars (listof var?)] [body any/c]) 217 | ]{ 218 | A return value of @racket[parse-lambda-args]. 219 | 220 | The number @racket[_n] should be the length of @racket[_arg-vars]. 221 | 222 | The @racket[_arg-vars] should be mutually unique. 223 | 224 | The @racket[_body] should be an s-expression representing an Fexpress expression. 225 | } 226 | 227 | 228 | @subsubsection[#:tag "an-fexpr-for-type-ascription"]{ 229 | An Fexpr for Type Ascription 230 | } 231 | 232 | @defform[#:kind "fexpr" (fexpress-the val/t_ val-expr)]{ 233 | An @tech{fexpr} implementing a type ascription operation. The subform @racket[val/t_] must be a @tech{negative type} syntactically, not just an expression that evaluates to one. The subform @racket[val-expr] is an expression the type applies to. The purpose of @tt{fexpress-the} is mainly to allow function bodies to use Lisp-1-style function application on local variables without inhibiting compilation. 234 | 235 | As the following example shows, it's possible to use @tt{fexpress-the} to declare that the local variables @racket[_f] and @racket[_g] are non-@tech{fexprs}. This allows their use sites to be compiled into procedure calls rather than less efficient fexpr calls: 236 | 237 | @examples[ 238 | #:label #f 239 | #:eval persistent-example-eval 240 | 241 | (define _my-compose 242 | (_fexpress-eval-and-log 243 | `(the ,(->/t_ (list (non-fexpr-value/t+)) 244 | (->/t_ (list (non-fexpr-value/t+)) 245 | (any-value/t_))) 246 | (clambda (f) 247 | (clambda (g) 248 | (clambda (x) 249 | (f (g x)))))))) 250 | 251 | (_logging (lambda () (((_my-compose sqrt) add1) 8))) 252 | ] 253 | 254 | If we don't declare that @racket[_g] is a non-fexpr, what happens is that the call to @racket[_g] is compiled into an invocation of the Fexpress interpreter. In order to pass a lexical environment into that interpreter, each surrounding @racket[fexpress-clambda] (or similar binding syntax) updates the local binding of @tt{env} so that the bindings held in @tt{env} always correspond to the lexical scope: 255 | 256 | @examples[ 257 | #:label #f 258 | #:eval persistent-example-eval 259 | 260 | (define _my-less-typed-compose 261 | (_fexpress-eval-and-log 262 | `(the ,(->/t_ (list (non-fexpr-value/t+)) 263 | (->/t_ (list (any-value/t+)) 264 | (any-value/t_))) 265 | (clambda (f) 266 | (clambda (g) 267 | (clambda (x) 268 | (f (g x)))))))) 269 | 270 | (_logging (lambda () (((_my-less-typed-compose sqrt) add1) 8))) 271 | ] 272 | 273 | If we don't use @tt{fexpress-the} at all, then we get the least optimized version of the code. This time, the call to @racket[_f] reenters the interpreter, and the call to @racket[_g] is just taken care of during that interpretation: 274 | 275 | @examples[ 276 | #:label #f 277 | #:eval persistent-example-eval 278 | 279 | (define _my-untyped-compose 280 | (_fexpress-eval-and-log 281 | `(clambda (f) 282 | (clambda (g) 283 | (clambda (x) 284 | (f (g x))))))) 285 | 286 | (_logging (lambda () (((_my-untyped-compose sqrt) add1) 8))) 287 | ] 288 | } 289 | 290 | 291 | @subsection[#:tag "continuation-expr"]{Continuation Expressions} 292 | 293 | An Fexpress @deftech{continuation expression} is a representation of the syntax around the evaluating part of an Fexpress expression. 294 | 295 | Usually, this is a series of pending @tech{fexpr} applications (@racket[apply/ce]) to perform in the current @racket[env?], followed by an ascribed @tech{negative type} to optimize the overall result by (@racket[done/ce]). Other kinds of copatterns or spine elements, like field or method accessor syntaxes, could fit in here as well. 296 | 297 | @defproc[(continuation-expr? [v any/c]) boolean?]{ 298 | Returns whether the given value is a @tech{continuation expression}. 299 | } 300 | 301 | @defthing[gen:continuation-expr any/c]{ 302 | A generic interface for @tech{continuation expressions}, which must implement the method @racket[continuation-expr-continue-eval/t+]. 303 | 304 | In order to perform compilation, Fexpress @tech{fexprs} usually need to know the structural details of the continuation expression that holds their arguments. Thus, when defining new continuation expressions, it's typical to define a structure type that does more than just implement the @racket[gen:continuation-expr] interface. For instance, it can also provide its predicate and field accessors as part of its intended API, or it can implement other interfaces on the side. 305 | } 306 | 307 | @defproc[ 308 | (continuation-expr-continue-eval/t+ [env env?] 309 | [cont continuation-expr?] 310 | [val/t+ type+?]) 311 | type+? 312 | ]{ 313 | (Makes @tech{fexpr} calls.) Assuming the given @tech{positive type} will have no known fexpr-calling behavior until we witness its potential values, returns another positive type for the potential values which result from transforming those according to the series of steps and the target @tech{negative type} listed in the given @tech{continuation expression}. 314 | 315 | There are many @tt{...-continue-eval/t+} and @tt{...-apply/t+} operations in Fexpress, and this is the one to call when the positive type's fexpr-calling behavior should be ignored but its values' fexpr-calling behavior, if any, should not be ignored. This will usually result in code that consults the value at run time and makes fexpr calls to it dynamically. A positive type usually delegates to this itself when its @racket[type+-continue-eval/t+] behavior has no better idea for what to do. 316 | } 317 | 318 | 319 | @subsubsection[#:tag "essential-continuation-exprs"]{ 320 | Essential Continuation Expressions 321 | } 322 | 323 | @defstruct*[done/ce ([type_ type_?])]{ 324 | A @tech{continuation expression} that represents that there's nothing left to do except return a value. The specified @tech{negative type} can serve as a hint for optimizing the value. 325 | } 326 | 327 | @defstruct*[apply/ce ([args any/c] [next continuation-expr?])]{ 328 | A @tech{continuation expression} that represents that the next thing to do to the value is to invoke it as an @tech{fexpr} with certain arguments. 329 | 330 | In typical code, the @racket[_args] to an fexpr call are usually a proper list. 331 | } 332 | 333 | 334 | @subsection[#:tag "type+"]{Positive Types} 335 | 336 | A @deftech{positive type} in Fexpress essentially acts like a symbolic value. Like other type systems, this kind of type designates a set of potential values. Depending on what assumptions it carries, it can produce a value (@racket[type+-eval]) and/or a @racket[compilation-result?] that evaluates to a value (@racket[type+-compile]). 337 | 338 | The type system in the Fexpress proof of concept exists only for the purpose of optimization, and it has only the bells and whistles that serve that purpose. In particular, this type system makes no attempt to be sound. A variable associated with a positive type can turn out to have a value that defies that type's assumptions or has been computed in a different way than the type would have computed it. 339 | 340 | @defproc[(type+? [v any/c]) boolean?]{ 341 | Returns whether the given value is a @tech{positive type}. 342 | } 343 | 344 | @defthing[gen:type+ any/c]{ 345 | A generic interface for @tech{positive types}, which must implement the following methods: 346 | 347 | @itemlist[ 348 | @item{@racket[type+-eval]} 349 | @item{@racket[type+-compile]} 350 | @item{@racket[at-variable/t+]} 351 | @item{@racket[type+-continue-eval/t+]} 352 | ] 353 | 354 | The implementations of these methods should satisfy certain algebraic laws: 355 | 356 | If both @racket[type+-eval] and @racket[type+-compile] both successfully produce results and don't perform any side effects along the way, the evaluation result should be the same as running the compilation result with @racket[compilation-result-eval] in any @racket[env?] where the bindings for its free variables have their own successful and pure @racket[type+-eval] and @racket[type+-compile] behaviors. 357 | 358 | The @racket[at-variable/t+] method should observe the lens laws with respect to @racket[type+-compile]: The result of getting a compilation result with @racket[type+-compile] after it's been replaced with @racket[at-variable/t+] should be the same as just calling @racket[var-compile] on the variable that was passed to the replacer. The result of replacing a compilation result with itself should be the same as not using @racket[at-variable/t+] at all. The result of replacing a compilation result and replacing it a second time should be the same as just skipping to the second replacement. 359 | } 360 | 361 | @defproc[(type+-eval [type+ type+?]) any/c]{ 362 | Attempt to compute a value of the given @tech{positive type}. 363 | } 364 | 365 | @defproc[(type+-compile [type+ type+?]) compilation-result?]{ 366 | Attempt to produce a @racket[compilation-result?] that evaluates to values of the given @tech{positive type} in the @racket[env?] the type belongs to. 367 | } 368 | 369 | @defproc[(at-variable/t+ [var var?] [type+ type+?]) type+?]{ 370 | Replaces the given @tech{positive type}'s compilation result so that it refers to the given Fexpress variable. The variable's potential bindings must be among the type's potential values, but nothing is done to verify this. 371 | 372 | Any type that's added to an @racket[env?] should be transformed this way, since it's now in scope under a dedicated name. 373 | } 374 | 375 | @defproc[ 376 | (type+-continue-eval/t+ [env env?] 377 | [cont continuation-expr?] 378 | [type+ type+?]) 379 | type+? 380 | ]{ 381 | (Makes @tech{fexpr} calls.) Returns a @tech{positive type} for the potential values which result from transforming the given @tech{positive type} according to a series of steps and a target @tech{negative type} listed in the given @tech{continuation expression}. 382 | 383 | There are many @tt{...-continue-eval/t+} and @tt{...-apply/t+} operations in Fexpress, and this is the most general one; it delegates to the others. 384 | } 385 | 386 | @defproc[ 387 | (lazy-value/t+ [eval (-> any/c)] [compile (-> compilation-result?)]) 388 | type+? 389 | ]{ 390 | Returns a @tech{positive type} with the given implementations of @racket[type+-eval] and @racket[type+-compile]. These should satisfy the algebraic laws described at @racket[gen:type+]. 391 | 392 | The resulting type doesn't carry any assumptions about the potential values' @tech{fexpr}-calling behavior. That is to say, its @racket[type+-continue-eval/t+] behavior only gives up and delegates to @racket[continuation-expr-continue-eval/t+]. 393 | } 394 | 395 | 396 | @subsubsection[#:tag "essential-positive-types"]{ 397 | Essential Positive Types 398 | } 399 | 400 | @defproc[(any-value/t+) type+?]{ 401 | Returns a @tech{positive type} which carries no assumptions about its potential values. 402 | } 403 | 404 | @defproc[(non-fexpr-value/t+) type+?]{ 405 | Returns a @tech{positive type} which carries an assumption that its potential values will not be @racket[fexpr?] values. This isn't necessarily a sound assumption, but certain operations will use this information to allow compilation to proceed even if a value of this type is invoked like an @tech{fexpr}. 406 | } 407 | 408 | @defproc[(specific-value/t+ [value any/c]) type+?]{ 409 | Returns a @tech{positive type} which carries an assumption that its potential values will all be the given value. It can also @racket[type+-eval] to that value. 410 | } 411 | 412 | 413 | @subsection[#:tag "type_"]{Negative Types} 414 | 415 | A @deftech{negative type} in Fexpress essentially acts like an optimization hint for compiling an expression of that type. 416 | 417 | @defproc[(type_? [v any/c]) boolean?]{ 418 | Returns whether the given value is a @tech{negative type}. 419 | } 420 | 421 | @defthing[gen:type_ any/c]{ 422 | A generic interface for @tech{negative types}. This interface doesn't have any methods. (It's not that it couldn't have methods, but we don't seem to need any for this proof of concept.) 423 | 424 | In order to perform compilation, Fexpress @tech{fexprs} sometimes need to know the structural details of the negative type they're expected to create a value in. Thus, when defining new negative types, it's typical to define a structure type that does more than just implement the @racket[gen:type_] interface. For instance, it can also provide its predicate and field accessors as part of its intended API, or it can implement other interfaces on the side. 425 | } 426 | 427 | 428 | @subsubsection[#:tag "essential-negative-types"]{ 429 | Essential Negative Types 430 | } 431 | 432 | @defstruct*[any-value/t_ ()]{ 433 | A @tech{negative type} which provides no hints as to what its potential values should be like. 434 | } 435 | 436 | @defstruct*[ 437 | ->/t_ 438 | ([arg-type+-list (listof type+?)] [return/t_ type_?]) 439 | ]{ 440 | A @tech{negative type} for functions that have the specified list of @tech{positive types} for their arguments and the single specified negative type for their results. 441 | 442 | If we unpack the meaning of positive and negative types in Fexpress, this is a compilation hint for expressions that return functions. It offers the given symbolic values as approximations for the function arguments, and it offers further hints for compiling the function body. 443 | } 444 | 445 | 446 | @subsection[#:tag "phases"]{Phases of the Language} 447 | 448 | 449 | @subsubsection[#:tag "representing-source"]{Representing Concepts in the Source} 450 | 451 | @defproc[(var? [v any/c]) boolean?]{ 452 | Returns whether the given value is an Fexpress variable name, which is represented by an interned symbol. 453 | } 454 | 455 | @defproc[(env? [v any/c]) boolean?]{ 456 | Returns whether the given value is an Fexpress lexical environment, which is represented by an immutable hash from variable names to @tech{positive types}. Besides being positive types, the values of the hash should also have successful @racket[type+-compile] behavior, and they should be equivalent to @racket[var-compile] for the same Fexpress variable. 457 | } 458 | 459 | @defproc[(free-vars? [v any/c]) boolean?]{ 460 | Returns whether the given value is an Fexpress free variable set, which is represented by an immutable hash from variable names to @racket[#t]. 461 | } 462 | 463 | @defproc[(env-get/t+ [env env?] [var var?]) type+?]{ 464 | Gets the @tech{positive type} associated with the given variable name in the given environment. Unlike simply calling @racket[hash-ref], this raises an informative error if the variable doesn't have a binding in the environment. 465 | } 466 | 467 | 468 | @subsubsection[#:tag "evaluator-compiler"]{The Combination Evaluator-Compiler} 469 | 470 | @defproc[ 471 | (fexpress-eval/t+ [env env?] [cont continuation-expr?] [expr any/c]) 472 | type+? 473 | ]{ 474 | Reduces the given Fexpress expression and @tech{continuation expression} in the given @racket[env?] to a @tech{positive type}. The resulting positive type can be transformed into an evaluated result using @racket[type+-eval] or a compiled result using @racket[type+-compile]. 475 | } 476 | 477 | @defproc[(literal? [v any/c]) boolean?]{ 478 | Returns whether the given value can be used as a datum literal in the Fexpress proof of concept. For this simple demonstration, we just support @racket[exact-integer?] values. 479 | } 480 | 481 | @defproc[ 482 | (unknown-non-fexpr-apply/t+ [env env?] 483 | [cont continuation-expr?] 484 | [op/t+ type+?] 485 | [get-op (-> any/c)] 486 | [args any/c]) 487 | type+? 488 | ]{ 489 | (Makes @tech{fexpr} calls, namely to an assumed non-fexpr value.) Returns a @tech{positive type} for the potential values which result from transforming the given positive type and the given function (for getting the value of that type) according to a @emph{procedure} call with the evaluated forms of the given arguments, followed by the series of additional steps and the target negative type listed in the given @tech{continuation expression}. 490 | 491 | There are many @tt{...-continue-eval/t+} and @tt{...-apply/t+} operations in Fexpress, and this is the one to call when a type's potential values are assumed not to be fexprs and yet they're definitely being invoked with an fexpr call. This is called either when a value turns out to be a non-fexpr at run time or when it's assumed to be a non-fexpr using @racket[non-fexpr-value/t+]. 492 | 493 | The given @racket[op/t+] type should be a type which evaluates to the result of @racket[get-op]. 494 | 495 | In typical code, the @racket[args] to an fexpr call are usually a proper list. This operation raises an error if they're not. 496 | } 497 | 498 | @defproc[ 499 | (specific-value-continue-eval/t+ [env env?] 500 | [cont continuation-expr?] 501 | [val/t+ type+?] 502 | [val any/c]) 503 | type+? 504 | ]{ 505 | (Makes @tech{fexpr} calls.) Returns a @tech{positive type} for the potential values which result from transforming the given positive type and the given value of that type according to the series of steps and the target @tech{negative type} listed in the given @tech{continuation expression}. 506 | 507 | There are many @tt{...-continue-eval/t+} and @tt{...-apply/t+} operations in Fexpress, and this is the one to call when the actual @emph{value} being called is known and can potentially be an fexpr with its own idea of how to proceed. A positive type processing a @racket[type+-continue-eval/t+] call usually delegates to this itself when the type's value is known at compile time, and a continuation expression processing a @racket[continuation-expr-continue-eval/t+] call usually delegates to this itself once the value is finally known at run time. 508 | 509 | The given @racket[val/t+] type should be a type which evaluates to the value @racket[val]. 510 | } 511 | 512 | @defproc[ 513 | (non-fexpr-continue-eval/t+ [env env?] 514 | [cont continuation-expr?] 515 | [val/t+ type+?]) 516 | type+? 517 | ]{ 518 | (Makes @tech{fexpr} calls.) Assuming the given @tech{positive type} and its values have no custom fexpr-calling behavior, returns a positive type for the potential values which result from transforming the given one according to the series of steps and the target @tech{negative type} listed in the given @tech{continuation expression}. 519 | 520 | There are many @tt{...-continue-eval/t+} and @tt{...-apply/t+} operations in Fexpress, and this is the one to call when the positive type @emph{and} its values should have their custom fexpr-calling behavior ignored. Fexpress doesn't usually ignore values' fexpr-calling behavior like this, but since this can lead to better performance, it can be explicitly requested by using @racket[(fexpress-the _...)] to ascribe a type that uses @racket[non-fexpr-value/t+]. 521 | } 522 | 523 | 524 | @subsubsection[#:tag "compiling-to-racket"]{Compiling to Racket} 525 | 526 | @defproc[(var-representation-in-racket [var var?]) symbol?]{ 527 | Converts an Fexpress variable name into the symbol it should be represented as in compiled Racket code for @racket[compilation-result?] values. Currently, it's the same symbol but with @racket["-"] prepended to it. 528 | } 529 | 530 | @defstruct*[ 531 | compilation-result 532 | ([depends-on-env? boolean?] [free-vars free-vars?] [expr any/c]) 533 | ]{ 534 | A bundle containing an s-expression ready to compile as Racket code and some information about its dependencies. If it depends on the variable @tt{env} being bound to a first-class representation of the entire lexical environment, the @racket[_depends-on-env?] field should be @racket[#t]. If it depends on Fexpress variables, those should be accounted for in the @racket[_free-vars] field. 535 | 536 | The @racket[_expr] should be an s-expression of Racket code. It may have free variables corresponding to the @racket[var-representation-in-racket] versions of the Fexpress free variables listed in @racket[_free-vars]. It may also have the free variable @tt{env} if @racket[_depends-on-env?] is @racket[#t]. The @tt{env} variable refers to the current lexical environment. It should not have other free variables, but if it needs to refer to Racket module bindings, it may do so with an embedded @racket[identifier?] syntax object. 537 | 538 | Depending on the lexical environment using @racket[_depends-on-env?] can lead to performance degradation in the surrounding parts of the Fexpress program, since an up-to-date first-class environment value must be constructed whenever variables come into scope. 539 | 540 | While we could make more extensive use of Racket syntax objects, we keep their use to a minimum here to demonstrate this language in a way that can be easily ported to other Lisp dialects and other languages with @racket[eval] variants available. 541 | } 542 | 543 | @defproc[(var-compile [var var?]) compilation-result?]{ 544 | Compiles an expression that just refers to the given Fexpress variable. 545 | } 546 | 547 | @defproc[ 548 | (compilation-result-eval [env env?] [compiled compilation-result?]) 549 | any/c 550 | ]{ 551 | Evaluates the given Fexpress compilation result, using the given Fexpress @racket[env?] to resolve its references to free variables. This uses Racket's @racket[eval], which fully compiles the Racket code before executing it. 552 | } 553 | -------------------------------------------------------------------------------- /fexpress-lib/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "fexpress") 4 | 5 | (define deps (list "base")) 6 | -------------------------------------------------------------------------------- /fexpress-lib/main.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | ; fexpress 4 | ; 5 | ; Fexpress, a compilation-friendly fexpr language. 6 | 7 | ; Copyright 2021 The Fexpress 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 | ; TODO: Once we have a non-minimalist fexpr language, put it here. For 23 | ; now, we're just implementing a very minimalist proof of concept in 24 | ; `fexpress/proof-of-concept`. 25 | -------------------------------------------------------------------------------- /fexpress-lib/proof-of-concept.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | ; fexpress/proof-of-concept 4 | ; 5 | ; The Fexpress proof-of-concept, a minimalist, compilation-friendly 6 | ; fexpr language. 7 | 8 | ; Copyright 2021 The Fexpress 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 racket/contract/base 25 | -> any any/c contract-out hash/c listof parameter/c)) 26 | (require (only-in racket/function const)) 27 | (require (only-in racket/generic define-generics)) 28 | (require (only-in racket/hash hash-union)) 29 | (require (only-in racket/list append*)) 30 | (require (only-in racket/match match match-define match-lambda**)) 31 | (require (only-in racket/syntax format-symbol)) 32 | 33 | 34 | (provide 35 | 36 | ; Diagnostic information collection 37 | (contract-out 38 | [current-fexpress-logger (parameter/c (-> any/c any))]) 39 | 40 | ; Representing concepts in Fexpress source code 41 | (contract-out 42 | [var? (-> any/c boolean?)] 43 | [free-vars? (-> any/c boolean?)] 44 | [env? (-> any/c boolean?)] 45 | [env-get/t+ (-> env? var? type+?)]) 46 | 47 | ; Generic interfaces to serve as extension points 48 | gen:fexpr 49 | (contract-out 50 | [fexpr? (-> any/c boolean?)] 51 | [fexpr-apply/t+ 52 | (-> env? continuation-expr? type+? fexpr? any/c type+?)]) 53 | ; NOTE: Instead of exporting this as a struct, we export only the 54 | ; constructor. 55 | #; 56 | (struct-out makeshift-fexpr) 57 | (contract-out 58 | [makeshift-fexpr 59 | (-> (-> env? continuation-expr? type+? any/c type+?) fexpr?)]) 60 | gen:continuation-expr 61 | (contract-out 62 | [continuation-expr? (-> any/c boolean?)] 63 | [continuation-expr-continue-eval/t+ 64 | (-> env? continuation-expr? type+? type+?)]) 65 | gen:type+ 66 | (contract-out 67 | [type+? (-> any/c boolean?)] 68 | [type+-eval (-> type+? any/c)] 69 | [type+-compile (-> type+? compilation-result?)] 70 | [at-variable/t+ (-> var? type+? type+?)] 71 | [type+-continue-eval/t+ 72 | (-> env? continuation-expr? type+? type+?)]) 73 | gen:type_ 74 | (contract-out 75 | [type_? (-> any/c boolean?)]) 76 | 77 | ; Utilities for compiling to Racket 78 | (contract-out 79 | [var-representation-in-racket (-> var? symbol?)]) 80 | (struct-out compilation-result) 81 | (contract-out 82 | [var-compile (-> var? compilation-result?)] 83 | [compilation-result-eval (-> env? compilation-result? any/c)]) 84 | 85 | ; Positive types 86 | ; 87 | ; NOTE: Instead of exporting all of these as structs, we export just 88 | ; some of them, and we export only the constructors. This seems to 89 | ; be sufficient; nothing we've built so far needs to know the 90 | ; identity of a positive type, just invoke its own methods. 91 | ; 92 | ; (struct-out lazy-value/t+) 93 | ; (struct-out any-variable-bound-value/t+) 94 | ; (struct-out any-value/t+) 95 | ; (struct-out variable-bound-non-fexpr-value/t+) 96 | ; (struct-out non-fexpr-value/t+) 97 | ; (struct-out specific-variable-bound-value/t+) 98 | ; (struct-out specific-value/t+) 99 | (contract-out 100 | [lazy-value/t+ (-> (-> any/c) (-> compilation-result?) type+?)] 101 | [any-value/t+ (-> type+?)] 102 | [non-fexpr-value/t+ (-> type+?)] 103 | [specific-value/t+ (-> any/c type+?)]) 104 | 105 | ; Negative types 106 | (struct-out any-value/t_) 107 | (struct-out ->/t_) 108 | 109 | ; Continuation expressions 110 | (struct-out done/ce) 111 | (struct-out apply/ce) 112 | 113 | ; The Fexpress combination evaluator-compiler 114 | (contract-out 115 | [literal? (-> any/c boolean?)] 116 | [fexpress-eval/t+ (-> env? continuation-expr? any/c type+?)] 117 | [unknown-non-fexpr-apply/t+ 118 | (-> env? continuation-expr? type+? (-> any/c) any/c type+?)] 119 | [specific-value-continue-eval/t+ 120 | (-> env? continuation-expr? type+? any/c type+?)] 121 | [non-fexpr-continue-eval/t+ 122 | (-> env? continuation-expr? type+? type+?)]) 123 | 124 | ; Fexprs for lambda operations 125 | (struct-out parsed-lambda-args) 126 | (contract-out 127 | [parse-lambda-args (-> symbol? any/c parsed-lambda-args?)] 128 | [fexpress-ilambda fexpr?] 129 | 130 | ; NOTE: We treat this as internal. 131 | #; 132 | [compile-clambda 133 | (-> env? continuation-expr? any/c compilation-result?)] 134 | 135 | [fexpress-clambda fexpr?]) 136 | 137 | ; An fexpr for type ascription 138 | (contract-out 139 | [fexpress-the fexpr?]) 140 | 141 | ; The Fexpress entrypoint 142 | (contract-out 143 | [env-of-specific-values (-> (hash/c var? any/c) env?)] 144 | [fexpress-eval (-> env? any/c any/c)]) 145 | 146 | ) 147 | 148 | 149 | (define-namespace-anchor here) 150 | 151 | 152 | 153 | ; ===== Diagnostic information collection ============================ 154 | 155 | ; A parameter holding a procedure that the Fexpress proof of concept 156 | ; uses to log diagnostic messages in s-expression form. Currently, we 157 | ; log two things: 158 | ; 159 | ; * Evaluation of Racket code, so that we can inspect what kind of 160 | ; Racket code Fexpress produces. 161 | ; 162 | ; * Moments where the compiled Racket code reenters the interpreter 163 | ; in order to call an fexpr. These moments are worth knowing about 164 | ; so we can optimize them away. 165 | ; 166 | ; Contract: 167 | ; (parameter/c (-> any/c any)) 168 | ; 169 | (define current-fexpress-logger (make-parameter (lambda (message) 170 | (void)))) 171 | 172 | 173 | 174 | ; ===== Representing concepts in Fexpress source code ================ 175 | 176 | ; A contract that recognizes this language's variable names, which are 177 | ; represented by interned symbols. 178 | ; 179 | ; Contract: 180 | ; (-> any/c boolean?) 181 | ; 182 | (define (var? v) 183 | (and (symbol? v) (symbol-interned? v))) 184 | 185 | ; A contract that recognizes Fexpress free variable sets, which are 186 | ; represented by immutable hashes from variable names to `#t`. 187 | ; 188 | ; Contract: 189 | ; (-> any/c boolean?) 190 | ; 191 | (define (free-vars? v) 192 | (and (hash? v) (immutable? v) 193 | (for/and ([(k v) (in-hash v)]) 194 | (and (var? k) (equal? #t v))))) 195 | 196 | ; A contract that recognizes this language's lexical environments, 197 | ; which are represented by immutable hashes from variable names to 198 | ; positive types. Besides being positive types, the values of the hash 199 | ; should also have successful `type+-compile` behavior, and it should 200 | ; be equivalent to `var-compile` for the same Fexpress variable. 201 | ; 202 | ; Contract: 203 | ; (-> any/c boolean?) 204 | ; 205 | (define (env? v) 206 | (and (hash? v) (immutable? v) 207 | (for/and ([(k v) (in-hash v)]) 208 | (and (var? k) (type+? v))))) 209 | 210 | ; Contract: 211 | ; (-> env? var? type+?) 212 | (define (env-get/t+ env var) 213 | (hash-ref env var 214 | (lambda () 215 | (raise-arguments-error 'env-get/t+ 216 | "Unbound variable" 217 | "var" var 218 | "env" env)))) 219 | 220 | 221 | 222 | ; ===== Generic interfaces to serve as extension points ============== 223 | 224 | ; An fexpr. In Fexpress, the calling convention for fexprs is a bit 225 | ; different than it might be in other fexpr languages. They receive 226 | ; their arguments on a continuation expression, and they can implement 227 | ; both compilation behavior and dynamic behavior via the interface of 228 | ; the positive types they return. 229 | ; 230 | (define-generics fexpr 231 | 232 | ; (Makes fexpr calls, namely to this one.) Returns a positive type 233 | ; for the potential values which result from transforming the given 234 | ; positive type and the given value (this fexpr) of that type 235 | ; according to an fexpr call with the given arguments followed by 236 | ; the series of additional steps and the target negative type listed 237 | ; in the given continuation expression. 238 | ; 239 | ; There are many `...-continue-eval/t+` and `...-apply/t+` 240 | ; operations in Fexpress, and this is the one to call when the 241 | ; actual *value* of the original type is known and is definitely an 242 | ; fexpr that is definitely being invoked. 243 | ; 244 | ; Contract: 245 | ; (-> env? continuation-expr? type+? fexpr? any/c type+?) 246 | ; 247 | ; The given positive type should evaluate to a value that is this 248 | ; fexpr. 249 | ; 250 | (fexpr-apply/t+ env cont fexpr/t+ fexpr args)) 251 | 252 | ; An fexpr. This way of creating an fexpr is good for when it isn't 253 | ; necessary to give the fexpr other structure type properties. Where 254 | ; the `gen:fexpr` interface is like `prop:procedure`, the 255 | ; `makeshift-fexpr` interface is like `lambda`. 256 | ; 257 | ; Field contract: 258 | ; (-> env? continuation-expr? type+? type+?) 259 | ; 260 | (struct makeshift-fexpr (apply/t+) #:transparent 261 | #:methods gen:fexpr 262 | [(define (fexpr-apply/t+ env cont op/t+ op args) 263 | (match-define (makeshift-fexpr apply/t+) op) 264 | (apply/t+ env cont op/t+ args))] 265 | #:property prop:procedure 266 | (lambda (fexpr . args) 267 | (error "attempted to call an Fexpress fexpr from Racket code, which is not supported"))) 268 | 269 | ; A continuation expression, which is a representation of the syntax 270 | ; around the evaluating part of an Fexpress expression. 271 | ; 272 | ; Usually, this is a series of pending fexpr applications (`apply/ce`) 273 | ; to perform in the current environment, followed by an ascribed 274 | ; negative type to optimize the overall result by (`done/ce`). Other 275 | ; kinds of copatterns or spine elements, like field or method accessor 276 | ; syntaxes, could fit in here as well. 277 | ; 278 | (define-generics continuation-expr 279 | 280 | ; (Makes fexpr calls.) Assuming the given positive type will have no 281 | ; known fexpr-calling behavior until we witness its potential 282 | ; values, returns another positive type for the potential values 283 | ; which result from transforming those according to the series of 284 | ; steps and the target negative type listed in the given 285 | ; continuation expression. 286 | ; 287 | ; There are many `...-continue-eval/t+` and `...-apply/t+` 288 | ; operations in Fexpress, and this is the one to call when the 289 | ; positive type's fexpr-calling behavior should be ignored but its 290 | ; values' fexpr-calling behavior, if any, should not be ignored. 291 | ; This will usually result in code that consults the value at run 292 | ; time and makes fexpr calls to it dynamically. A positive type 293 | ; usually delegates to this itself when its `type+-continue-eval/t+` 294 | ; behavior has no better idea for what to do. 295 | ; 296 | ; Contract: 297 | ; (-> env? continuation-expr? type+? type+?) 298 | ; 299 | (continuation-expr-continue-eval/t+ env continuation-expr val/t+)) 300 | 301 | ; A positive type. Positive types in Fexpress arguably act less like 302 | ; types and more like symbolic values. 303 | (define-generics type+ 304 | 305 | ; Attempts to compute a value of the type. 306 | ; 307 | ; Contract: 308 | ; (-> type+? any/c) 309 | ; 310 | (type+-eval type+) 311 | 312 | ; Attempts to produce a compilation result that evaluates to values 313 | ; of the given type in the environment the type belongs to. 314 | ; 315 | ; Contract: 316 | ; (-> type+? compilation-result?) 317 | ; 318 | (type+-compile type+) 319 | 320 | ; Replaces the type's compilation result so that it refers to the 321 | ; given Fexpress variable. The variable's potential bindings must be 322 | ; among the type's potential values, but nothing is done to verify 323 | ; this. 324 | ; 325 | ; Any type that's added to an environment should be transformed this 326 | ; way, since it's now in scope under a dedicated name. 327 | ; 328 | ; Contract: 329 | ; (-> var? type+? type+?) 330 | ; 331 | (at-variable/t+ var type+) 332 | 333 | ; (Makes fexpr calls.) Returns a positive type for the potential 334 | ; values which result from transforming this type according to a 335 | ; series of steps and a target negative type listed in the given 336 | ; continuation expression. 337 | ; 338 | ; There are many `...-continue-eval/t+` and `...-apply/t+` 339 | ; operations in Fexpress, and this is the most general one; it 340 | ; delegates to the others. 341 | ; 342 | ; Contract: 343 | ; (-> env? continuation-expr? type+? type+?) 344 | ; 345 | (type+-continue-eval/t+ env cont type+)) 346 | 347 | ; A negative type. Fexpress's fexprs can use these as optimization 348 | ; hints. There are no methods because each fexpr may have different 349 | ; kinds of special cases it's looking to optimize. (That's not to say 350 | ; we wouldn't add methods if we turned out to have a use for them.) 351 | ; 352 | (define-generics type_ 353 | ) 354 | 355 | 356 | 357 | ; ===== Utilities for compiling to Racket ============================ 358 | 359 | ; Converts an Fexpress variable name into the symbol it should be 360 | ; represented as in compiled Racket code for `compilation-result?` 361 | ; values. Currently, it's the same symbol but with "-" prepended to 362 | ; it. 363 | ; 364 | ; Contract: 365 | ; (-> var? symbol?) 366 | ; 367 | (define (var-representation-in-racket var) 368 | (format-symbol "-~a" var)) 369 | 370 | ; Field contracts: 371 | ; boolean? free-vars? any/c 372 | ; 373 | ; The `expr` should be an s-expression of Racket code. It may have 374 | ; free variables corresponding to the `var-representation-in-racket` 375 | ; versions of what the Fexpress free variables would be. It may also 376 | ; have the free variable `env` if `depends-on-env?` is `#t`. The `env` 377 | ; variable refers to the current lexical environment. It may also have 378 | ; Racket syntax objects, so as to refer unambiguously to Racket module 379 | ; imports. 380 | ; 381 | ; Depending on the lexical environment using `depends-on-env?` can 382 | ; lead to performance degradation in the surrounding parts of the 383 | ; Fexpress program, since an up-to-date first-class environment value 384 | ; must be constructed whenever variables come into scope. 385 | ; 386 | ; While we could make more extensive use of Racket syntax objects, we 387 | ; keep their use to a minimum here to demonstrate this language in a 388 | ; way that can be easily ported to other Lisp dialects and other 389 | ; languages with `eval` variants available. 390 | ; 391 | (struct compilation-result (depends-on-env? free-vars expr) 392 | #:transparent) 393 | 394 | ; Compiles an expression that just refers to the given Fexpress 395 | ; variable. 396 | ; 397 | ; Contract: 398 | ; (-> var? compilation-result?) 399 | ; 400 | (define (var-compile var) 401 | (compilation-result #f (hash var #t) 402 | (var-representation-in-racket var))) 403 | 404 | ; TODO CLEANUP: Consider implementing some `compilation-result` monad 405 | ; operations here. They might make parts of the code more 406 | ; straightforward for people used to monads, but I'm not sure if 407 | ; that's the audience. 408 | 409 | ; Evaluates the given Fexpress compilation result, using the given 410 | ; Fexpress environment to resolve its references to free variables. 411 | ; This uses Racket's `eval`, which fully compiles the Racket code 412 | ; before executing it. 413 | ; 414 | ; Contract: 415 | ; (-> env? compilation-result? any/c) 416 | ; 417 | (define (compilation-result-eval env compiled) 418 | (match-define 419 | (compilation-result depends-on-env? free-vars lambda-compiled) 420 | compiled) 421 | 422 | ; NOTE: We sort the free variables to make our generated Racket code 423 | ; deterministic, since we check that code in tests. 424 | ; 425 | ; TODO: See if we should use a sorted collection in the first place 426 | ; so we don't have to do this. 427 | ; 428 | (define free-vars-list (sort (hash-keys free-vars) symboldatum (datum->syntax #f lambda-maker-compiled)))) 439 | (define lambda-maker 440 | (eval lambda-maker-compiled 441 | (namespace-anchor->namespace here))) 442 | (define free-var-type+-list 443 | (for/list ([free-var (in-list free-vars-list)]) 444 | (env-get/t+ env free-var))) 445 | (define free-var-val-list 446 | (for/list ([val/t+ (in-list free-var-type+-list)]) 447 | (type+-eval val/t+))) 448 | (apply lambda-maker 449 | (for/fold ([lambda-maker-env env]) 450 | ([free-var (in-list free-vars-list)] 451 | [val/t+ (in-list free-var-type+-list)]) 452 | (hash-set lambda-maker-env free-var 453 | (at-variable/t+ free-var val/t+))) 454 | free-var-val-list)) 455 | 456 | 457 | 458 | ; ===== Positive types =============================================== 459 | 460 | ; Field contracts: 461 | ; (-> any/c) (-> compilation-result?) 462 | (struct lazy-value/t+ (eval compile) #:transparent 463 | #:methods gen:type+ 464 | [(define (type+-eval type+) 465 | (match-define (lazy-value/t+ eval compile) type+) 466 | (eval)) 467 | (define (type+-compile type+) 468 | (match-define (lazy-value/t+ eval compile) type+) 469 | (compile)) 470 | (define (at-variable/t+ var type+) 471 | (match-define (lazy-value/t+ eval compile) type+) 472 | (lazy-value/t+ eval (const (var-compile var)))) 473 | (define (type+-continue-eval/t+ env cont val/t+) 474 | (continuation-expr-continue-eval/t+ env cont val/t+))]) 475 | 476 | ; (A helper for `any-value/t+`.) 477 | ; 478 | ; Field contract: 479 | ; var? 480 | ; 481 | (struct any-variable-bound-value/t+ (var) #:transparent 482 | #:methods gen:type+ 483 | [(define (type+-eval type+) 484 | (error "tried to evaluate the value level of the ascribed type `(any-value/t+)`")) 485 | (define (type+-compile type+) 486 | (match-define (any-variable-bound-value/t+ var) type+) 487 | (var-compile var)) 488 | (define (at-variable/t+ var type+) 489 | (any-variable-bound-value/t+ var)) 490 | (define (type+-continue-eval/t+ env cont val/t+) 491 | (continuation-expr-continue-eval/t+ env cont val/t+))]) 492 | 493 | (struct any-value/t+ () #:transparent 494 | #:methods gen:type+ 495 | [(define (type+-eval type+) 496 | (error "tried to evaluate the value level of the ascribed type `(any-value/t+)`")) 497 | (define (type+-compile type+) 498 | (error "tried to compile the value level of the ascribed type `(any-value/t+)`")) 499 | (define (at-variable/t+ var type+) 500 | (any-variable-bound-value/t+ var)) 501 | (define (type+-continue-eval/t+ env cont val/t+) 502 | (continuation-expr-continue-eval/t+ env cont val/t+))]) 503 | 504 | ; ----- 505 | ; NOTE: Past this point, a few things are mutually recursive. 506 | ; 507 | ; Each element of the of the following 6-cycle depends on the one 508 | ; before (wrapping around from the first to the last): 509 | ; 510 | ; * `specific-variable-bound-value/t+` 511 | ; * `specific-value/t+` 512 | ; * `apply/ce` 513 | ; * `fexpress-eval/t+` 514 | ; * `unknown-non-fexpr-apply/t+` 515 | ; * `specific-value-continue-eval/t+` 516 | ; 517 | ; Each element of the following 3-path depends on each one adjacent to 518 | ; it (without wrapping): 519 | ; 520 | ; * `specific-value/t+` 521 | ; * `specific-value-continue-eval/t+` 522 | ; * `apply/ce` 523 | ; 524 | ; Cycles of other lengths may be traced through the relationships 525 | ; already described. 526 | ; 527 | ; We also place `variable-bound-non-fexpr-value/t+` and 528 | ; `non-fexpr-value/t+` here, although they depend on 529 | ; `non-fexpr-continue-eval/t+` defined further below. We could put 530 | ; them just after `non-fexpr-continue-eval/t+`, but we put them here 531 | ; for code organization purposes, to group all the positive type 532 | ; definitions together. 533 | ; ----- 534 | 535 | ; (A helper for `non-fexpr-value/t+`.) 536 | ; 537 | ; A positive type of values that we're allowing ourselves to assume to 538 | ; be non-fexprs for optimization purposes, and which are also known to 539 | ; be bound to the specified Fexpress variable. 540 | ; 541 | ; Field contract: 542 | ; var? 543 | ; 544 | (struct variable-bound-non-fexpr-value/t+ (var) #:transparent 545 | #:methods gen:type+ 546 | [(define (type+-eval type+) 547 | (error "tried to evaluate the value level of the ascribed type `(non-fexpr-value/t+)`")) 548 | (define (type+-compile type+) 549 | (match-define (variable-bound-non-fexpr-value/t+ var) type+) 550 | (var-compile var)) 551 | (define (at-variable/t+ var type+) 552 | (variable-bound-non-fexpr-value/t+ var)) 553 | (define (type+-continue-eval/t+ env cont val/t+) 554 | (non-fexpr-continue-eval/t+ env cont val/t+))]) 555 | 556 | ; A positive type of values that we're allowing ourselves to assume to 557 | ; be non-fexprs for optimization purposes. 558 | (struct non-fexpr-value/t+ () #:transparent 559 | #:methods gen:type+ 560 | [(define (type+-eval type+) 561 | (error "tried to evaluate the value level of the ascribed type `(non-fexpr-value/t+)`")) 562 | (define (type+-compile type+) 563 | (error "tried to compile the value level of the ascribed type `(non-fexpr-value/t+)`")) 564 | (define (at-variable/t+ var type+) 565 | (variable-bound-non-fexpr-value/t+ var)) 566 | (define (type+-continue-eval/t+ env cont val/t+) 567 | (non-fexpr-continue-eval/t+ env cont val/t+))]) 568 | 569 | ; (A helper for `specific-value/t+`.) 570 | ; 571 | ; Field contracts: 572 | ; var? any/c 573 | ; 574 | (struct specific-variable-bound-value/t+ (var val) #:transparent 575 | #:methods gen:type+ 576 | [(define (type+-eval type+) 577 | (match-define (specific-variable-bound-value/t+ var val) type+) 578 | val) 579 | (define (type+-compile type+) 580 | (match-define (specific-variable-bound-value/t+ var val) type+) 581 | (var-compile var)) 582 | (define (at-variable/t+ var type+) 583 | (match-define (specific-variable-bound-value/t+ original-var val) 584 | type+) 585 | (specific-variable-bound-value/t+ var val)) 586 | (define (type+-continue-eval/t+ env cont val/t+) 587 | (match-define (specific-variable-bound-value/t+ var val) val/t+) 588 | (specific-value-continue-eval/t+ env cont val/t+ val))]) 589 | 590 | ; Field contract: 591 | ; any/c 592 | (struct specific-value/t+ (value) #:transparent 593 | #:methods gen:type+ 594 | [(define (type+-eval type+) 595 | (match-define (specific-value/t+ value) type+) 596 | value) 597 | (define (type+-compile type+) 598 | (error "Tried to compile the a value level of a positive type that represented a reified first-class value that didn't have a known compiled form. This may be an internal bug in the Fexpress proof of concept.")) 599 | (define (at-variable/t+ var type+) 600 | (match-define (specific-value/t+ value) type+) 601 | (specific-variable-bound-value/t+ var value)) 602 | (define (type+-continue-eval/t+ env cont val/t+) 603 | (match-define (specific-value/t+ value) val/t+) 604 | (specific-value-continue-eval/t+ env cont val/t+ value))]) 605 | 606 | 607 | 608 | ; ===== Negative types =============================================== 609 | 610 | (struct any-value/t_ () #:transparent 611 | #:methods gen:type_ 612 | []) 613 | 614 | ; A negative type for functions that have the specified list of 615 | ; positive types for their arguments and the single specified negative 616 | ; type for their results. 617 | ; 618 | ; If we unpack the meaning of positive and negative types in Fexpress, 619 | ; this is a compilation hint for expressions that return functions. It 620 | ; offers the given symbolic values as approximations for the function 621 | ; arguments, and it offers further hints for compiling the function 622 | ; body. 623 | ; 624 | ; Field contracts: 625 | ; (listof type+?) type_? 626 | ; 627 | (struct ->/t_ (arg-type+-list return/t_) #:transparent 628 | #:methods gen:type_ 629 | []) 630 | 631 | 632 | 633 | ; ===== Continuation expressions ===================================== 634 | 635 | ; A continuation expression that represents that there's nothing left 636 | ; to do except return a value. The specified negative type can serve 637 | ; as a hint for optimizing the value. 638 | ; 639 | ; Field contract: 640 | ; type_? 641 | ; 642 | (struct done/ce (type_) #:transparent 643 | #:methods gen:continuation-expr 644 | [(define (continuation-expr-continue-eval/t+ env cont val/t+) 645 | val/t+)]) 646 | 647 | ; A continuation expression that represents that the next thing to do 648 | ; to the value is to invoke it as an fexpr with certain arguments. 649 | ; 650 | ; Field contracts: 651 | ; any/c continuation-expr? 652 | ; 653 | ; In typical code, the `args` to an fexpr call are usually a proper 654 | ; list. 655 | ; 656 | (struct apply/ce (args next) #:transparent 657 | #:methods gen:continuation-expr 658 | [(define (continuation-expr-continue-eval/t+ env cont val/t+) 659 | (match-define (apply/ce args next) cont) 660 | (type+-continue-eval/t+ env next 661 | (lazy-value/t+ 662 | (lambda () 663 | (type+-eval 664 | (specific-value-continue-eval/t+ 665 | env 666 | (apply/ce args (done/ce (any-value/t_))) 667 | val/t+ 668 | (type+-eval val/t+)))) 669 | (lambda () 670 | (match-define (compilation-result _ free-vars op-compiled) 671 | (type+-compile val/t+)) 672 | (compilation-result #t free-vars 673 | 674 | ; NOTE: Since we're just going to `type+-eval` it anyway, 675 | ; we don't need anything more than `specific-value/t+` 676 | ; here. The `val/t+` type may know extra things like how 677 | ; to compile the value, but we don't need to make any 678 | ; effort to reify that information here. 679 | ; 680 | `(,#'begin 681 | ( 682 | (,#'current-fexpress-logger) 683 | "Reentering the interpreter") 684 | (,#'type+-eval 685 | (,#'type+-continue-eval/t+ 686 | env 687 | (,#'apply/ce (,#'quote ,args) 688 | (,#'done/ce (,#'any-value/t_))) 689 | (,#'specific-value/t+ ,op-compiled)))))))))]) 690 | 691 | 692 | 693 | ; ===== The Fexpress combination evaluator-compiler ================== 694 | 695 | ; Returns whether the given value can be used as a datum literal in 696 | ; the Fexpress proof of concept. For this simple demonstration, we 697 | ; just support integers. 698 | ; 699 | ; Contract: 700 | ; (-> any/c boolean?) 701 | ; 702 | (define (literal? v) 703 | (exact-integer? v)) 704 | 705 | ; Reduces the given Fexpress expression and continuation expression in 706 | ; the given environment to a positive type. The resulting positive 707 | ; type can be transformed into an evaluated result using `type+-eval` 708 | ; or a compiled result using `type+-compile`. 709 | ; 710 | ; Contract: 711 | ; (-> env continuation-expr? any/c type+?) 712 | ; 713 | ; The `expr` should be an s-expression of Fexpress code. 714 | ; 715 | (define (fexpress-eval/t+ env cont expr) 716 | (match expr 717 | [`(,op-expr . ,args) 718 | (fexpress-eval/t+ env (apply/ce args cont) op-expr)] 719 | [(? symbol? var) 720 | (type+-continue-eval/t+ env cont (env-get/t+ env var))] 721 | [(? literal? val) 722 | (type+-continue-eval/t+ env cont 723 | ; TODO LAZY: Rather than just using `lazy-value/t+` here, we 724 | ; could also specialize `type+-continue-eval/t+` to raise an 725 | ; error if a literal is used in functional position. 726 | (lazy-value/t+ 727 | (const val) 728 | (const 729 | (compilation-result #f (hash) `(,#'#%datum . ,val)))))] 730 | [_ 731 | (raise-arguments-error 'fexpress-eval/t+ 732 | "unrecognized expression" 733 | "expr" expr)])) 734 | 735 | ; (Makes fexpr calls, namely to an assumed non-fexpr value.) Returns a 736 | ; positive type for the potential values which result from 737 | ; transforming the given positive type and the given function (for 738 | ; getting the value of that type) according to a *procedure* call with 739 | ; the evaluated forms of the given arguments, followed by the series 740 | ; of additional steps and the target negative type listed in the given 741 | ; continuation expression. 742 | ; 743 | ; There are many `...-continue-eval/t+` and `...-apply/t+` operations 744 | ; in Fexpress, and this is the one to call when a type's potential 745 | ; values are assumed not to be fexprs and yet they're definitely being 746 | ; invoked with an fexpr call. This is called either when a value turns 747 | ; out to be a non-fexpr at run time or when it's assumed to be a 748 | ; non-fexpr using `non-fexpr-value/t+`. 749 | ; 750 | ; Contract: 751 | ; (-> env? continuation-expr? type+? (-> any/c) any/c type+?) 752 | ; 753 | ; The given `op/t+` type should be a type which evaluates to the 754 | ; result of `get-op`. 755 | ; 756 | ; In typical code, the `args` to an fexpr call are usually a proper 757 | ; list. This operation raises an error if they're not. 758 | ; 759 | (define (unknown-non-fexpr-apply/t+ env cont op/t+ get-op args) 760 | (unless (list? args) 761 | (error "found an improper list of arguments when processing a procedure call")) 762 | (define arg-type+-list 763 | (for/list ([arg (in-list args)]) 764 | (fexpress-eval/t+ env (done/ce (any-value/t_)) arg))) 765 | (type+-continue-eval/t+ env cont 766 | 767 | ; TODO LAZY: Rather than just using `lazy-value/t+` here, we could 768 | ; also specialize `type+-continue-eval/t+` to treat certain 769 | ; procedures as being guaranteed not to return an fexpr. That 770 | ; could let us use those procedure calls in functional position 771 | ; without inhibiting compilation. 772 | ; 773 | (lazy-value/t+ 774 | (lambda () 775 | (apply (get-op) 776 | (for/list ([arg/t+ (in-list arg-type+-list)]) 777 | (type+-eval arg/t+)))) 778 | (lambda () 779 | (define op-compilation-result (type+-compile op/t+)) 780 | (define arg-compilation-results 781 | (for/list ([arg/t+ (in-list arg-type+-list)]) 782 | (type+-compile arg/t+))) 783 | (match-define 784 | (compilation-result 785 | op-depends-on-env? op-free-vars op-compiled) 786 | op-compilation-result) 787 | (let next ([depends-on-env? op-depends-on-env?] 788 | [free-vars op-free-vars] 789 | [rev-compiled-args (list)] 790 | [arg-compilation-results arg-compilation-results]) 791 | (match arg-compilation-results 792 | [(list) 793 | (compilation-result depends-on-env? free-vars 794 | `(,#'#%app ,op-compiled 795 | ,@(reverse rev-compiled-args)))] 796 | [(cons 797 | (compilation-result arg-depends-on-env? 798 | arg-free-vars 799 | compiled-arg) 800 | arg-compilation-results) 801 | (next (or depends-on-env? arg-depends-on-env?) 802 | (hash-union free-vars arg-free-vars 803 | #:combine (match-lambda** 804 | [(#t #t) #t])) 805 | (cons compiled-arg rev-compiled-args) 806 | arg-compilation-results)])))))) 807 | 808 | ; (Makes fexpr calls.) Returns a positive type for the potential 809 | ; values which result from transforming the given positive type and 810 | ; the given value of that type according to the series of steps and 811 | ; the target negative type listed in the given continuation 812 | ; expression. 813 | ; 814 | ; There are many `...-continue-eval/t+` and `...-apply/t+` operations 815 | ; in Fexpress, and this is the one to call when the actual *value* 816 | ; being called is known and can potentially be an fexpr with its own 817 | ; idea of how to proceed. A positive type processing a 818 | ; `type+-continue-eval/t+` call usually delegates to this itself when 819 | ; the type's value is known at compile time, and a continuation 820 | ; expression processing a `continuation-expr-continue-eval/t+` call 821 | ; usually delegates to this itself once the value is finally known at 822 | ; run time. 823 | ; 824 | ; Contract: 825 | ; (-> env? continuation-expr? type+? any/c type+?) 826 | ; 827 | ; The given `val/t+` type should be a type which evaluates to the 828 | ; value `val`. 829 | ; 830 | (define (specific-value-continue-eval/t+ env cont val/t+ val) 831 | ; TODO CLEANUP: Consider moving this branch to a 832 | ; `continuation-expr-continue-eval-value/t+` method. 833 | (match cont 834 | [(apply/ce args cont) 835 | (cond 836 | [(fexpr? val) (fexpr-apply/t+ env cont val/t+ val args)] 837 | [#t 838 | (cond 839 | [(procedure? val) 840 | (unless (and (list? args) 841 | (procedure-arity-includes? val (length args))) 842 | (error "Wrong number of arguments to a procedure")) 843 | (unknown-non-fexpr-apply/t+ 844 | env cont val/t+ (lambda () val) args)] 845 | [#t (error "Uncallable value")])])] 846 | [_ (continuation-expr-continue-eval/t+ env cont val/t+)])) 847 | 848 | ; (Makes fexpr calls.) Assuming the given positive type and its values 849 | ; have no custom fexpr-calling behavior, returns a positive type for 850 | ; the potential values which result from transforming the given one 851 | ; according to the series of steps and the target negative type listed 852 | ; in the given continuation expression. 853 | ; 854 | ; There are many `...-continue-eval/t+` and `...-apply/t+` operations 855 | ; in Fexpress, and this is the one to call when the positive type 856 | ; *and* its values should have their custom fexpr-calling behavior 857 | ; ignored. Fexpress doesn't usually ignore values' fexpr-calling 858 | ; behavior like this, but since this can lead to better performance, 859 | ; it can be explicitly requested by using `(the ...)` to ascribe a 860 | ; type that uses `non-fexpr-value/t+`. 861 | ; 862 | ; Contract: 863 | ; (-> env? continuation-expr? type+? type+?) 864 | ; 865 | (define (non-fexpr-continue-eval/t+ env cont val/t+) 866 | ; TODO CLEANUP: Consider moving this branch to a 867 | ; `continuation-expr-continue-eval-value/t+` method. 868 | (match cont 869 | [(apply/ce args cont) 870 | (unknown-non-fexpr-apply/t+ 871 | env cont val/t+ (lambda () (type+-eval val/t+)) args)] 872 | [_ (continuation-expr-continue-eval/t+ env cont val/t+)])) 873 | 874 | 875 | 876 | ; ===== Fexprs for lambda operations ================================= 877 | 878 | ; A return value of `parse-lambda-args`. 879 | ; 880 | ; Field contracts: 881 | ; natural? (listof var?) any/c 882 | ; 883 | ; The number `n` should be the length of `arg-vars`. 884 | ; 885 | ; The `arg-vars` should be mutually unique. 886 | ; 887 | ; The `body` should be an s-expression representing an Fexpress 888 | ; expression. 889 | ; 890 | (struct parsed-lambda-args (n arg-vars body) #:transparent) 891 | 892 | ; Asserts that the given subforms are in the format expected for a 893 | ; lambda form -- namely, a list of two elements, the first of which is 894 | ; a list of mutually unique variables and the second of which, the 895 | ; body, is any value. (The body is usually an s-expression 896 | ; representing an Fexpress expression.) If the subforms do fit this 897 | ; format, returns a `parsed-lambda-args` struct carrying the number of 898 | ; arguments, the argument variable names, and the body. If they don't, 899 | ; an error attributed to the operation name given by `err-name` will 900 | ; be raised. 901 | ; 902 | ; Contract: 903 | ; (-> symbol? any/c parsed-lambda-args?) 904 | ; 905 | (define (parse-lambda-args err-name args) 906 | (match args 907 | [`(,arg-vars ,body) 908 | (unless (list? arg-vars) 909 | (raise-arguments-error err-name 910 | "expected the argument list to be a list" 911 | "argument list" arg-vars)) 912 | (unless (andmap symbol? arg-vars) 913 | (raise-arguments-error err-name 914 | "expected the argument list to be a list of symbols" 915 | "argument list" arg-vars)) 916 | (define n (length arg-vars)) 917 | (unless (equal? n (hash-count 918 | (for/hash ([arg (in-list arg-vars)]) 919 | (values arg #t)))) 920 | (raise-arguments-error err-name 921 | "expected the argument list to be a list of mutually unique symbols" 922 | "argument list" arg-vars)) 923 | (parsed-lambda-args n arg-vars body)] 924 | [_ 925 | (raise-arguments-error err-name 926 | "expected an argument list and a body" 927 | "subforms" args)])) 928 | 929 | ; An fexpr implementing an interpreted lambda operation. This doesn't 930 | ; attempt to compile the body. The resulting function evaluates the 931 | ; body dynamically every time it's called. 932 | ; 933 | ; When calling this fexpr, the subforms should be parseable according 934 | ; to `parse-lambda-args`. 935 | ; 936 | (define fexpress-ilambda 937 | (makeshift-fexpr 938 | #;apply/t+ 939 | (lambda (env cont op/t+ args) 940 | (match-define (parsed-lambda-args n arg-vars body) 941 | (parse-lambda-args 'ilambda args)) 942 | (type+-continue-eval/t+ env cont 943 | (specific-value/t+ 944 | (lambda arg-values 945 | (define received-n (length arg-values)) 946 | (unless (equal? n received-n) 947 | (raise-arguments-error 'ilambda 948 | "wrong number of arguments at call time" 949 | "number expected" n 950 | "number received" received-n 951 | "arguments expected" arg-vars 952 | "arguments received" arg-values)) 953 | (define body-env 954 | (for/fold ([env env]) 955 | ([arg-var (in-list arg-vars)] 956 | [arg-value (in-list arg-values)]) 957 | (hash-set env arg-var 958 | (at-variable/t+ arg-var 959 | (specific-value/t+ arg-value))))) 960 | (type+-eval 961 | (fexpress-eval/t+ body-env (done/ce (any-value/t_)) 962 | body)))))))) 963 | 964 | ; (A helper for `fexpress-clambda`.) 965 | ; 966 | ; Compiles a lambda form into a `compilation-result?`. The resulting 967 | ; function is likely to be as fast as the equivalent Racket code 968 | ; unless it uses Fexpress features that inhibit compilation, in which 969 | ; case it falls back to interpreting the relevant Fexpress code. 970 | ; 971 | ; Contract: 972 | ; (-> env? continuation-expr? any/c compilation-result?) 973 | ; 974 | ; The `args` should be parseable according to `parse-lambda-args`. If 975 | ; they're not, an error attributed to the operation name "`clambda`" 976 | ; will be raised. 977 | ; 978 | (define (compile-clambda env cont args) 979 | (match-define (parsed-lambda-args n arg-vars body) 980 | (parse-lambda-args 'clambda args)) 981 | (match-define (list arg-type+-list return-val/t_) 982 | (match cont 983 | ; TODO CLEANUP: Consider moving this branch to methods on the 984 | ; `gen:continuation-expr` and `gen:type_` generic interfaces. 985 | [(done/ce (->/t_ arg-type+-list return-val/t_)) 986 | (unless (equal? n (length arg-type+-list)) 987 | (error "Expected the type of a function to have just as many arguments as the argument list of the function")) 988 | (list 989 | (for/list ([arg-var (in-list arg-vars)] 990 | [arg/t+ (in-list arg-type+-list)]) 991 | (at-variable/t+ arg-var arg/t+)) 992 | return-val/t_)] 993 | [_ 994 | (list 995 | (for/list ([arg-var (in-list arg-vars)]) 996 | (at-variable/t+ arg-var (any-value/t+))) 997 | (any-value/t_))])) 998 | (define body-env 999 | (for/fold ([env env]) 1000 | ([arg-var (in-list arg-vars)] 1001 | [arg/t+ (in-list arg-type+-list)]) 1002 | (hash-set env arg-var arg/t+))) 1003 | (match-define 1004 | (compilation-result depends-on-env? body-free-vars body-compiled) 1005 | (type+-compile 1006 | (fexpress-eval/t+ body-env (done/ce return-val/t_) body))) 1007 | (define body-with-env-compiled 1008 | (if depends-on-env? 1009 | `(,#'let 1010 | ([env 1011 | (,#'hash-set* env 1012 | ,@(append* 1013 | (for/list ([arg-var (in-list arg-vars)]) 1014 | `( 1015 | ',arg-var 1016 | (,#'at-variable/t+ (,#'quote ,arg-var) 1017 | (,#'specific-value/t+ 1018 | ,(var-representation-in-racket 1019 | arg-var)))))))]) 1020 | ,body-compiled) 1021 | body-compiled)) 1022 | (define free-vars 1023 | (for/fold ([free-vars body-free-vars]) 1024 | ([arg-var (in-list arg-vars)]) 1025 | (hash-remove free-vars arg-var))) 1026 | (define lambda-compiled 1027 | `(,#'lambda 1028 | ,(for/list ([arg-var (in-list arg-vars)]) 1029 | (var-representation-in-racket arg-var)) 1030 | ,body-with-env-compiled)) 1031 | (compilation-result depends-on-env? free-vars lambda-compiled)) 1032 | 1033 | ; An fexpr implementing a compiled lambda operation. This attempts to 1034 | ; compile the body. The resulting function is likely to be as fast as 1035 | ; the equivalent Racket code unless it uses Fexpress features that 1036 | ; inhibit compilation, in which case it falls back to interpreting the 1037 | ; relevant Fexpress code. 1038 | ; 1039 | ; When calling this fexpr, the subforms should be parseable according 1040 | ; to `parse-lambda-args`. 1041 | ; 1042 | (define fexpress-clambda 1043 | (makeshift-fexpr 1044 | #;apply/t+ 1045 | (lambda (env cont op/t+ args) 1046 | (define compiled-clambda (compile-clambda env cont args)) 1047 | (type+-continue-eval/t+ env cont 1048 | 1049 | ; TODO LAZY: Rather than just using `lazy-value/t+` here, we 1050 | ; could also specialize `type+-continue-eval/t+` to treat a 1051 | ; `clambda` as being guaranteed not to return an fexpr. That 1052 | ; could let us use them in functional position without 1053 | ; inhibiting compilation. 1054 | ; 1055 | (lazy-value/t+ 1056 | (lambda () 1057 | (compilation-result-eval env compiled-clambda)) 1058 | (const compiled-clambda)))))) 1059 | 1060 | 1061 | 1062 | ; ===== An fexpr for type ascription ================================= 1063 | 1064 | ; An fexpr implementing a type ascription operation. The usage is 1065 | ; `(the val/t_ val)`, where `val/t_` is syntactically a negative type 1066 | ; (not just an expression that evaluates to one) and `val` is an 1067 | ; expression the type applies to. The purpose of `the` is mainly to 1068 | ; allow function bodies to use Lisp-1-style function application on 1069 | ; local variables without inhibiting compilation. 1070 | ; 1071 | (define fexpress-the 1072 | (makeshift-fexpr 1073 | #;apply/t+ 1074 | (lambda (env cont op/t+ args) 1075 | (match args 1076 | [`(,expr/t_ ,expr) 1077 | (unless (type_? expr/t_) 1078 | (error "the: expected the type to be a negative type value, syntactically, and not merely an expression that evaluated to one")) 1079 | (type+-continue-eval/t+ env cont 1080 | (fexpress-eval/t+ env (done/ce expr/t_) expr))] 1081 | [_ 1082 | (error "the: expected a literal negative type and an expression")])))) 1083 | 1084 | 1085 | 1086 | ; ===== The Fexpress entrypoint ====================================== 1087 | 1088 | ; Contract: 1089 | ; (-> (hash/c var? any/c) env?) 1090 | (define (env-of-specific-values specific-values) 1091 | (for/fold ([env (hash)]) ([(var val) (in-hash specific-values)]) 1092 | (hash-set env var (at-variable/t+ var (specific-value/t+ val))))) 1093 | 1094 | ; Contract: 1095 | ; (-> env? any/c any/c) 1096 | ; 1097 | ; The `expr` should be an s-expression of Fexpress code. 1098 | ; 1099 | (define (fexpress-eval env expr) 1100 | (type+-eval (fexpress-eval/t+ env (done/ce (any-value/t_)) expr))) 1101 | -------------------------------------------------------------------------------- /fexpress-test/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "fexpress") 4 | 5 | (define deps (list "base" "fexpress-lib" "rackunit-lib")) 6 | -------------------------------------------------------------------------------- /fexpress-test/tests.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | ; fexpress/tests 4 | ; 5 | ; Unit tests. 6 | 7 | ; Copyright 2021 The Fexpress 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 data/queue enqueue! make-queue queue->list)) 23 | (require (only-in racket/match match-define)) 24 | (require (only-in rackunit check-equal?)) 25 | 26 | (require fexpress/proof-of-concept) 27 | 28 | ; (We provide nothing from this module.) 29 | 30 | 31 | (define (logging-for-test-fn body) 32 | (define log (make-queue)) 33 | (define result 34 | (parameterize ([current-fexpress-logger 35 | (lambda (msg) (enqueue! log msg))]) 36 | (body))) 37 | (list (queue->list log) result)) 38 | 39 | (define-syntax-rule (logging-for-test body) 40 | (logging-for-test-fn (lambda () body))) 41 | 42 | (define test-env 43 | (env-of-specific-values 44 | (hash 'the fexpress-the 45 | 'ilambda fexpress-ilambda 46 | 'clambda fexpress-clambda 47 | 'funcall (lambda (f . args) (apply f args)) 48 | '+ + 49 | '* *))) 50 | 51 | 52 | ; NOTE: This example is in the docs. 53 | (check-equal? 54 | (logging-for-test 55 | (fexpress-eval test-env 56 | '(+ 1 2))) 57 | (list '() 3)) 58 | 59 | ; NOTE: This example is in the docs. 60 | (check-equal? 61 | (logging-for-test 62 | (fexpress-eval test-env 63 | '((ilambda (x y) (+ x y 3)) 1 2))) 64 | (list '() 6)) 65 | 66 | ; NOTE: This example is in the docs. 67 | (check-equal? 68 | (logging-for-test 69 | (fexpress-eval test-env 70 | '((clambda (x y) (+ x y 3)) 1 2))) 71 | (list 72 | '(("Evaluating Racket code:" 73 | (lambda (env -+) 74 | (lambda (-x -y) (#%app -+ -x -y (#%datum . 3)))))) 75 | 6)) 76 | 77 | ; NOTE: This example is in the docs. 78 | (check-equal? 79 | (logging-for-test 80 | (fexpress-eval test-env 81 | '(funcall 82 | (clambda (square) 83 | (funcall 84 | (clambda (double) 85 | (funcall double 86 | (funcall double 87 | (+ (funcall square 3) (funcall square 4))))) 88 | (clambda (x) (+ x x)))) 89 | (clambda (x) (* x x))))) 90 | (list 91 | '(("Evaluating Racket code:" 92 | (lambda (env -+ -funcall) 93 | (lambda (-square) 94 | (#%app -funcall 95 | (lambda (-double) 96 | (#%app -funcall -double 97 | (#%app -funcall -double 98 | (#%app -+ 99 | (#%app -funcall -square (#%datum . 3)) 100 | (#%app -funcall -square (#%datum . 4)))))) 101 | (lambda (-x) (#%app -+ -x -x)))))) 102 | ("Evaluating Racket code:" 103 | (lambda (env -*) (lambda (-x) (#%app -* -x -x))))) 104 | 100)) 105 | 106 | (check-equal? 107 | (fexpress-eval test-env 108 | '(((clambda (x y) (clambda (z) (+ x y z))) 1 2) 3)) 109 | 6) 110 | 111 | 112 | ; NOTE: This example is in the docs. 113 | 114 | (match-define (list my-compose-log my-compose) 115 | (logging-for-test 116 | (fexpress-eval test-env 117 | ; This should evaluate to a curried composition function. 118 | `(the ,(->/t_ (list (non-fexpr-value/t+)) 119 | (->/t_ (list (non-fexpr-value/t+)) 120 | (any-value/t_))) 121 | (clambda (f) 122 | (clambda (g) 123 | (clambda (x) 124 | (f (g x))))))))) 125 | 126 | (check-equal? my-compose-log 127 | '(("Evaluating Racket code:" 128 | (lambda (env) 129 | (lambda (-f) 130 | (lambda (-g) (lambda (-x) (#%app -f (#%app -g -x)))))))) 131 | "Using `fexpress-the` allows certain Lisp-1-style code to be efficient") 132 | 133 | (check-equal? 134 | (logging-for-test (((my-compose sqrt) add1) 8)) 135 | (list '() 3) 136 | "Using `fexpress-the` allows certain Lisp-1-style code to not reenter the interpreter") 137 | 138 | 139 | ; NOTE: This example is in the docs. 140 | 141 | (match-define (list my-less-typed-compose-log my-less-typed-compose) 142 | (logging-for-test 143 | (fexpress-eval test-env 144 | `(the ,(->/t_ (list (non-fexpr-value/t+)) 145 | (->/t_ (list (any-value/t+)) 146 | (any-value/t_))) 147 | (clambda (f) 148 | (clambda (g) 149 | (clambda (x) 150 | (f (g x))))))))) 151 | 152 | (check-equal? my-less-typed-compose-log 153 | '(("Evaluating Racket code:" 154 | (lambda (env) 155 | (lambda (-f) 156 | (let ([env 157 | (hash-set* env 'f 158 | (at-variable/t+ 'f (specific-value/t+ -f)))]) 159 | (lambda (-g) 160 | (let ([env 161 | (hash-set* env 'g 162 | (at-variable/t+ 'g (specific-value/t+ -g)))]) 163 | (lambda (-x) 164 | (let ([env 165 | (hash-set* env 'x 166 | (at-variable/t+ 'x 167 | (specific-value/t+ -x)))]) 168 | (#%app -f 169 | (begin 170 | ((current-fexpress-logger) 171 | "Reentering the interpreter") 172 | (type+-eval 173 | (type+-continue-eval/t+ env 174 | (apply/ce '(x) (done/ce (any-value/t_))) 175 | (specific-value/t+ -g)))))))))))))) 176 | "Using `fexpress-the` with a less specific type admits fewer optimizations") 177 | 178 | (check-equal? 179 | (logging-for-test (((my-less-typed-compose sqrt) add1) 8)) 180 | (list '("Reentering the interpreter") 3) 181 | "Using `fexpress-the` with a less specific type causes the code to reenter the interpreter") 182 | 183 | 184 | ; NOTE: This example is in the docs. 185 | 186 | (match-define (list my-untyped-compose-log my-untyped-compose) 187 | (logging-for-test 188 | (fexpress-eval test-env 189 | `(clambda (f) 190 | (clambda (g) 191 | (clambda (x) 192 | (f (g x)))))))) 193 | 194 | (check-equal? my-untyped-compose-log 195 | '(("Evaluating Racket code:" 196 | (lambda (env) 197 | (lambda (-f) 198 | (let ([env 199 | (hash-set* env 'f 200 | (at-variable/t+ 'f (specific-value/t+ -f)))]) 201 | (lambda (-g) 202 | (let ([env 203 | (hash-set* env 'g 204 | (at-variable/t+ 'g (specific-value/t+ -g)))]) 205 | (lambda (-x) 206 | (let ([env 207 | (hash-set* env 'x 208 | (at-variable/t+ 'x 209 | (specific-value/t+ -x)))]) 210 | (begin 211 | ((current-fexpress-logger) 212 | "Reentering the interpreter") 213 | (type+-eval 214 | (type+-continue-eval/t+ env 215 | (apply/ce '((g x)) (done/ce (any-value/t_))) 216 | (specific-value/t+ -f))))))))))))) 217 | "Opting not to use `fexpress-the` admits fewer optimizations") 218 | 219 | (check-equal? 220 | (logging-for-test (((my-untyped-compose sqrt) add1) 8)) 221 | (list '("Reentering the interpreter") 3) 222 | "Opting not to use `fexpress-the` causes the code to reenter the interpreter") 223 | -------------------------------------------------------------------------------- /fexpress-test/tests/and.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | ; fexpress/tests/and 4 | ; 5 | ; Unit tests for an example `my-and` operator that works as a Racket 6 | ; macro, an Fexpress fexpr, and a procedure depending on how it's 7 | ; used. 8 | 9 | ; Copyright 2021 The Fexpress 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 (for-syntax racket/base)) 25 | (require (for-syntax (only-in syntax/parse expr id syntax-parse))) 26 | 27 | (require (only-in data/queue enqueue! make-queue queue->list)) 28 | (require (only-in racket/function identity)) 29 | (require (only-in racket/hash hash-union)) 30 | (require (only-in racket/match match match-lambda**)) 31 | (require (only-in rackunit check-equal?)) 32 | 33 | (require fexpress/proof-of-concept) 34 | 35 | ; (We provide nothing from this module.) 36 | 37 | 38 | (define (logging-for-test-fn body) 39 | (define log (make-queue)) 40 | (define result 41 | (parameterize ([current-fexpress-logger 42 | (lambda (msg) (enqueue! log msg))]) 43 | (body))) 44 | (list (queue->list log) result)) 45 | 46 | (define-syntax-rule (logging-for-test body) 47 | (logging-for-test-fn (lambda () body))) 48 | 49 | 50 | 51 | ; The docs mention that languages with pervasive fexprs allow `and` to 52 | ; be passed to `map` with some success. It's a reasonable idea to 53 | ; disallow this, but it can sometimes look snazzy to allow it. The 54 | ; reason it almost makes sense is because `and`, unlike most other 55 | ; fexprs, actually resembles a procedure; it's basically a procedure 56 | ; with its own custom evaluation strategy. 57 | ; 58 | ; If we want to achieve some of the punning that allows `and` to work 59 | ; with `map` in pervasively fexpr-based languages, we can actually 60 | ; achieve most of it in Racket without using Fexpress at all: 61 | ; 62 | ; (define (my-and-procedure . args) 63 | ; (andmap identity args)) 64 | ; 65 | ; (define-syntax (my-and stx) 66 | ; (syntax-parse stx 67 | ; [_:id #'my-and-procedure] 68 | ; [(_ args:expr ...) #'(and args ...)])) 69 | ; 70 | ; If `my-and` is used in operator position, it works like `and`. If 71 | ; it's used in an expression position, it works like a first-class 72 | ; procedure. Of course, as soon as we pass it anywhere as a procedure, 73 | ; that procedure carries no memory of what the custom evaluation 74 | ; strategy was. 75 | ; 76 | ; With Fexpress, we can do a little more punning than that, allowing 77 | ; `my-and` to work like a Racket macro, an Fexpress fexpr, or a 78 | ; procedure. The first-class value carries the fexpr and procedure 79 | ; behaviors at the same time, and the fexpr behavior means it still 80 | ; has some memory of its custom evaluation strategy, even if there's 81 | ; no way to turn it back from a run-time first-class value into a 82 | ; compile-time Racket syntax binding. 83 | ; 84 | ; Calling `my-and` using (my-and ...) from Racket code will always 85 | ; call it as either a Racket macro or a procedure. It's only within 86 | ; Fexpress code that the fexpr behavior can be invoked. 87 | 88 | (define (my-and-procedure . args) 89 | (andmap identity args)) 90 | 91 | (struct my-and-fexpr-representation () 92 | ; If we're invoking `my-and` as a procedure, we use a dedicated code 93 | ; path for that. 94 | #:property prop:procedure 95 | (lambda (self . args) 96 | (apply my-and-procedure args)) 97 | #:methods gen:fexpr 98 | [(define (fexpr-apply/t+ env cont op/t+ op args) 99 | (define arg-type+-list 100 | (for/list ([arg (in-list args)]) 101 | (fexpress-eval/t+ env (done/ce (any-value/t_)) arg))) 102 | (lazy-value/t+ 103 | (lambda () 104 | ; If we're interpreting an fexpr call to `my-and`, we 105 | ; interpret the arguments, short-circuiting as soon as any of 106 | ; them is true. 107 | (for/and ([arg/t+ (in-list arg-type+-list)]) 108 | (type+-eval arg/t+))) 109 | (lambda () 110 | ; If we're compiling an fexpr call to `my-and`, we compile 111 | ; the arguments, and we put them into a Racket `and` form. 112 | (define arg-compilation-results 113 | (for/list ([arg/t+ (in-list arg-type+-list)]) 114 | (type+-compile arg/t+))) 115 | (let next ([depends-on-env? #f] 116 | [free-vars (hash)] 117 | [rev-args-compiled (list)] 118 | [arg-compilation-results 119 | arg-compilation-results]) 120 | (match arg-compilation-results 121 | [(list) 122 | (compilation-result depends-on-env? free-vars 123 | `(,#'and ,@(reverse rev-args-compiled)))] 124 | [(cons 125 | (compilation-result 126 | arg-depends-on-env? arg-free-vars arg-compiled) 127 | arg-compilation-results) 128 | (next (or depends-on-env? arg-depends-on-env?) 129 | (hash-union free-vars arg-free-vars 130 | #:combine (match-lambda** 131 | [(#t #t) #t])) 132 | (cons arg-compiled rev-args-compiled) 133 | arg-compilation-results)])))))]) 134 | 135 | (define my-and-fexpr (my-and-fexpr-representation)) 136 | 137 | (define-syntax (my-and stx) 138 | (syntax-parse stx 139 | [_:id #'my-and-fexpr] 140 | ; If we're invoking `my-and` as a Racket macro, we use a dedicated 141 | ; code path for that. 142 | [(_ args:expr ...) #'(and args ...)])) 143 | 144 | 145 | (define test-env 146 | (env-of-specific-values 147 | (hash 'clambda fexpress-clambda 148 | 'and my-and 149 | 'get-false (lambda () #f) 150 | 'err (lambda () (error "Shouldn't have called this"))))) 151 | 152 | 153 | (check-equal? 154 | (let ([mutable-variable 1]) 155 | (define and-result (my-and #f (set! mutable-variable 2))) 156 | (list mutable-variable and-result)) 157 | (list 1 #f) 158 | "Using `my-and` as a Racket macro works and does short-circuiting.") 159 | 160 | (check-equal? 161 | (let ([mutable-variable 1]) 162 | (define and-result 163 | ((car (list my-and)) #f (set! mutable-variable 2))) 164 | (list mutable-variable and-result)) 165 | (list 2 #f) 166 | "Using `my-and` as a first-class function works, but it doesn't do short-circuiting.") 167 | 168 | (check-equal? 169 | (let ([mutable-variable 1]) 170 | (define and-result 171 | (map my-and (list #t #f) (list #f (set! mutable-variable 2)))) 172 | (list mutable-variable and-result)) 173 | (list 2 (list #f #f)) 174 | "Passing `my-and` to something that calls it like a function works. The arguments of that higher-order function are, however, evaluated as usual.") 175 | 176 | (check-equal? 177 | (logging-for-test 178 | (fexpress-eval test-env 179 | '(and (get-false) (err)))) 180 | (list '() #f) 181 | "Interpreting an Fexpress fexpr call to `my-and` works and does short-circuiting.") 182 | 183 | (check-equal? 184 | (logging-for-test 185 | (fexpress-eval test-env 186 | '((clambda (x) (and x (err))) (get-false)))) 187 | (list 188 | '(("Evaluating Racket code:" 189 | (lambda (env -err) (lambda (-x) (and -x (#%app -err)))))) 190 | #f) 191 | "Compiling an Fexpress fexpr call to `my-and` works and does short-circuiting.") 192 | -------------------------------------------------------------------------------- /fexpress/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection 'multi) 4 | 5 | (define implies (list "fexpress-doc" "fexpress-lib")) 6 | (define deps implies) 7 | -------------------------------------------------------------------------------- /notes/20210813-prior-art-and-musings.md: -------------------------------------------------------------------------------- 1 | # Prior art and musings 2 | 3 | There have been approaches that make fexprs tamer. 4 | 5 | * Kernel is a language that allows fexprs to have lexical scope, unlike the traditional dynamically scoped Lisp dialects fexprs originated in. It does this by explicitly passing them the caller's lexical environment so they can evaluate their arguments in that scope. 6 | 7 | * A topic that often comes up with fexprs is partial evaluation. I'm not aware of specific projects that succeed at this. 8 | 9 | I've occasionally tried to tackle this. As early as November 14, 2009, in [my 7th Arc Forum comment ever](http://arclanguage.org/item?id=10739) -- nostalgic -- I was proposing combining fexprs with static types to be able to optimize them. On November 13, 2011, I started up this repo with an attempt to write a partial evaluator. 10 | -------------------------------------------------------------------------------- /notes/code-sketches/fexpress.js: -------------------------------------------------------------------------------- 1 | // fexpress.js 2 | 3 | // Copyright (c) 2011 Ross Angle 4 | // 5 | // Permission is hereby granted, free of charge, to any person 6 | // obtaining a copy of this software and associated documentation 7 | // files (the "Software"), to deal in the Software without 8 | // restriction, including without limitation the rights to use, 9 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the 11 | // Software is furnished to do so, subject to the following 12 | // conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | // OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | // Permission to use this software is also granted under the 27 | // Perl Foundation's Artistic License 2.0. You may use either license, 28 | // at your option. 29 | 30 | 31 | // This depends on Lathe.js (https://github.com/rocketnia/lathe), 32 | // which must be on the global variable "_". 33 | 34 | // Fexpress is intended to be a programming language with partial 35 | // evaluation and predictable performance. To this end, it should 36 | // evaluate things extremely eagerly, even evaluating the bodies of 37 | // function abstractions before they're called. In order to make this 38 | // simple, nothing in Fexpress has side effects. 39 | // 40 | // Actually, right now fexpress's function abstraction just wraps its 41 | // body expression in a data structure, making for a more traditional 42 | // evaluation order. But the implementation is set up in a way that 43 | // should make it straightforward to implement partial evaluation, and 44 | // the language axioms are designed in a way that should make that 45 | // partial evaluation pay off. Easy for me to say now, right? 46 | // 47 | // Fexpress uses fexprs that act as functions of the form 48 | // "( unparsed argument list, caller's environment ) -> return value", 49 | // like Kernel. Unlike Kernel, these are pure functions. 50 | // 51 | // Fexprs are a very simple way to breathe extensible meaning into a 52 | // nearly meaningless but structured program representation. Either 53 | // the program is a symbol whose meaning is determined by dictionary 54 | // lookup, or it's a nonempty list of programs whose overall meaning 55 | // is determined by the first program. In languages with macros, 56 | // macroexpansion tends to work this way anyway, making those 57 | // languages at least as complicated (albeit only if that phase is 58 | // considered part of the language). In some sense, in an fexpr 59 | // language, macroexpansion is all there is. 60 | 61 | 62 | /* 63 | 64 | Things with -- in front of them have been considered but aren't part of the implementation. 65 | 66 | Eval[ Wrap[ Cons, rep ], env ] ==> Call[ Eval[ Get[ Car, rep ], env ], Get[ Cdr, rep ], env ] 67 | Call[ Wrap[ Op, rep ], args, env ] ==> 68 | Eval[ Get[ Impl, rep ], Shadow[ Get[ BodyArg, rep ], args, Shadow[ Get[ EnvArg, rep ], env, Get[ LexicalEnv, rep ] ] ] ] 69 | 70 | -- Unwrap[ type1, Wrap[ type2, rep ] ] where type1 is type2 ==> rep 71 | Get[ key1, Shadow[ key2, val, rest ] ] where key1 is key2 ==> val 72 | Get[ key1, Shadow[ key2, val, rest ] ] where key1 isn't key2 ==> Get[ key1, rest ] 73 | -- Get[ key1, Shadow[ key2, val, rest ] ] ==> IfEq[ key1, key2, val, Get[ key1, rest ] ] 74 | -- IfEq[ a, b, then, else ] where a is b ==> then 75 | -- IfEq[ a, b, then, else ] where a is not b ==> else 76 | 77 | Eval[ expr@Wrap[ String, rep ], env ] ==> Get[ expr, env ] 78 | 79 | Shadow[ key1, val1, Shadow[ key2, val2, rest ] ] where key2 is less than key1 ==> 80 | Shadow[ key2, val2, Shadow[ key1, val1, rest ] ] 81 | Shadow[ key1, val1, Shadow[ key2, val2, rest ] ] where key1 is key2 ==> Shadow[ key1, val1, rest ] 82 | 83 | Call[ Vau, '(bodyarg envarg impl), env ] ==> Wrap[ Op, { LexicalEnv: env, BodyArg: 'bodyarg, EnvArg: 'envarg, Impl: 'impl } ] 84 | 85 | */ 86 | 87 | 88 | function KnownString( string ) { 89 | this.string_ = "" + string; 90 | } 91 | 92 | KnownString.prototype.toString = function () { 93 | return JSON.stringify( this.string_ ); 94 | }; 95 | 96 | function KnownWrap( type, rep ) { 97 | this.type_ = type; 98 | this.rep_ = rep; 99 | } 100 | 101 | KnownWrap.prototype.toString = function () { 102 | return "" + this.type_ + "::" + this.rep_; 103 | }; 104 | 105 | function KnownMap( sortedEntries ) { 106 | this.sortedEntries_ = sortedEntries; 107 | } 108 | 109 | KnownMap.prototype.toString = function () { 110 | return this.sortedEntries_.length === 0 ? "{}" : 111 | "{ " + _.arrMap( this.sortedEntries_, function ( it ) { 112 | return "" + it[ 0 ] + ": " + it[ 1 ]; 113 | } ).join( ", " ) + " }"; 114 | }; 115 | 116 | // TODO: Actually choose a particular order. This is just set up to 117 | // ensure *some* order, not necessarily the nicest one. It does ensure 118 | // that strings are in alphabetical order, so that our uses of 119 | // wrapmapExpr already put their Shadow[ ... ] nesting in normal form. 120 | function compareKnown( a, b ) { 121 | var as = [ a ], bs = [ b ]; 122 | while ( as.length !== 0 ) { 123 | a = as.pop(), b = bs.pop(); 124 | if ( a instanceof KnownString ) { 125 | if ( b instanceof KnownString ) { 126 | var astr = a.string_, bstr = b.string_; 127 | if ( astr !== bstr ) 128 | return astr < bstr ? -1 : 1; 129 | } else { 130 | return -1; 131 | } 132 | } else if ( b instanceof KnownString ) { 133 | return 1; 134 | } else if ( a instanceof KnownWrap ) { 135 | if ( b instanceof KnownWrap ) { 136 | as.push( a.type_, a.rep_ ); 137 | bs.push( b.type_, b.rep_ ); 138 | } else { 139 | return -1; 140 | } 141 | } else if ( b instanceof KnownWrap ) { 142 | return 1; 143 | } else { 144 | var aents = a.sortedEntries_, bents = b.sortedEntries_; 145 | var an = aents.length, bn = bents.length; 146 | if ( an !== bn ) 147 | return an < bn ? -1 : 1; 148 | _.arrEach( aents, function ( it ) { 149 | as.push( it[ 0 ], it[ 1 ] ); 150 | } ); 151 | _.arrEach( bents, function ( it ) { 152 | bs.push( it[ 0 ], it[ 1 ] ); 153 | } ); 154 | } 155 | } 156 | return 0; 157 | } 158 | 159 | function PatternVar( name ) { this.name_ = name; } 160 | function v( name ) { return new PatternVar( name ); } 161 | function mat( expr, pattern ) { 162 | var results = {}; 163 | function addResults( expr, pattern ) { 164 | if ( pattern instanceof PatternVar ) { 165 | results[ pattern.name_ ] = expr; 166 | } else if ( _.isString( pattern ) ) { 167 | if ( !(_.isString( expr ) && "" + expr === "" + pattern) ) 168 | return void (results = null); 169 | } else if ( _.likeArray( pattern ) ) { 170 | var n = pattern.length; 171 | if ( !(_.likeArray( expr ) && expr.length === n) ) 172 | return void (results = null); 173 | for ( var i = 0; i < n && results; i++ ) 174 | addResults( expr[ i ], pattern[ i ] ); 175 | } else { 176 | throw new Error(); 177 | } 178 | } 179 | addResults( expr, pattern ); 180 | return results && function ( name ) { return results[ name ]; }; 181 | } 182 | 183 | function known( expr ) { 184 | var done = false, hasResult = false, result = null; 185 | var endResult; 186 | function ret( result ) { 187 | done = true; 188 | endResult = result; 189 | } 190 | while ( !done ) { 191 | var m; 192 | if ( _.isString( expr ) ) { 193 | hasResult = true, result = new KnownString( expr ); 194 | } else if ( m = mat( expr, [ "Wrap", v( "type" ), v( "rep" ) ] ) ) 195 | (function () { // local scope 196 | var type = m( "type" ), rep = m( "rep" ); 197 | var oldRet = ret; 198 | expr = type; ret = function ( knownType ) { 199 | if ( knownType === null ) 200 | return void (hasResult = true, result = null, ret = oldRet); 201 | expr = rep; ret = function ( knownRep ) { 202 | if ( knownRep === null ) 203 | return void (hasResult = true, result = null, ret = oldRet); 204 | hasResult = true, result = new KnownWrap( knownType, knownRep ); 205 | ret = oldRet; }; }; 206 | })(); 207 | else if ( mat( expr, [ "EmptyMap" ] ) ) { 208 | hasResult = true, result = new KnownMap( [] ); 209 | } else if ( m = mat( expr, [ "Shadow", v( "key" ), v( "val" ), v( "rest" ) ] ) ) 210 | (function () { // local scope 211 | var key = m( "key" ), val = m( "val" ), rest = m( "rest" ); 212 | var oldRet = ret; 213 | expr = key; ret = function ( knownKey ) { 214 | if ( knownKey === null ) 215 | return void (hasResult = true, result = null, ret = oldRet); 216 | expr = val; ret = function ( knownVal ) { 217 | if ( knownVal === null ) 218 | return void (hasResult = true, result = null, ret = oldRet); 219 | // TODO: Stop requiring the rest to be fully known. If a 220 | // key with a known value shadows a key with an unknown 221 | // value, the result should be known. 222 | expr = rest; ret = function ( knownRest ) { 223 | if ( knownRest instanceof KnownMap ) { 224 | var entries = []; 225 | // TODO: Represent sortedEntries_ as a heap, or at 226 | // least do a binary search here. 227 | var hasOurs = false; 228 | _.arrEach( knownRest.sortedEntries_, function ( entry ) { 229 | if ( hasOurs ) 230 | return void entries.push( entry ); 231 | var compared = compareKnown( knownKey, entry[ 0 ] ); 232 | if ( compared <= 0 ) { 233 | entries.push( [ knownKey, knownVal ] ); 234 | hasOurs = true; 235 | } 236 | if ( compared !== 0 ) 237 | entries.push( entry ); 238 | } ); 239 | if ( !hasOurs ) 240 | entries.push( [ knownKey, knownVal ] ); 241 | hasResult = true, result = new KnownMap( entries ); 242 | ret = oldRet; 243 | } else { 244 | hasResult = true, result = null, ret = oldRet; 245 | } 246 | }; }; }; 247 | })(); 248 | else { 249 | hasResult = true, result = null; 250 | } 251 | while ( hasResult ) { 252 | var resultWas = result; 253 | hasResult = false, result = null; 254 | ret( resultWas ); 255 | } 256 | } 257 | return endResult; 258 | } 259 | 260 | function mapExprArray( args ) { 261 | var result = [ "EmptyMap" ]; 262 | for ( var i = args.length - 1; 0 <= i; i -= 2 ) 263 | result = [ "Shadow", args[ i - 1 ], args[ i ], result ]; 264 | return result; 265 | } 266 | 267 | function mapExpr( var_args ) { 268 | return mapExprArray( arguments ); 269 | } 270 | 271 | function wrapmapExpr( type, var_args ) { 272 | return [ "Wrap", type, mapExprArray( _.arrCut( arguments, 1 ) ) ]; 273 | } 274 | 275 | function listExprArray( args ) { 276 | var result = wrapmapExpr( "Nil" ); 277 | for ( var i = args.length - 1; 0 <= i; i-- ) { 278 | // NOTE: The order of fields matters here, so that the 279 | // Shadow[ ... ] nesting is in normal form. 280 | result = 281 | wrapmapExpr( "Cons", "Car", args[ i ], "Cdr", result ); 282 | } 283 | return result; 284 | } 285 | 286 | function listExpr( var_args ) { 287 | return listExprArray( arguments ); 288 | } 289 | 290 | function specializeOneLevel( expr ) { 291 | var done = false; 292 | function step( result ) { 293 | done = true; 294 | } 295 | while ( !done ) { 296 | while ( true ) { 297 | var m, key1, key2; 298 | if ( m = mat( expr, [ "Eval", [ "Wrap", "Cons", v( "rep" ) ], v( "env" ) ] ) ) 299 | (function () { // local scope 300 | // Eval[ Wrap[ Cons, rep ], env ] ==> Call[ Eval[ Get[ Car, rep ], env ], Get[ Cdr, rep ], env ] 301 | var rep = m( "rep" ), env = m( "env" ); 302 | var oldStep = step; 303 | expr = [ "Get", "Car", rep ]; step = function ( opExpr ) { 304 | expr = [ "Eval", opExpr, env ]; step = function ( op ) { 305 | expr = [ "Get", "Cdr", rep ]; step = function ( args ) { 306 | expr = [ "Call", op, args, env ]; 307 | step = oldStep; }; }; }; 308 | })(); 309 | else if ( m = mat( expr, [ "Call", [ "Wrap", "Op", v( "rep" ) ], v( "args" ), v( "env" ) ] ) ) 310 | (function () { // local scope 311 | // Call[ Wrap[ Op, rep ], args, env ] ==> 312 | // Eval[ Get[ Impl, rep ], Shadow[ Get[ BodyArg, rep ], args, Shadow[ Get[ EnvArg, rep ], env, Get[ LexicalEnv, rep ] ] ] ] 313 | var rep = m( "rep" ), args = m( "args" ), env = m( "env" ); 314 | var oldStep = step; 315 | expr = [ "Get", "Impl", rep ]; step = function ( impl ) { 316 | expr = [ "Get", "BodyArg", rep ]; step = function ( bodyArg ) { 317 | expr = [ "Get", "EnvArg", rep ]; step = function ( envArg ) { 318 | expr = [ "Get", "LexicalEnv", rep ]; step = function ( lexicalEnv ) { 319 | expr = [ "Shadow", envArg, env, lexicalEnv ]; step = function ( halfLocalEnv ) { 320 | expr = [ "Shadow", bodyArg, args, halfLocalEnv ]; step = function ( localEnv ) { 321 | expr = [ "Eval", impl, localEnv ]; 322 | step = oldStep; }; }; }; }; }; }; 323 | })(); 324 | // TODO: Oops, this shouldn't need key1 and key2 to be completely known, just known enough. 325 | else if ( (m = mat( expr, [ "Get", v( "key1" ), [ "Shadow", v( "key2" ), v( "val" ), v( "rest" ) ] ] )) 326 | && (key1 = known( m( "key1" ) )) !== null 327 | && (key2 = known( m( "key2" ) )) !== null ) { 328 | // Get[ key1, Shadow[ key2, val, rest ] ] where key1 is key2 ==> val 329 | // Get[ key1, Shadow[ key2, val, rest ] ] where key1 isn't key2 ==> Get[ key1, rest ] 330 | var compared = compareKnown( key1, key2 ); 331 | expr = compared === 0 ? m( "val" ) : [ "Get", m( "key1" ), m( "rest" ) ]; 332 | } else if ( (m = mat( expr, [ "Eval", v( "expr" ), v( "env" ) ] )) 333 | && mat( m( "expr" ), [ "Wrap", "String", v( "rep" ) ] ) ) { 334 | // Eval[ expr@Wrap[ String, rep ], env ] ==> Get[ expr, env ] 335 | expr = [ "Get", m( "expr" ), m( "env" ) ]; 336 | // TODO: Oops, this shouldn't need key1 and key2 to be completely known, just known enough. 337 | } else if ( (m = mat( expr, [ "Shadow", v( "key1" ), v( "val1" ), [ "Shadow", v( "key2" ), v( "val2" ), v( "rest" ) ] ] )) 338 | && (key1 = known( m( "key1" ) )) !== null 339 | && (key2 = known( m( "key2" ) )) !== null 340 | && compareKnown( key2, key1 ) < 0 ) 341 | (function () { // local scope 342 | // Shadow[ key1, val1, Shadow[ key2, val2, rest ] ] where key2 is less than key1 ==> 343 | // Shadow[ key2, val2, Shadow[ key1, val1, rest ] ] 344 | var key2 = m( "key2" ), val2 = m( "val2" ); 345 | var oldStep = step; 346 | expr = [ "Shadow", m( "key1" ), m( "val1" ), m( "rest" ) ]; step = function ( halfRest ) { 347 | expr = [ "Shadow", key2, val2, halfRest ]; 348 | step = oldStep; }; 349 | })(); 350 | // TODO: Oops, this shouldn't need key1 and key2 to be completely known, just known enough. 351 | else if ( (m = mat( expr, [ "Shadow", v( "key1" ), v( "val1" ), [ "Shadow", v( "key2" ), v( "val2" ), v( "rest" ) ] ] )) 352 | && (key1 = known( m( "key1" ) )) !== null 353 | && (key2 = known( m( "key2" ) )) !== null 354 | && compareKnown( key1, key2 ) === 0 ) { 355 | // Shadow[ key1, val1, Shadow[ key2, val2, rest ] ] where key1 is key2 ==> Shadow[ key1, val1, rest ] 356 | expr = [ "Shadow", m( "key1" ), m( "val1" ), m( "rest" ) ]; 357 | // TODO: Instead of capturing the lexically enclosing environment, this should partially evaluate its body based on that environment, with unknowns in place of the bodyarg and envarg. 358 | } else if ( m = mat( expr, [ "Call", "Vau", listExpr( v( "bodyarg" ), v( "envarg" ), v( "impl" ) ), v( "env" ) ] ) ) { 359 | // Call[ Vau, '(bodyarg envarg impl), env ] ==> Wrap[ Op, { LexicalEnv: env, BodyArg: 'bodyarg, EnvArg: 'envarg, Impl: 'impl } ] 360 | // NOTE: The order of fields matters here, so that the Shadow[ ... ] nesting is in normal form. 361 | expr = wrapmapExpr( "Op", "BodyArg", m( "bodyarg" ), "EnvArg", m( "envarg" ), "Impl", m( "impl" ), "LexicalEnv", m( "env" ) ); 362 | } else { 363 | break; 364 | } 365 | } 366 | step( expr ); 367 | } 368 | return expr; 369 | } 370 | 371 | // Test cases (all passing!): 372 | // 373 | // mat( specializeOneLevel( [ "Shadow", [ "Wrap", "String", "Woo" ], "Hoo", [ "Shadow", [ "Wrap", "String", "Coo" ], "Hoo", [ "Shadow", [ "Wrap", "String", "Boo" ], "Hoo", [ "EmptyMap" ] ] ] ] ), 374 | // [ "Shadow", [ "Wrap", "String", "Boo" ], "Hoo", [ "Shadow", [ "Wrap", "String", "Coo" ], "Hoo", [ "Shadow", [ "Wrap", "String", "Woo" ], "Hoo", [ "EmptyMap" ] ] ] ] ) 375 | // 376 | // mat( specializeOneLevel( [ "Shadow", [ "Wrap", "String", "Woo" ], "Hoo", [ "Shadow", [ "Wrap", "String", "Woo" ], "Boo", [ "EmptyMap" ] ] ] ), 377 | // [ "Shadow", [ "Wrap", "String", "Woo" ], "Hoo", [ "EmptyMap" ] ] ) 378 | // 379 | // mat( specializeOneLevel( [ "Eval", [ "Wrap", "String", "Woo" ], [ "Shadow", [ "Wrap", "String", "Woo" ], "Hoo", [ "EmptyMap" ] ] ] ), 380 | // "Hoo" ) 381 | // 382 | // mat( specializeOneLevel( [ "Eval", listExpr( [ "Wrap", "String", "Woo" ], "X", "Y", "Z" ), [ "Shadow", [ "Wrap", "String", "Woo" ], "Vau", [ "EmptyMap" ] ] ] ), 383 | // [ "Wrap", "Op", [ "Shadow", "BodyArg", "X", [ "Shadow", "EnvArg", "Y", [ "Shadow", "Impl", "Z", [ "Shadow", "LexicalEnv", [ "Shadow", [ "Wrap", "String", "Woo" ], "Vau", [ "EmptyMap" ] ], [ "EmptyMap" ] ] ] ] ] ] ) 384 | 385 | --------------------------------------------------------------------------------