2 The Turnstile Reference
2.1 Typing Unicode Characters
Turnstile utilizes unicode. Here are DrRacket keyboard shortcuts for the relevant characters. Type (any unique prefix of) the following and then press Control-\ or Alt-\.
\vdash → ⊢
\gg → ≫
\rightarrow → →
\Rightarrow → ⇒
\Leftarrow → ⇐
\succ → ≻
See also Non-Unicode Identifiers for non-unicode aliases.
2.2 Forms
syntax
(define-typed-syntax (name-id . pattern) ≫ premise ... -------- conclusion) (define-typed-syntax name-id option ... rule ...+)
option = syntax-parse option rule =
[expr-pattern ≫ premise ... -------- conclusion] |
[expr-pattern ⇐ type-pattern ≫ premise ... -------- ⇐-conclusion] |
[expr-pattern ⇐ key pattern ≫ premise ... -------- ⇐-conclusion] expr-pattern = syntax-parse syntax pattern type-pattern = syntax-parse syntax pattern expr-template = quasisyntax template type-template = quasisyntax template key = identifier? premise = [⊢ tc ... prem-options] ooo ... | [ctx-elem ... ⊢ tc ... prem-options] ooo ... | [ctx ⊢ tc ... prem-options] ooo ... | [ctx ctx ⊢ tc ... prem-options] ooo ... | [⊢ . tc-elem] ooo ... | [ctx-elem ... ⊢ . tc-elem] ooo ... | [ctx ⊢ . tc-elem] ooo ... | [ctx ctx ⊢ . tc-elem] ooo ... | type-relation | #:mode mode-expr (premise ...) | #:submode fn-expr (premise ...) | syntax-parse pattern directive ctx = (ctx-elem ...) ctx-elem = [id ≫ id key template ... ...] ooo ... tc = tc-elem ooo ... tc-elem = [expr-template ≫ expr-pattern ⇒ type-pattern] | [expr-template ≫ expr-pattern ⇒ key pattern] | [expr-template ≫ expr-pattern (⇒ key pattern) ooo ...] | [expr-template ≫ expr-pattern ⇐ type-template] | [expr-template ≫ expr-pattern ⇐ key template] | [expr-template ≫ expr-pattern (⇐ key template) ooo ...] type-relation = [type-template τ= type-template] ooo ... | [type-template τ= type-template #:for expr-template] ooo ... | [type-template τ⊑ type-template] ooo ... | [type-template τ⊑ type-template #:for expr-template] ooo ... prem-options =
| #:mode mode-expr | #:submode fn-expr conclusion = [⊢ expr-template ⇒ type-template] | [⊢ expr-template ⇒ key template] | [⊢ expr-template (⇒ key template) ooo ...] | [≻ expr-template] | [#:error expr-template] ⇐-conclusion = [⊢ expr-template] ooo = ...
Premises
Bidirectional judgements
[⊢ e ≫ e- ⇒ τ] declares that expression e expands to e- and has type τ, where e is the input and, e- and τ outputs. Syntactically, e is a syntax template position and e- and τ are syntax pattern positions.
Dually, one may write [⊢ e ≫ e- ⇐ τ] to check that e has type τ. Here, both e and τ are inputs (templates) and only e- is an output (pattern).
Each bidirectional arrow has a generalized form that associates a key with a value, e.g., [⊢ e ≫ e- (⇒ key pat) ...]. A programmer may use this generalized form to specify propagation of arbitrary values, associated with any number of keys. For example, a type and effect system may wish to additionally propagate source locations of allocations and mutations. When no key is specified, :, i.e., the "type" key, is used.
Contexts
A context may be specified to the left of the turnstile. A context element has shape [⊢ x ≫ x- key pat ... ...] where x- is a pattern matching the expansion of x and the interleaved key ... and pat ... are arbitrary key-value pairs, e.g. a : key and type pattern.
A context has let* semantics where each binding is in scope for subsequent parts of the context. This means type and term variables may be in the same context (if properly ordered). In addition, Turnstile allows writing two separate contexts, grouped by parens, where bindings in first are in scope for the second. This is often convenient for scenarios that Racket’s pattern language cannot express, e.g., when there two distinct groups of bindings, a pattern x ... y ... will not work as expected.
For convenience, lone identifiers written to the left of the turnstile are automatically treated as type variables.
Modes
The keyword #:mode, when appearing at end of a typechecking rule, sets the parameter current-mode to the mode object supplied by mode-expr, for the extent of that rule. #:mode, when appearing as its own premise, sets the current-mode parameter for the extent of all the grouped sub-premises.
The keyword #:submode is similar to #:mode, but it calls (fn-expr (current-mode)) to obtain the new mode object. Thus, #:mode (fn-expr (current-mode)) is identical to #:submode fn-expr.
See Modes for more details.
WARNING: #:mode is unaware of the backtracking behavior of syntax-parse. If pattern backtracking escapes a #:mode group, it may leave current-mode in an undesirable state.
Conclusion
Bidirectional judgements below the conclusion line invert their inputs and outputs so that both positions in [⊢ e- ⇒ τ] are syntax templates and means that e- is the output of the generated macro and it has type τ attached. The generalized key-value form of the bidirectional judgements may also be used in the conclusion.
The ≻ conclusion form is useful in many scenarios where the rule being implemented may not want to attach type information. E.g.,
when a rule’s output is another typed macro.
For example, here is a hypothetical typed-let* that is implemented in terms of a typed-let:
(define-typed-syntax typed-let* [(_ () e_body) ≫ -------- [≻ e_body]] [(_ ([x e] [x_rst e_rst] ...) e_body) ≫ -------- [≻ (typed-let ([x e]) (typed-let* ([x_rst e_rst] ...) e_body))]]) The conclusion in the first clause utilizes ≻ since the body already carries its own type. The second clause uses ≻ because it defers to typed-let, which will attach type information.
when a rule extends another.
This is related to the first example. For example, here is a #%datum that extends another with more typed literals (see also Extending a Language).
(define-typed-syntax typed-datum [(_ . n:integer) ≫ -------- [⊢ (#%datum- . n) ⇒ Int]] [(_ . x) ≫ -------- [#:error (type-error #:src #'x #:msg "Unsupported literal: ~v" #'x)]]) (define-typed-syntax extended-datum [(_ . s:str) ≫ -------- [⊢ (#%datum- . s) ⇒ String]] [(_ . x) ≫ -------- [≻ (typed-datum . x)]]) for top-level forms.
For example, here is a basic typed version of define:
(define-typed-syntax define [(_ x:id e) ≫ [⊢ e ≫ e- ⇒ τ] #:with y (generate-temporary #'x) -------- [≻ (begin- (define-typed-variable-rename x ≫ y : τ) (define- y e-))]]) This macro creates an indirection define-typed-variable-rename in order to attach type information to the top-level x identifier, so the define forms themselves do not need type information.
Note: define-typed-syntax is generated by (define-syntax-category type). See define-syntax-category for more information.
syntax
(define-typerule ....)
syntax
(define-syntax/typecheck ....)
syntax
(syntax-parse/typecheck stx option ... rule ...+)
syntax
(define-typed-variable-rename typed-var ≫ untyped-var : type)
syntax
(define-typed-variable id typed-e ⇐ τ)
(define-typed-variable id typed-e) (define-typed-variable id untyped-e ⇒ τ)
With the ⇐ variant, typed-e is checked to have type τ. id also has type τ.
With middle variant, id has the type of typed-e.
With the ⇒ variant, both id and untyped-e are assigned type τ (using assign-type).
The difference between define-typed-variable and define-typed-variable-rename is that the latter assumes that an untyped name already exists. In contrast, define-typed-variable uses generate-temporary to first define an untyped name, and then uses define-typed-variable-rename to define the typed name in terms of the untyped name. E.g., define-primop essentially just uses define-typed-variable-rename, but a typed define needs more.
Here is an example typed-λ and typed-define that expands to typed-λ and define-typed-variable.
(define-typerule typed-λ [(_ ([x:id (~datum :) τ:type] ...) e) ≫ [[x ≫ x- : τ.norm] ... ⊢ e ≫ e- ⇒ τout] -------------- [⊢ (λ- (x- ...) e-) ⇒ (-> τ.norm ... τout)]] [(_ (x:id ...) e) ⇐ (~-> τin ... τout) ≫ [[x ≫ x- : τin] ... ⊢ e ≫ e- ⇐ τout] -------------- [⊢ (λ- (x- ...) e-)]]) (define-typerule (define (f:id [x:id (~datum :) τin:type] ...) (~datum :) τout:type e) ≫ -------- [≻ (define-typed-variable f (typed-λ (x ...) e) ⇐ (-> τin.norm ... τout.norm))])
syntax
(define-primop typed-op-id τ)
(define-primop typed-op-id : τ) (define-primop typed-op-id op-id τ) (define-primop typed-op-id op-id : τ) (define-primop typed-op-id #:as op-id τ) (define-primop typed-op-id #:as op-id : τ)
(define-primop typed+ + : (→ Int Int))
When not specified, op-id is typed-op-id suffixed with - (see Suffixed Racket Bindings).
syntax
(define-syntax-category name-id)
(define-syntax-category key1 name-id) (define-syntax-category key1 name-id key2)
Each category of syntax is associated with two keys: key1 is used when attaching values in the category to other syntax, e.g., attaching types to terms, and key2 is used for attaching an additional syntax property to values in the category, e.g. using #%type to indicate well-formedness.
If no keys are specified, key1 is : and key2 is ::. If only key1 is given, then key2 is key1 appended to itself.
Defining another category, e.g., (define-syntax-category kind) defines a separate set of forms and functions, e.g., define-kinded-syntax, define-base-kind, kind=?, etc.
The following forms and functions are automatically defined by a (define-syntax-category type) declaration: define-typed-syntax, define-base-type, define-base-types, define-type-constructor, define-binding-type, define-internal-type-constructor, define-internal-binding-type, current-type-eval, current-typecheck-relation, typecheck?, typechecks?, current-type=?, type=?, types=?, same-types?, type?, current-type?, any-type?, current-any-type?, assign-type, typeof, #%type, #%type?, mk-type, type, any-type, type-bind, type-ctx, type-ann
2.3 Forms for Defining Types
Note: The forms in this section are generated by (define-syntax-category type). See define-syntax-category for more information.
syntax
(define-base-type base-type-name-id option ...)
(define-base-type base-type-name-id key tag option ...)
option = #:runtime
τ, an identifier macro representing type τ.
τ?, a phase 1 predicate recognizing type τ.
~τ, a phase 1 pattern expander recognizing type τ.
Types defined with define-base-type are automatically tagged with a key2-keyed (as specified in the define-syntax-category declaration) #%type syntax property (essentially, a default kind), unless the second form above is used, in which case the specified tag is attached to the type using the specified key.
The #:runtime argument dictates that, rather than throwing an error, type evaluates to a value at runtime.
syntax
(define-base-types base-type-name-id ...)
syntax
(define-type-constructor name-id option ...)
option = #:arity op n | #:runtime | #:arg-variances expr | #:extra-info stx
τ, a macro for constructing an instance of type τ, with the specified arity. Validates inputs and expands to τ-, attaching #%type at key2.
τ-, an internal macro that expands to the internal (i.e., fully expanded) type representation. Does not validate inputs or attach any extra properties. This macro is useful when creating a separate kind system, see define-internal-type-constructor.
τ?, a phase 1 predicate recognizing type τ.
~τ, a phase 1 pattern expander recognizing type τ.
The #:arity argument specifies the valid shapes for the type. For example (define-type-constructor → #:arity >= 1) defines an arrow type and (define-type-constructor Pair #:arity = 2) defines a pair type. The default arity is = 1.
The #:runtime argument dictates that, rather than throwing an error, type evaluates to a value at runtime.
The #:arg-variances argument is a transformer converting a syntax object of the type to a list of variances for the arguments to the type constructor.
The possible variances are invariant, contravariant, covariant, and irrelevant.
If #:arg-variances is not specified, invariant is used for all positions.
Example:
(define-type-constructor → #:arity >= 1 |
#:arg-variances |
(λ (stx) |
(syntax-parse stx |
[(_ τ_in ... τ_out) |
(append |
(make-list (stx-length #'[τ_in ...]) contravariant) |
(list covariant))]))) |
The #:extra-info argument is useful for attaching additional metainformation to types, for example to communicate accessors to a pattern matching form.
syntax
(define-binding-type name-id option ...)
option = #:arity op n | #:bvs op n | #:arr kindcon | #:arg-variances expr | #:extra-info stx
The #:arity and #:bvs arguments specify the valid shapes for the type. For example (define-binding-type ∀ #:arity = 1 #:bvs = 1) defines a type with shape (∀ (X) τ), where τ may reference X.
The default #:arity is = 1 and the default #:bvs is >= 0.
Use the #:arr argument to define a type with kind annotations on the type variables. The #:arr argument is an "arrow" that "saves" the annotations after a type is expanded and annotations are erased, analogous to how → "saves" the type annotations on a lambda.
syntax
(define-internal-type-constructor name-id option ...)
option = #:arg-variances expr | #:extra-info stx
syntax
(define-internal-binding-type name-id option ...)
option = #:arr kindcon | #:arg-variances expr | #:extra-info stx
2.4 Type Operations
Note: The forms in this section are generated by (define-syntax-category type). See define-syntax-category for more information.
It’s not important to immediately understand all these definitions. Some, like type? and mk-type, are more "low-level" and are mainly used by the other forms. The most useful forms are probably define-typed-syntax, and the type-defining forms define-base-type, define-type-constructor, and define-binding-type.
parameter
(current-type-eval) → (-> syntax? syntax?)
(current-type-eval type-eval) → void? type-eval : (-> syntax? syntax?)
It defaults to full expansion, i.e., (lambda (stx) (local-expand stx 'expression null)), and also stores extra surface syntax information used for error reporting.
One should extend current-type-eval if canonicalization of types depends on combinations of different types, e.g., type lambdas and type application in F-omega.
procedure
(typecheck? τ1 τ2) → boolean?
τ1 : type? τ2 : type?
procedure
(typechecks? τs1 τs2) → boolean?
τs1 : (or/c (listof type?) (stx-listof type?)) τs2 : (or/c (listof type?) (stx-listof type?))
parameter
→ (-> type? type? immutable-bound-id-table? immutable-bound-id-table? boolean?) (current-type=? type-cmp) → void? type-cmp : (-> type? type? immutable-bound-id-table? immutable-bound-id-table? boolean?)
> (define-base-type Int) > (define-base-type String) > (begin-for-syntax (displayln (type=? #'Int #'Int))) #t
> (begin-for-syntax (displayln (type=? #'Int #'String))) #f
> (define-type-constructor → #:arity > 0) > (define-binding-type ∀ #:arity = 1 #:bvs = 1)
> (begin-for-syntax (displayln (type=? ((current-type-eval) #'(∀ (X) X)) ((current-type-eval) #'(∀ (Y) Y))))) #t
> (begin-for-syntax (displayln (type=? ((current-type-eval) #'(∀ (X) (∀ (Y) (→ X Y)))) ((current-type-eval) #'(∀ (Y) (∀ (X) (→ Y X))))))) #t
> (begin-for-syntax (displayln (type=? ((current-type-eval) #'(∀ (Y) (∀ (X) (→ Y X)))) ((current-type-eval) #'(∀ (X) (∀ (X) (→ X X))))))) #f
procedure
(same-types? τs) → boolean?
τs : (listof type?)
parameter
(current-type?) → (-> syntax? boolean?)
(current-type? type-pred) → void? type-pred : (-> syntax? boolean?)
parameter
(current-any-type?) → (-> syntax? boolean?)
(current-any-type? type-pred) → void? type-pred : (-> syntax? boolean?)
procedure
(assign-type e τ) → syntax?
e : syntax? τ : type?
syntax
2.5 Syntax Patterns
syntax
(~typecheck premise ...)
For example the pattern
(~typecheck [⊢ a ≫ a- ⇒ τ_a] [⊢ b ≫ b- ⇐ τ_a])
typechecks a and b, expecting b to have the type of a, and binding a- and b- to the expanded versions.
This is most useful in places where you want to do typechecking in something other than a type rule, like in a function or a syntax class.
> (begin-for-syntax (struct clause [stx- result-type]) ; f : Stx -> Clause (define (f stx) (syntax-parse stx [(~and [condition:expr body:expr] (~typecheck [⊢ condition ≫ condition- ⇐ Bool] [⊢ body ≫ body- ⇒ τ_body])) (clause #'[condition- body-] #'τ_body)])))
syntax
(~⊢ tc ...)
For example the pattern (~⊢ a ≫ a- ⇒ τ_a) typechecks a, binding the expanded version to a- and the type to τ_a.
> (begin-for-syntax (struct clause [stx- result-type]) ; f : Stx -> Clause (define (f stx) (syntax-parse stx [(~and [condition:expr body:expr] (~⊢ condition ≫ condition- ⇐ Bool) (~⊢ body ≫ body- ⇒ τ_body)) (clause #'[condition- body-] #'τ_body)])))
2.6 Syntax Classes
Note: The forms in this section are generated by (define-syntax-category type). See define-syntax-category for more information.
syntax class
syntax class
syntax class
syntax class
syntax class
2.7 require and provide-related Forms
syntax
(type-out ty-id ...)
Note: type-out is generated by (define-syntax-category type). See define-syntax-category for more information.
syntax
(typed-out x+ty+maybe-rename ...)
x+ty+maybe-rename = [x ty] | [x : ty] | [[x ty] out-x] | [[x : ty] out-x] x = identifier? out-x = identifier? ty = type?
Equivalent to a define-primop that automatically provides its definition.
syntax
(extends base-lang option ...)
option = #:except id ... | #:rename [old new] ...
syntax
(reuse name ... #:from base-lang)
name = id | [old new]
2.8 Suffixed Racket Bindings
To help avoid name conflicts, Turnstile re-provides all Racket bindings with a - suffix. These bindings are automatically used in some cases, e.g., define-primop, but in general are useful for avoiding name conflicts, particularly for commonly used macros like #%app.
2.9 #lang turnstile/lang
Deprecated: see #lang turnstile/quicklang.
2.10 #lang turnstile/quicklang
Languages implemented using #lang turnstile are responsible for defining and providing certain forms required by Racket, e.g., #%module-begin.
For convenience, Turnstile additionally supplies #lang turnstile/quicklang. Languages implemented using this language will automatically provide Racket’s #%module-begin, #%top-interaction, #%top, require, and some require transformers, e.g., rename-in.
2.11 #lang turnstile/base
Only provides racket/base at phase 1. Does not provide syntax/parse at phase 1.
In contrast, full turnstile provides all of racket and syntax/parse at phase 1.
turnstile/base may load faster than full turnstile (roughly 15% for an empty file, with Racket 7).
2.12 Lower-level Functions
This section describes lower-level functions and parameters. It’s usually not necessary to call these directly, since define-typed-syntax and other forms already do so, but some type systems may require extending some functionality.
procedure
(infer es [ #:ctx ctx #:tvctx tvctx #:tag tag #:stop-list? stop-list?]) → (list tvs xs es τs) es : (listof expr-stx) ctx : (listof binding-stx) = null tvctx : (listof tyvar-binding-stx) = null tag : symbol? = ': stop-list? : boolean? = #t
(infer (list #'(+ x 1)) #:ctx #'([x : Int]))
might return
(list #'() #'(x-) #'((#%plain-app + x- 1)) #'(Int)).
The context elements have the same shape as in define-typed-syntax. The contexts use let* semantics, where each binding is in scope for subsequent bindings.
Use the tag keyword argument to specify the key for the returned "type". The default key is :. For example, a programmer may want to specify a :: key when using infer to compute the kinds on types.
The #:stop-list? argument controls an optimization to avoid unnecessary re-expansion. When #t, expansion for typechecking stops once Racket kernel forms are reached. Note that because types are expected to be in fully expanded form, this flag should be #f when infer is used to infer kinds.
parameter
(current-use-stop-list? flag) → void? flag : boolean?
= #t
procedure
(subst τ x e [cmp]) → expr-stx
τ : syntax? x : identifier? e : syntax?
cmp : (-> identifier? identifier? boolean?) = bound-identifier=?
procedure
(substs τs xs e [cmp]) → expr-stx
τs : (listof syntax?) xs : (listof identifier?) e : syntax?
cmp : (-> identifier? identifier? boolean?) = bound-identifier=?
syntax
(type-error #:src srx-stx #:msg msg args ...)
> (begin-for-syntax (define t ((current-type-eval) #'(→ Int Int))) (displayln (type->str t))) (→ Int Int)
2.13 Subtyping
WARNING: very experimental
Types defined with define-type-constructor and define-binding-type may specify variance information and subtyping languages may use this information to help compute the subtype relation.
value
value
value
value
2.14 Modes
WARNING: experimental
(require turnstile/mode) | package: turnstile-lib |
Modes are typically used by the #:mode and #:submode keywords in define-typed-syntax (and related forms). When judgements are parameterized by a mode value, the parameter current-mode is set to that value for the extent of the sub-premises. Additionally, the function mode-setup-fn is called before setting current-mode, and the function mode-teardown-fn is called after current-mode is restored to its previous value.
User defined modes should be defined as structs that inherit from mode.
procedure
(make-mode [ #:setup setup-fn #:teardown teardown-fn]) → mode? setup-fn : (-> any) = void teardown-fn : (-> any) = void
parameter
(current-mode) → mode?
(current-mode mode) → void? mode : mode?
= (make-mode)
> (define-struct (my-mode mode) ())
> (define M (make-my-mode (λ () (displayln "M setup")) (λ () (displayln "M teardown"))))
> (with-mode M (displayln (current-mode)))
M setup
#<my-mode>
M teardown
procedure
(make-param-mode param value) → mode?
param : parameter? value : any/c
> (define current-scope (make-parameter 'outer))
> (with-mode (make-param-mode current-scope 'inner) (displayln (current-scope))) inner
2.15 Miscellaneous Syntax Object Functions
These are all phase 1 functions.
procedure
(stx-length stx) → exact-nonnegative-integer?
stx : syntax?
procedure
(stx-length=? stx1 stx2) → boolean?
stx1 : syntax? stx2 : syntax?
2.16 Special Identifiers
The following identifiers are bound (at phase 1) but are only valid within other Turnstile forms like define-typed-syntax.
syntax
syntax
syntax
syntax
syntax
2.17 Non-Unicode Identifiers
(require turnstile/no-unicode) | package: turnstile-lib |
The following identifiers are non-unicode aliases to other Turnstile ids.
syntax
syntax
syntax
syntax
syntax