Understand macro and its strategies by tracking typed/racket
typed/racket is huge and seems impossible to understand, so I want to make try about this situation. This article will give you a systematic way to learn how to break down a complex macro system, by showing you a short tracking into typed/racket.
The following code is all based on commit: 20acc4.
First, please download or browse the typed/racket repository and open your DrRacket editor. In the beginning, we start from a tiny example.
#lang typed/racket (define a : Integer 1)
Put the code into DrRacket and click the Macro Stepper button, you will see a window showing some information like the following.
(#%plain-module-begin (define a : Integer 1)) → [Macro transformation] (#%plain-module-begin (begin (: a Integer) (define a 1)))
Thus, you know the very first transformation is:
(define <name> : <Typ> <exp>) ; ===> (: <name> <Typ>) (define <name> <exp>)
You can try to rewrite the code like this, and put the cursor on to
(: a Integer), it refers to
(define a 1). Don't you feel
amazing? They are separate codes, but related to each other? Let's
explore the next part to figure out this. Keep clicking the Macro
Stepper, you will get the following.
(#%plain-module-begin (define-values () (:-helper (: a Integer) #f a Integer)) (define a 1)) → [Macro transformation] (#%plain-module-begin (define-values () (:-helper (: a Integer) #f a Integer)) (define-values (a) 1))
:-helper is the target we should care about, it will expand to
:-internal. However, the definition isn't that clear, thus I made a
small pruned version for it.
#lang racket (require syntax/parse/define (for-syntax racket/syntax)) (define-syntax-parser : [(_ n:id ty) (syntax-property (syntax/loc #'n (begin (quote-syntax (:-internal n ty) #:local))) 'disappeared-use (syntax-local-introduce #'n))]) (define-syntax-parser def #:datum-literals (:) [(_ n:id : ty exp) (with-syntax ([tmp-name (generate-temporary)]) #'(begin (: n ty) (define n exp)))]) (def a : Int 0) (: b Int) (define b "abc")
:-helper stage and produces
:-internal, in a
quote-syntax! Another important thing here is the
this will tell racket where is the
#'n came from, and
(syntax-local-introduce #'n) refers to the context to know it(It's
easy to determine because we generate
(define n exp)). Keep tracking
:-internal, we will see syntax-class
type-declaration is trying to
(define-internal-classes ... [type-declaration (:-internal id:identifier type)] ...)
This syntax-class is used in
tc-toplevel/pass1 for recording information based on
them will be used later.
;; syntax? -> (listof def-binding?) (define (tc-toplevel/pass1 form) (parameterize ([current-orig-stx form]) (syntax-parse form #:literals (values define-values #%plain-app begin define-syntaxes make-struct-type) ... [t:type-declaration (register-type/undefined #'t.id (add-extracted-props-to-lexical-env (-id-path #'r.name) (parse-type #'t.type))) (register-scoped-tvars #'t.id (parse-literal-alls #'t.type)) (list)] ...)
The next is the type checking code, in
tc-toplevel/pass2 typed/racket extract type information what it
just recorded, then invoke
tc-expr/check to check
;; typecheck the expressions of a module-top-level form ;; no side-effects ;; syntax? -> (or/c 'no-type tc-results/c) (define (tc-toplevel/pass2 form [expected (-tc-any-results #f)]) (do-time (format "pass2 ~a line ~a" (if #t (substring (~a (syntax-source form)) (max 0 (- (string-length (~a (syntax-source form))) 20))) (syntax-source form)) (syntax-line form))) (parameterize ([current-orig-stx form]) (syntax-parse form #:literal-sets (kernel-literals) ... [(define-values (var:typed-id^ ...) expr) (let ([ts (attribute var.type)]) (when (= 1 (length ts)) (add-scoped-tvars #'expr (lookup-scoped-tvars (stx-car #'(var ...))))) (tc-expr/check #'expr (ret ts))) 'no-type] ...)
Finally, we can see the
tc-expr/check, as the following code. If you have interest, you can
keep digging, but I'm going to jump out and introduce the big picture of
;; form : what expression are we typechecking? ;; expected : what is the expected tc-result (can be #f) ;; existential? : do we want to create an existential ;; identifier for this expression if it does not ;; have a non-trivual object? This is useful when ;; the type of other expressions can depend on ;; the specific type of this term. (define (tc-expr/check form expected [existential? #f]) (parameterize ([current-orig-stx form]) ;; the argument must be syntax (unless (syntax? form) (int-err "bad form input to tc-expr: ~a" form)) (define result ;; if there is an annotation, use that expected type for internal checking (syntax-parse form [exp:type-ascription^ (add-scoped-tvars #'exp (parse-literal-alls (attribute exp.value))) (tc-expr/check/internal #'exp (parse-tc-results (attribute exp.value)))] [_ (reduce-tc-results/subsumption (tc-expr/check/internal form expected))])) ;; if 'existential?' is true, then it means this expression should be ;; given an existential identifier as an object if it has no object (define adjusted-result (cond [existential? (match result [(tc-result1: t ps (not (? Object?))) (ret t ps (-id-path (gen-existential-id)))] [_ result])] [else result])) (add-typeof-expr form adjusted-result) (cond-check-below adjusted-result expected)))
tc-toplevel/pass1, I can tell the big picture now, the
type-check will check the module and invoke
tc-toplevel/pass1 for every top-level form.
;; actually do the work on a module ;; produces prelude and post-lude syntax objects ;; syntax-list -> (values syntax syntax) (define (type-check forms0) ...)
In the very beginning, we already know the transformation. Hence, what typed/racket does, is convert your code into two parts:
- The type information and others meta data be quoted by
- The code will be executed in the final and need to be checked, with the information collects above.
This is all, hope you learn something and realize what can the
interceptor point like
#%module-begin can bring, and maybe have some
ideas about how to use these!