commit dfa5f226277fc9cafc4418c27455693cbc59380c Author: Georges Dupéron Date: Thu Apr 27 23:41:41 2017 +0200 Squashed commits diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a59348 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*~ +\#* +.\#* +.DS_Store +compiled/ +/doc/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3ba5fc5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: c +sudo: false + +env: + global: + # RACKET_DIR is an argument to install-racket.sh + - RACKET_DIR=~/racket + - PATH="$RACKET_DIR/bin:$PATH" + matrix: + # RACKET_VERSION is an argument to install-racket.sh + - RACKET_VERSION=6.5 + - RACKET_VERSION=6.6 + - RACKET_VERSION=6.7 + - RACKET_VERSION=6.8 + - RACKET_VERSION=RELEASE + - RACKET_VERSION=HEAD + +before_install: +- curl -L https://raw.githubusercontent.com/greghendershott/travis-racket/master/install-racket.sh | bash +- raco pkg install --deps search-auto doc-coverage cover cover-codecov # or cover-coveralls + +install: +- raco pkg install --deps search-auto -j 2 + +script: +- raco test -x -p "$(basename "$TRAVIS_BUILD_DIR")" +- raco setup --check-pkg-deps --no-zo --no-launcher --no-install --no-post-install --no-docs --pkgs "$(basename "$TRAVIS_BUILD_DIR")" +- raco doc-coverage "$(basename "$TRAVIS_BUILD_DIR")" +- raco cover -s main -s test -s doc -f codecov -f html -d ~/coverage . || true +# TODO: add an option to cover to run the "outer" module too, not just the submodules. +# TODO: deploy the coverage info. \ No newline at end of file diff --git a/LICENSE-more.md b/LICENSE-more.md new file mode 100644 index 0000000..3f3e28e --- /dev/null +++ b/LICENSE-more.md @@ -0,0 +1,28 @@ +remember + +Parts of this this software were initially written as part of a project +at Cortus, S.A.S. which can be reached at 97 Rue de Freyr, 34000 +Montpellier, France. I got their permission to redistribute the code in +the Public Domain. + + + +This package is in distributed under the Creative Commons CC0 license +https://creativecommons.org/publicdomain/zero/1.0/, as specified by +the LICENSE.txt file. + + + +The CC0 license is equivalent to a dedication to the Public Domain +in most countries, but is also effective in countries which do not +recognize explicit dedications to the Public Domain. + + + +In order to avoid any potential licensing issues, this package is explicitly +distributed under the Creative Commons CC0 license +https://creativecommons.org/publicdomain/zero/1.0/, or under the GNU Lesser +General Public License (LGPL) https://opensource.org/licenses/LGPL-3.0, or +under the Apache License Version 2.0 +https://opensource.org/licenses/Apache-2.0, or under the MIT license +https://opensource.org/licenses/MIT, at your option. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..670154e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c7e56c --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +[![Build Status,](https://img.shields.io/travis/jsmaniac/remember/master.svg)](https://travis-ci.org/jsmaniac/remember) +[![Coverage Status,](https://img.shields.io/codecov/c/github/jsmaniac//master.svg)](https://codecov.io/gh/jsmaniac/remember) +[![Build Stats,](https://img.shields.io/badge/build-stats-blue.svg)](http://jsmaniac.github.io/travis-stats/#jsmaniac/remember) +[![Online Documentation,](https://img.shields.io/badge/docs-online-blue.svg)](http://docs.racket-lang.org/remember/) +[![Maintained as of 2017,](https://img.shields.io/maintenance/yes/2017.svg)](https://github.com/jsmaniac/remember/issues) +[![License: CC0 v1.0.](https://img.shields.io/badge/license-CC0-blue.svg)](https://creativecommons.org/publicdomain/zero/1.0/) + +remember +======== + +This Racket library provides a compile-time memoize feature. It allows +remembering a value with `(remember-write! 'category 'value)`. In subsequent +compilations, `(get-remembered 'category)` will return a set of all +previously-remembered values. + +Installation +============ + +raco pkg install remember + +Example use case: the `phc-adt` library +======================================= + +This library is used to implement "interned" structure and constructor types +in the [`phc-adt`](https://github.com/jsmaniac/phc-adt) library. The `phc-adt` +library needs to know the set of all structure and constructor types used in +the program, and uses `remember` to automatically memoize structure +descriptors and constructor names. + +When the `structure` macro defined in +[`structure.hl.rkt`](https://github.com/jsmaniac/phc-adt/blob/refactor/structure.hl.rkt) +encounters an unknown list of field names, it uses the `remember` library to +append the tuple of field names to a user-specified file. That file is loaded +in subsequent compilations, so that the tuple of fields is known to `phc-adt`. + +The memoized descriptors are used to know all possible structs that can +contain a field with the desired name when accessing it with `(get instance +field-name)`. The `get` macro can then retrieve the field's value using the +right accessor (for example `(struct123-fieldname instance)`). Knowing all +existing structures allows `get` to perform some kind of dynamic dispatch to +obtain the appropriate accessor, for example using a `cond` which tests for +all possible types. + +The `constructor` macro defined in +[`constructor.hl.rkt`](https://github.com/jsmaniac/phc-adt/blob/refactor/constructor.hl.rkt) +works in the same way, but remembers the name of the constructor's tag instead +of field names. The memoization feature is used so that all uses of a +constructor with a given name are equivalent, across all files. + diff --git a/info.rkt b/info.rkt new file mode 100644 index 0000000..f42f487 --- /dev/null +++ b/info.rkt @@ -0,0 +1,22 @@ +#lang info +(define collection "remember") +(define deps '("base" + "rackunit-lib" + "compatibility-lib" + "scribble-lib" + "typed-racket-lib" + "phc-toolkit" + "hyper-literate")) +(define build-deps '("scribble-lib" + "racket-doc" + "typed-racket-doc" + "scribble-enhanced")) +(define scribblings '(("scribblings/remember.scrbl" ()) + ("remember-implementation.hl.rkt" () (omit-start)))) +(define compile-omit-paths '("test/test-error.rkt")) +(define test-omit-paths '("test/test-error.rkt")) +(define pkg-desc (string-append "Compile-time memoize across compilations." + " Writes values to a file, so that they will" + " be remembered during the next compilation.")) +(define version "0.9") +(define pkg-authors '(|Georges Dupéron|)) diff --git a/licenses/bsd.txt b/licenses/bsd.txt new file mode 100644 index 0000000..21a8f5c --- /dev/null +++ b/licenses/bsd.txt @@ -0,0 +1,19 @@ +Copyright (c) 2000-2015 Dipanwita Sarkar, Andrew W. Keep, R. Kent Dybvig, Oscar Waddell + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/lgpl-3.0--license.txt b/licenses/lgpl-3.0--license.txt new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/licenses/lgpl-3.0--license.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/main.rkt b/main.rkt new file mode 100644 index 0000000..9ccddaa --- /dev/null +++ b/main.rkt @@ -0,0 +1,3 @@ +#lang racket +(require "remember-implementation.hl.rkt") +(provide (all-from-out "remember-implementation.hl.rkt")) \ No newline at end of file diff --git a/remember-implementation.hl.rkt b/remember-implementation.hl.rkt new file mode 100644 index 0000000..ff323b2 --- /dev/null +++ b/remember-implementation.hl.rkt @@ -0,0 +1,303 @@ +#lang hyper-literate racket/base +@(require scribble-enhanced/doc) +@doc-lib-setup + +@title[#:style manual-doc-style + #:tag "remember" + #:tag-prefix "(lib remember/remember-implementation.hl.rkt)" + ]{Implementation of Remember} + +@(chunks-toc-prefix + '("(lib remember/remember-implementation.hl.rkt)")) + +@(table-of-contents) + +@section{@racket[remember]} + +This module allows macros to remember some values across +compilations. Values are stored within the +@tc[remembered-values] hash table, which associates a +@racket[_category] (a symbol) with a set of values. + +@chunk[ + (begin-for-syntax + (define remembered-values (make-hash)))] + +A second set tracks values which were recently written, but +not initially added via @racket[remembered!] or +@racket[remembered-add!]. + +@chunk[ + (begin-for-syntax + (define written-values (make-hash)))] + +The user can specify input files from which remembered +values are loaded, and optionally an output file to which +new, not-yet-remembered values will be appended: + +@CHUNK[ + (define-for-syntax remember-output-file-parameter + (make-parameter #f (or? path-string? false?))) + + (define-syntax (remember-output-file stx) + (syntax-case stx () + [(_ new-value) + (string? (syntax-e #'new-value)) + (begin (remember-output-file-parameter (syntax-e #'new-value)) + #'(void))] + [(_) + (quasisyntax/loc stx remember-output-file-parameter)])) + + (define-syntax (remember-input-file stx) + (syntax-case stx () + [(_ name) + (string? (syntax-e #'name)) + #'(require (only-in name))])) + + (define-syntax-rule (remember-io-file name) + (begin (remember-input-file name) + (remember-output-file name)))] + +@CHUNK[ + (define-syntax-rule (remembered! category value) + (begin-for-syntax + (remembered-add! 'category 'value))) + + (define-for-syntax writable? + (disjoin number? + string? + symbol? + char? + null? + (λ (v) (and (pair? v) + (writable? (car v)) + (writable? (cdr v)))) + (λ (v) (and (vector? v) + (andmap writable? (vector->list v)))))) + + (define-for-syntax (remembered-add! category value) + (unless (writable? value) + (error "Value to remember does not seem to be safely writable:" + value)) + (unless (symbol? category) + (error (format "The category was not a symbol, when remembering ~a:" + value) + category)) + (hash-update! remembered-values + category + (λ (s) (set-add s value)) + set)) + + (define-for-syntax (remembered-add-written! category value) + (unless (writable? value) + (error "Value to remember does not seem to be safely writable:" + value)) + (unless (symbol? category) + (error (format "The category was not a symbol, when remembering ~a:" + value) + category)) + (hash-update! written-values + category + (λ (s) (set-add s value)) + set)) + + (define-for-syntax (remembered? category value) + (unless (writable? value) + (error "Value to remember does not seem to be safely writable:" + value)) + (set-member? (hash-ref remembered-values category set) value)) + + (define-for-syntax (written? category value) + (unless (writable? value) + (error "Value to remember does not seem to be safely writable:" + value)) + (set-member? (hash-ref written-values category set) value)) + + (define-for-syntax (remembered-or-written? category value) + (or (remembered? category value) + (written? category value))) + + (define-for-syntax (remember-write! category value) + (unless (writable? value) + (error "Value to remember does not seem to be safely writable:" + value)) + (unless (or (remembered? category value) + (written? category value)) + (when (remember-output-file-parameter) + (with-output-file [port (remember-output-file-parameter)] + #:exists 'append + (writeln (list 'remembered! category value) + port))) + (remembered-add-written! category value)))] + +@chunk[ + (begin-for-syntax + (define remember-errors-list '()) + (define remember-lifted-error #f))] + +@chunk[ + (define-for-syntax (remembered-error! category + stx-value + [stx-errs (list stx-value)]) + (set! remember-errors-list + (cons (list category stx-value stx-errs) remember-errors-list)) + + (unless (disable-remember-immediate-error) + (if (not (syntax-local-lift-context)) + ;; Trigger the error right now + (remember-all-hard-error) + ;; Lift a delayed error, which will be triggered later on + (lift-maybe-delayed-errors)))) + + (define-for-syntax (remembered-add-error! category stx-value) + (remembered-add! category (syntax-e stx-value)) + (remembered-error! category stx-value))] + +@CHUNK[ + ;; These two functions allow us to wait around 1000 levels of nested + ;; macro-expansion before triggering the error. + ;; If the error is triggered immediately when the lifted statements are + ;; added at the end of the module, then it can get executed before macros + ;; used in the righ-hand side of a (define …) are expanded, for example. + ;; Since these macros may need to remember more values, it's better to + ;; wait until they are all expanded. + ;; The number 1000 above in #`(delay-remember-all-hard-error1 1000) is + ;; arbitrary, but should be enough for most practical purposes, worst + ;; case the file would require a few more compilations to settle. + (define-syntax (delay-remember-all-hard-error1 stx) + (syntax-case stx () + [(_ n) + (number? (syntax-e #'n)) + (if (> (syntax-e #'n) 0) + #`(let () + (define blob + (delay-remember-all-hard-error2 #,(- (syntax-e #'n) 1))) + (void)) + (begin (syntax-local-lift-module-end-declaration + #`(remember-all-hard-error-macro)) + #'(void)))])) + + (define-syntax (delay-remember-all-hard-error2 stx) + (syntax-case stx () + [(_ n) + (number? (syntax-e #'n)) + (begin + (syntax-local-lift-module-end-declaration + #'(delay-remember-all-hard-error1 n)) + #'n)])) + + (define-for-syntax (remember-all-hard-error) + (define remember-errors-list-orig remember-errors-list) + (set! remember-errors-list '()) + (unless (empty? remember-errors-list-orig) + (raise-syntax-error + 'remember + (format (~a "The values ~a were not remembered." + " Some of them may have been added to the" + " appropriate list automatically." + " Please recompile this file now.") + (string-join (remove-duplicates + (reverse + (stx-map (compose ~a syntax->datum) + (map cadr + remember-errors-list-orig)))) + ", ")) + #f + #f + (remove-duplicates + (append-map caddr remember-errors-list-orig) + #:key (λ (e) + (cons (syntax->datum e) + (build-source-location-list e))))))) + (define-syntax (remember-all-hard-error-macro stx) + (remember-all-hard-error) + #'(void))] + +The @racket[disable-remember-immediate-error] parameter allows code to +temporarily prevent @racket[remembered-error!] from lifting a delayed error. +This can be useful for example when calling @racket[remembered-error!] from a +context where @racket[(syntax-local-lift-context)] is @racket[#false], e.g. +outside of the expansion of a macro, but within a @racket[begin-for-syntax] +block. + +@chunk[ + (define-for-syntax disable-remember-immediate-error (make-parameter #f))] + +The error is still put aside, so that if a delayed error was triggered by +another call to @racket[remembered-error!], the error will still be included +with the other delayed errors. If no delayed error is triggered during +macro-expansion, the error that was put aside will be ignored. To prevent +that, the user can call @racket[lift-maybe-delayed-errors] within a context +where lifts are possible. + +@chunk[ + (define-for-syntax (lift-maybe-delayed-errors) + (if (syntax-transforming-module-expression?) + ;; Lift a delayed error, attempting to allow several (1000) levels + ;; of nested let blocks to expand before pulling the alarm signal. + (unless remember-lifted-error + (set! remember-lifted-error #t) + (syntax-local-lift-module-end-declaration + #`(delay-remember-all-hard-error1 1000))) + ;; Lift a delayed error, which will be triggered after the current + ;; expansion pass (i.e. before the contents of any let form is + ;; expanded). + (syntax-local-lift-expression + #`(remember-all-hard-error-macro))))] + + +@CHUNK[ + (define-for-syntax (get-remembered category) + (hash-ref remembered-values category set))] + +@chunk[ + (begin-for-syntax + (provide get-remembered + remembered-add! + remembered? + remembered-or-written? + remember-write! + remembered-error! + remember-output-file-parameter + disable-remember-immediate-error + lift-maybe-delayed-errors)) + (provide remember-input-file + remember-output-file + remember-io-file + remembered!) + + (module+ private + (begin-for-syntax + (provide remembered-add-written!)))] + +@; TODO: circumvents bug https://github.com/racket/scribble/issues/44 +@(require racket/require) +@chunk[<*> + (require mzlib/etc + ;; TODO: circumvent https://github.com/racket/scribble/issues/44 + racket/require + (subtract-in phc-toolkit/untyped syntax/stx) + syntax/stx + (for-syntax racket/base + racket/function + racket/bool + racket/set + racket/list + mzlib/etc + ;;TODO: https://github.com/racket/scribble/issues/44 + (subtract-in phc-toolkit/untyped + syntax/stx) + syntax/stx + syntax/srcloc + racket/string + racket/format)) + + + + + + + + + + ] \ No newline at end of file diff --git a/scribblings/remember.scrbl b/scribblings/remember.scrbl new file mode 100644 index 0000000..e2ce798 --- /dev/null +++ b/scribblings/remember.scrbl @@ -0,0 +1,258 @@ +#lang scribble/manual +@require[@for-label[remember + racket/base]] + +@title{Remember: storage for macros which is persistant across compilations} +@author{Georges Dupéron} + +@defmodule[remember] + +This library is implemented using literate programming. The +implementation details are presented in +@other-doc['(lib "remember/remember-implementation.hl.rkt")]. + +This module allows macros to remember some values across +compilations. Values are grouped by @racket[_category], so +that multiple macros can use this facility without +interfering with each other. The @racket[_category] is +simply a symbol given when remembering the value. + +The list of all remembered values for a given +@racket[_category] is returned by @racket[get-remembered], +and it is possible to check if a single value has been +remembered using @racket[remembered?]. + +Values are loaded from files using +@racket[remember-input-file] and @racket[remember-io-file]. +An output file can be set with +@racket[remember-output-file] and +@racket[remember-io-file]. + +When an output file has been declared, new values passed to +@racket[remember-write!] are marked as +@racket[remembered-or-written?] and appended to that file +(more precisely, the expression +@racket[(remembered! _category _value)] is appended to the +file, followed by a newline). + +When initially created by the user, the output file should +contain the code below, which will be followed by the +automatically-generated +@racket[(remembered! _category _value)] statements: + +@codeblock[#:keep-lang-line? #t]|{ + #lang racket + (require remember)}| + +The @racket[remembered!] macro indicates an +already-remembered value, and is typically used inside input +files. The @racket[for-syntax] function +@racket[remembered-add!] can also be used instead, to mark a +value as @racket[remembered?] without adding it to any file +(this can be useful for values which should implicitly be +remembered). + +@defproc[#:kind "for-syntax procedure" + (get-remembered [category symbol?]) list?]{ + Returns a list of all values that have been remembered for + the given @racket[category] (i.e. all values passed as the + second argument to @racket[remembered-add!], + @racket[remember-write!] or @racket[remembered!], with the given + category as the first argument).} + +@defproc[#:kind "for-syntax procedure" + (remembered-add! [category symbol?] [value any/c]) void?]{ + Marks the given @racket[value] as remembered in the given + @racket[category]. If the same value is remembered twice + for the same category, the second occurrence is ignored + (i.e. values are stored in a distinct @racket[set] for each + category). + + This @racket[for-syntax] procedure is called by the + @racket[remembered!] macro, but can also be executed on its + own.} + +@defproc[#:kind "for-syntax procedure" + (remembered? [category symbol?] [value any/c]) boolean?]{ + Checks whether the given @racket[value] has already been + added to the set of remembered values for the given + @racket[category].} + +@defproc[#:kind "for-syntax procedure" + (remembered-or-written? [category symbol?] [value any/c]) boolean?]{ + Checks whether the given @racket[value] has already been + added to the set of remembered values for the given + @racket[category], or if it was freshly written to a file + during the current expansion.} + +@defproc[#:kind "for-syntax procedure" + (remember-write! [category symbol?] [value any/c]) void?]{ + Adds the given @racket[value] to the current + @racket[remember-output-file] for the given category. More + precisely, the expression + @racket[(remembered! category value)] is appended to the + file, followed by a newline. + + If the value is already @racket[remembered-or-written?], + then the file is left unchanged, i.e. two or more calls to + @racket[remember-write!] with the same @racket[category] + and @racket[value] will only append an expression to the + file the first time. + + The value is also added to the set of + @racket[remembered-or-written?] values, so that subsequent + calls to @racket[remembered-or-written?] return + @racket[#t] for that category and value. Calls to + @racket[remembered?] will be unaffected, and will still + return @racket[#f]. If some declarations are created by a + library based on the @racket[get-remembered] set, it is + therefore possible to check whether a value was already + present, or if it was added by a subsequent + @racket[remember-write!].} + +@defproc[#:kind "for-syntax procedure" + (remembered-error! [category symbol] [stx-value syntax?]) void?]{ + Produces a delayed error indicating that this value has + not been remembered, but was added to the output file. + + This procedure just triggers the error, and is not + concerned with actually adding the value to the output + file. + + The error is added in a lifted declaration which is + inserted at the end of the current module, using + @racket[syntax-local-lift-module-end-declaration]. It + should therefore be triggered only when the compilation + reaches the end of the file, if no other error was raised + before. + + This allows as many @racket[remembered-error!] errors as + possible to be accumulated; all of these are then shown + when the file is fully expanded. The goal is to be able to + add all values to the output file in a single run, instead + of aborting after each value which is not remembered. This + would otherwise require recompiling the program once for + each value which is not initially remembered. + + TODO: it would be nice to factor out the delayed error + mechanism into a separate package, so that multiple + libraries can add errors, and all of them get reported, + without one preventing the others from executing. This + function would likely keep the same signature, and just + delegate to the delayed-error library.} + +@defparam[disable-remember-immediate-error disable? boolean? #:value #f]{ + The @racket[disable-remember-immediate-error] parameter allows code to + temporarily prevent @racket[remembered-error!] from lifting a delayed error. + This can be useful for example when calling @racket[remembered-error!] from a + context where @racket[(syntax-local-lift-context)] is @racket[#false], e.g. + outside of the expansion of a macro, but within a @racket[begin-for-syntax] + block. + + The error is still put aside, so that if a delayed error was triggered by + another call to @racket[remembered-error!], the error will still be included + with the other delayed errors. If no delayed error is triggered during + macro-expansion, the error that was put aside will be ignored. To prevent + this from happening, call @racket[lift-maybe-delayed-errors] within a context + where lifts are possible.} + +@defproc[(lift-maybe-delayed-errors) void?]{ + Uses @racket[syntax-local-lift-module-end-declaration] or + @racket[syntax-local-lift-expression], depending on the context, to lift an + expression which will trigger delayed errors, if any. If no delayed errors + have been recorded by @racket[remembered-error!] when the lifted form is + executed, then nothing will happen and expansion will proceed. + + Note that when @racket[(syntax-transforming-module-expression?)] returns + @racket[#false], @racket[syntax-local-lift-expression] is used. The lifted + form is then run as part of the current expansion pass, before the contents of + any @racket[let] forms are expanded. This means that calls to + @racket[remembered-error!] must not happen within the expansion of nested + @racket[let] forms (with respect to the @racket[let] form being expanded (if + any) when @racket[lift-maybe-delayed-errors] is called), as they would add + delayed errors too late, i.e. after the lifted form got executed.} + +@defform[(remember-input-file name) + #:grammar ([name string?])]{ + The file is loaded with @racket[require], but no + identifier is imported from that module. Instead, + @racket[remembered?] relies on its internal mutable + @racket[for-syntax] hash table which stores remembered + values associated to their category. + + @racket[remembered-values]. Values are added to the hash + via the @racket[remembered!] macro. The @racket[name] file + should therefore @racket[require] the + @racketmodname[remember] library, and contain a number of + calls to @racket[remembered!], each adding a new value to + the mutable hash.} + +@deftogether[ + (@defform*[((remember-output-file) + (remember-output-file name)) + #:grammar ([name (or/c string? false?)])] + @defproc*[#:kind "for-syntax parameter" + #:link-target? #f + ([(remember-output-file) (or/c string? false?)] + [(remember-output-file [name (or/c string? false?)]) void?])] + )]{ + Indicates that new values added via + @racket[remember-write!] should be appended to the file + @racket[name]. More precisely, the expression + @racket[(remembered! _category _value)] is appended to the + file, followed by a newline. + + Note that if the @racket[_value] given to + @racket[remember-write!] is already registered in an input + file with @racket[remembered!] for the same category, it + will not be appended to the output file. + + For now there can only be one @racket[output] file at the + same time, any call to @racket[remember-output-file] + overrides the setting from previous calls. Future versions + of this library may offer the possibility to specify an + output file per @racket[_category]. + + The special value @racket[#f] indicates that there is no + output file, in which case @racket[remember-write!] simply + marks the @racket[value] as + @racket[remembered-or-written?] for that category, without + altering any file. + + This identifier exists both as a macro and a for-syntax + parameter. When called without any argument, it expands to + (for the macro) or returns (for the for-syntax parameter) + the last value set using either the macro or by passing an + argument to the for-syntax parameter.} + +@defparam[remember-output-file-parameter output-file + (or/c path-string? false?) + #:value #f]{ + This for-syntax parameter that new values added via @racket[remember-write!] + should be appended to the file whose name is stored within the parameter. + + The @racket[remember-output-file] macro simply sets this parameter.} + +@defform[(remember-io-file name) + #:grammar ([name string?])]{ + Indicates that calls to @racket[remembered!] in this file + should be taken into account, and that new values added + with @racket[remember-write!] should be appended to this + file. + + It is equivalent to: + @racketblock[(remember-input-file name) + (remember-output-file name)]} + +@defform[(remembered! category value) + #:grammar ([category identifier?])]{ + Marks the given @racket[value] as remembered in the given + @racket[category]. If the same value is remembered twice + for the same category, the second occurrence is ignored + (i.e. values are stored in a distinct @racket[set] for each + category). + + Calls to this macro are usually present in an input file + loaded with @racket[remember-input-file] or + @racket[remember-io-file], but can also be inserted in the + main file or any other file loaded with @racket[require].} \ No newline at end of file diff --git a/test/input-error.rkt b/test/input-error.rkt new file mode 100644 index 0000000..81b9d1d --- /dev/null +++ b/test/input-error.rkt @@ -0,0 +1,7 @@ +#lang racket +(require remember) +(remembered! foo-error (1 2 3)) +(remembered! foo-error (1 2 3 4)) +(remembered! foo-error (1 2 3 5)) +(define + 'wrong) +(provide +) \ No newline at end of file diff --git a/test/input1.rkt b/test/input1.rkt new file mode 100644 index 0000000..3dcb6a4 --- /dev/null +++ b/test/input1.rkt @@ -0,0 +1,5 @@ +#lang racket +(require remember) +(remembered! foo (1 2 3)) +(remembered! foo (1 2 3 4)) +(remembered! foo (1 2 3 5)) \ No newline at end of file diff --git a/test/input3.rkt b/test/input3.rkt new file mode 100644 index 0000000..c0e93ae --- /dev/null +++ b/test/input3.rkt @@ -0,0 +1,7 @@ +#lang racket +(require remember) +(remembered! foo3 (1 2 3)) +(remembered! foo3 (1 2 3 4)) +(remembered! foo3 (1 2 3 5)) +(define + 'wrong) +(provide +) \ No newline at end of file diff --git a/test/io2.rkt b/test/io2.rkt new file mode 100644 index 0000000..0329c05 --- /dev/null +++ b/test/io2.rkt @@ -0,0 +1,3 @@ +#lang racket +(require remember) +(remembered! bar (1 2 3 xyz)) diff --git a/test/test-error.rkt b/test/test-error.rkt new file mode 100644 index 0000000..4cd454b --- /dev/null +++ b/test/test-error.rkt @@ -0,0 +1,18 @@ +#lang racket + +(require remember + rackunit) +(remember-input-file "input-error.rkt") +(define-syntax (test-rem stx) + (syntax-case stx () + [(_ val) + (let ([v (syntax-e #'val)]) + (unless (remembered? 'err-category v) + (remembered-error! 'err-category #'val))) + #'(void)])) + +(test-rem one) +(test-rem two) +(check-equal? (+ 1 2) 3) +(test-rem three) +(test-rem four) \ No newline at end of file diff --git a/test/test1.rkt b/test/test1.rkt new file mode 100644 index 0000000..261354e --- /dev/null +++ b/test/test1.rkt @@ -0,0 +1,13 @@ +#lang racket +(require remember + rackunit + (submod "../remember-implementation.hl.rkt" private)) +(remember-input-file "input1.rkt") +(begin-for-syntax + (require rackunit) + (define secs (current-seconds)) + (remembered-add-written! 'foo `(1 2 3 secs)) + (check-false (remembered? 'foo `(1 2 3 secs))) + (check-true (remembered-or-written? 'foo `(1 2 3 secs)))) +;; check that no identifiers were imported from "input1.rkt". +(check-not-equal? + 'wrong) \ No newline at end of file diff --git a/test/test2.rkt b/test/test2.rkt new file mode 100644 index 0000000..f7ddba3 --- /dev/null +++ b/test/test2.rkt @@ -0,0 +1,9 @@ +#lang racket +(require remember) +(remember-io-file "io2.rkt") +(begin-for-syntax + (require rackunit) + ;; Manually check for an error the first time this + ;; file is compiled after emptying io2.rkt + (check-true (remembered? 'bar '(1 2 3 xyz))) + (remember-write! 'bar '(1 2 3 xyz))) \ No newline at end of file diff --git a/test/test3.rkt b/test/test3.rkt new file mode 100644 index 0000000..8a29fe1 --- /dev/null +++ b/test/test3.rkt @@ -0,0 +1,8 @@ +#lang racket +(require remember) +(remember-io-file "input3.rkt") +(begin-for-syntax + (require rackunit + racket/set) + (check set=? (get-remembered 'foo3) + (set '(1 2 3) '(1 2 3 5) '(1 2 3 4)))) \ No newline at end of file