Contracts and Type Annotations

The :std/contract package provides facilities for contract checking and type annotations.

To use the bindings from this module:

(import :std/contract)

Macros

using

(using declaration body ...)
(using (declaration ...) body ...)

declaration:
 (var [expr] :~ predicate)  ; contract check with predicate
 (var [expr] : Type)        ; contract check or cast with type
 (var [expr] :- Type)       ; type assertion

 Type:
  struct identifier
  class identifier
  interface identifier

The macro expands the declarations and creates a block that evaluates the body with the following effects:

  • If the declaration is a predicate check with :~, the object identified by var will be checked to satisfy predicate. If the check fails, a ContractViolation will be raised.
  • If the declaration is a type contract with :, which can be a struct, class, or interface type, then:
    • for structs and classes, the object identified by var will be predicate checked.
    • for interfaces, the object identified by var will be cast to the interface.
  • If the declaration is a type assertion with :- then the relevant information will be propagated to the rest of the expansion and the compiler with an annotation.
  • Within the body ... syntactic context:
    • for structs, references to var.field will resolve to the relevant field accessor/mutator.
    • for classes, references to var.slot-or-field will resolve to the relevant slot or field accessor/mutator.
    • for interfaces, calls of the form (var.method ...) will dispatch to the relevant interface method:
      • If the declaration is a checked declaration with :, then the safe, contract checking facade procedure will be used.
      • If the declaration is a type assertion with :-, then the unchecked facade procedure will be used.
  • The form with the optional expression in the declaration expands to a let over using. So (using (var expr :~ contract) body ...) expands to (let (var expr) (using (var :~ contract) body ...)) and so on.

Example

Here is an example from the standard library:

(defstruct lru-cache (ht hd tl size cap))
(defstruct node (key val prev next))

(def (lru-cache-ref lru key (default absent-obj))
  (using (lru : lru-cache)
    (cond
     ((hash-get lru.ht key)
      => (lambda (n)
           (using (n :- node)
             (lru-cache-touch! lru n)
             n.val)))
     ((eq? default absent-obj)
      (raise-unbound-key lru-cache-ref lru key))
     (else default))))

(def (lru-cache-touch! lru n)
  (using ((lru :- lru-cache)
          (n :- node)
          (hd lru.hd :- node)
          (tl lru.tl :- node))
    (cond
     ((eq? n hd))
     ((eq? n tl)
      (using (prev n.prev :- node)
        (set! prev.next #f)
        (set! lru.tl prev)
        (set! n.next hd)
        (set! hd.prev n)
        (set! n.prev #f)
        (set! lru.hd n)))
     (else
      (using ((prev n.prev :- node)
              (next n.next :- node))
        (set! prev.next next)
        (set! next.prev prev)
        (set! n.next hd)
        (set! hd.prev n)
        (set! n.prev #f)
        (set! lru.hd n))))))

maybe

(maybe predicate) -> lambda (o) -> bool

Macro that creates a predicate that checks that the object is either #f or satisfies predicate.

in-range?

(in-range? start end) -> lambda (o) -> bool

Macro that creates a predicate that checks that

  • the object satisfies fixnum?
  • the fixnum is in the [start, end) range, exclusive for the right bound.

in-range-inclusive?

(in-range-inclusive? start end) -> lambda (o) -> bool

Macro that creates a predicate that checks that

  • the object satisfies fixnum?
  • the fixnum is in the [start, end] range, inclusive for the right bound.

nonnegative-fixnum?

(nonnegative-fixnum? o)

Macro version of the builtin nonnegative-fixnum? procedure