AN EXAMPLE OF MACROS AND MODULES (ver 3) -- Jens Axel Søgaard --------------------------------------------------------------------------------- Note: I tried to think up a simple example. Suggestions for making simplifications are welcome. A real-world example of these ideas can be found in the PLT implementation of the EOPL's define-datatypes and cases, which is two macros that work together. (See ) See [Flatt] for a more elaboarate explanation of goals and phases. [Flatt] "Composable and Compilable Macros - You Want it When?". Matthew Flatt, ICFP 2002 GOAL 1 ------ An important goal for a module system that supports both interactive and compiles use is: 1) The meaning of a code fragment does not depend on the order in which code is compiled and executed. This is an important goal because: i) A programmer can think of the module system as a way to specify which language the code of the module should be written in. A "language" should in this context be thought of as a set of visible bindings for both values and syntax. ii) Users of a module does not need to know how anything about the dependencies of the included module. iii) If the program works in an interactive environment, it will also work when compiled. GOAL 2 ------ The second goal is: 2) All errors that can be checked statically should be detected at compile-time. The rationale for this goal is: i) Static checking provides better error messages than run-time errors, since errors can inform of the programmer of the syntactical location of the error. This saves programmer time. ii) Things checked at compile-time need not be checked at run-time. Reducing the number of run-time checks. This leads to faster programs. [NOTE: Macro writers should respect goal 2) when they write their own macros. ] PROBLEMS IN ACHIEVING THE GOAL ------------------------------ The following is an attempt to show the difficulties of achieving both goal at the same time in the presence of macros. The difficult thing to get right for module system is the handling of the compile-time environment. The compile time environment is used by macros to share information between different macros. Information shared between macros is needed to to do proper static checking. As an concrete example we will look at the implementation of two forms: (define-constant name value) (constant name) The (define-constant name value) register that name is a constant, and (constant name) expands to the value. Two things should be checked at compile-time: a) Only names defined by define-constant can be used by constant. b) A name can only be defined by define-constant once. An usage example: > (define-constant root2 (sqrt 2)) > (constant root2) 1.4142135623730951 > (define e 2.71828) > (constant e) error: e not defined as constant > (define-constant root2 1.1415) error: define-constant: constant already defined; root2 Here is the first attempt: ;;; compile-time.scm (module compile-time mzscheme (provide register-constant lookup-constant constant-registered? constants syntax-error) ; list of registered constants (define constants '()) ; register a constant (define (register-constant name value) (set! constants (cons (cons name value) constants))) ; check if a constant is registered (define (constant-registered? name) (if (assoc name constants) #t #f)) ; lookup the value associated with the name of a name (define (lookup-constant name error-thunk) (let ((a (assoc name constants))) (if a (cdr a) (error-thunk)))) ; helper for signaling syntax errors (define (syntax-error message . values) (apply raise-syntax-error #f message values))) ;;; constants.scm (module constants mzscheme (provide define-constant constant) (require-for-syntax compile-time) (define-syntax (define-constant stx) (syntax-case stx () ((_ name datum) (begin (let ([constant-name (syntax-object->datum #'name)] [constant-value (syntax-object->datum #'datum)]) ;; Register the constant name and value for this compilation, ;; but first check that the constant hasn't been defined before (if (constant-registered? constant-name) (syntax-error "define-constant: can't redefine constant: " (syntax name)) (register-constant constant-name constant-value) ; <- compile-time of constants.scm ) ;; The result is simply the invisible value #'(void)))))) (define-syntax (constant stx) (syntax-case stx () ((_ name) (let* ([constant-name (syntax-object->datum #'name)] [constant-value (lookup-constant constant-name (lambda () (syntax-error "constant: unknown constant; " (syntax name))))]) (datum->syntax-object #'stx constant-value))))) ) And now let us try it: > (require constants) > (constant pi/2) [bug] pi/2: constant: unknown constant; in: pi/2 > (define-constant pi/2 (* 2 (atan 1))) > (constant pi/2) 1.5707963267948966 > (define-constant pi/2 4) [bug] pi/2: define-constant: can't redefine constant: in: pi/2 Seems to work fine, right? Let's see what happens when modules enter the picture: (module roots mzscheme (require constants) (define-constant root2 (sqrt 2)) (define-constant root3 (sqrt 3))) > (require constants) > (require roots) > (constant root2) root2: constant: unknown constant; in: root2 Why did this happen? During the compilation of constants.scm the constants are registered with the help of the operations in compile-time.scm among these operations are constant-registered? . When the compilation of constants.scm is finished, the imported values and functions from compile-time.scm disappears. When roots.scm is compiled, the values used during compilation of constants.scm is needed again, but the compiler doesn't ensure that these are revived. But we can't really blame the compiler - how should it know what to reinstate? Let's take a look at the conclusion in [Flatt]: "A language that allows macro transformers to perform arbitrary computation must enforce a separation between computations: run time versus compile-time, as well as the compile-time of one module versus the compile time of another. Without an enforced separation, the meaning of a code fragment can depend on the order in which code is compiled and executed." The key here is "as well as the compile-time of one module versus the compile time of another.". To handle this Flatt introduces phases. Furthermore the problem of what to reinstate is solved by spliting the normal environment and the syntactical environment. The solution is to change the definition of define-constant so that it registers the definitions also at compile-time for other modules. ;;; constants.scm (module constants mzscheme (provide define-constant constant) (require-for-syntax compile-time) (define-syntax (define-constant stx) (syntax-case stx () ((_ name datum) (begin (let ([constant-name (syntax-object->datum #'name)] [constant-value (syntax-object->datum #'datum)]) ;; Register the constant name and value for this compilation, ;; but first check that the constant hasn't been defined before (if (constant-registered? constant-name) (syntax-error "define-constant: can't redefine constant: " (syntax name)) (register-constant constant-name constant-value) ; <- compile-time of constants.scm ) ;;; Register for future compilations: ; If a module with a (register-constant ...) is required at compile time by another ; module M, then the constant names needs to be registered at the compile time of M. ; The right hand side of a define-syntax is evaluatued at compiletime, so ; we (mis)use it to register the constant name at the compile time of M. #`(begin-for-syntax (register-constant 'name #,constant-value))))))) (define-syntax (constant stx) (syntax-case stx () ((_ name) (let* ([constant-name (syntax-object->datum #'name)] [constant-value (lookup-constant constant-name (lambda () (syntax-error "constant: unknown constant; " (syntax name))))]) (datum->syntax-object #'stx constant-value))))) ) Now we get: > (module trig mzscheme (require constants) (define-constant pi (* 4 (atan 1))) (define-constant pi/2 (* 2 (atan 1)))) > (require constants) > (require trig) > (constant pi/2) 1.5707963267948966