added struct-guard/c

This commit is contained in:
Robby Findler 2019-02-09 09:18:12 -06:00
parent 5c775fa04c
commit 4ed5d7d98b
7 changed files with 127 additions and 7 deletions

View File

@ -1,7 +1,8 @@
#lang scribble/doc #lang scribble/doc
@(require "mz.rkt") @(require "mz.rkt")
@(require (for-label syntax/modcollapse @(require (for-label syntax/modcollapse
racket/stxparam)) racket/stxparam
racket/serialize))
@(define contract-eval @(define contract-eval
(lambda () (lambda ()
@ -1785,6 +1786,11 @@ the original structure-type name does not act as a constructor.
If the @racket[#:omit-constructor] option is present, the constructor If the @racket[#:omit-constructor] option is present, the constructor
is not provided. is not provided.
Note that if the struct is created with @racket[serializable-struct]
or @racket[define-serializable-struct], @racket[contract-out] does not
protect struct instances that are created via
@racket[deserialize]. Consider using @racket[struct-guard/c] instead.
The @racket[#:∃], @racket[#:exists], @racket[#:∀], and @racket[#:forall] The @racket[#:∃], @racket[#:exists], @racket[#:∀], and @racket[#:forall]
clauses define new abstract contracts. The variables are bound in the clauses define new abstract contracts. The variables are bound in the
remainder of the @racket[contract-out] form to new contracts that hide remainder of the @racket[contract-out] form to new contracts that hide
@ -1842,6 +1848,19 @@ except that a @racket[_contract-expr] within @racket[provide/contract]
is evaluated at the position of the @racket[provide/contract] form is evaluated at the position of the @racket[provide/contract] form
instead of at the end of the enclosing module.} instead of at the end of the enclosing module.}
@defform[(struct-guard/c contract-expr ...)]{
Returns a procedure suitable to be passed as the @racket[#:guard]
argument to @racket[struct], @racket[serializable-struct] (and related forms).
The guard procedure ensures that each contract protects the
corresponding field values, as long as the struct is not mutated.
Mutations are not protected.
@examples[#:eval (contract-eval) #:once
(struct snake (weight hungry?)
#:guard (struct-guard/c real? boolean?))
(eval:error (snake 1.5 "yep"))]
}
@subsection{Nested Contract Boundaries} @subsection{Nested Contract Boundaries}
@defmodule*/no-declare[(racket/contract/region)] @defmodule*/no-declare[(racket/contract/region)]
@declare-exporting-ctc[racket/contract/region] @declare-exporting-ctc[racket/contract/region]

View File

@ -436,8 +436,8 @@ serializable with @racket[serialize]. This form is allowed only at
the top level or in a module's top level (so that deserialization the top level or in a module's top level (so that deserialization
information can be found later). information can be found later).
Serialization only supports cycles involving the created structure Serialization supports cycles involving the created structure
type when all fields are mutable (or when the cycle can be broken type only when all fields are mutable (or when the cycle can be broken
through some other mutable value). through some other mutable value).
In addition to the bindings generated by @racket[struct], In addition to the bindings generated by @racket[struct],
@ -456,6 +456,11 @@ information bound to
@racketidfont{deserialize-info:}@racket[_id]@racketidfont{-v0}. See @racketidfont{deserialize-info:}@racket[_id]@racketidfont{-v0}. See
@racket[make-deserialize-info] for more information. @racket[make-deserialize-info] for more information.
Beware that the previous paragraph means that if a serializable struct
is exported via @racket[contract-out], for example, the contracts are not
checked during deserialization. Consider using @racket[struct-guard/c]
instead.
The @racket[-v0] suffix on the deserialization enables future The @racket[-v0] suffix on the deserialization enables future
versioning on the structure type through versioning on the structure type through
@racket[serializable-struct/versions]. @racket[serializable-struct/versions].

View File

@ -3,7 +3,7 @@
(require "test-util.rkt") (require "test-util.rkt")
(parameterize ([current-contract-namespace (parameterize ([current-contract-namespace
(make-basic-contract-namespace)]) (make-basic-contract-namespace 'racket/contract/region)])
(test/spec-passed (test/spec-passed
'struct-type-prop.1 'struct-type-prop.1
'(let () '(let ()
@ -74,4 +74,33 @@
(define s3 (s list?)) (define s3 (s list?))
((prop-ref s3) 'apple))) ((prop-ref s3) 'apple)))
(test/spec-failed
'struct-guard/c.1
'(let ()
(with-contract pos
#:result any/c
(let ()
(struct s (x y) #:guard (struct-guard/c integer? boolean?))
(s 1 1))))
"(region pos)")
(test/spec-failed
'struct-guard/c.2
'(let ()
(with-contract pos
#:result any/c
(let ()
(struct s (x y) #:guard (struct-guard/c integer? boolean?))
(s #t #f))))
"(region pos)")
(test/spec-passed/result
'struct-guard/c.3
'(with-handlers ([exn:fail? (λ (x) (regexp-match? #rx"^the-name-of-the-struct, field 2:"
(exn-message x)))])
(struct the-name-of-the-struct (x y) #:guard (struct-guard/c integer? boolean?))
(the-name-of-the-struct 1 1))
#t)
) )

View File

@ -143,6 +143,7 @@
get/build-val-first-projection get/build-val-first-projection
suggest/c suggest/c
struct-guard/c
;; not documented.... (ie unintentional export) ;; not documented.... (ie unintentional export)
n->th) n->th)

View File

@ -71,7 +71,7 @@
(define (apply-contract c v pos neg name loc context-limit) (define (apply-contract c v pos neg name loc context-limit)
((make-apply-contract c pos neg name loc context-limit) v)) ((make-apply-contract c pos neg name loc context-limit) v))
(define (make-apply-contract c pos neg name loc context-limit) (define (make-apply-contract c pos neg name loc context-limit [backwards? #f])
(let ([c (coerce-contract 'contract c)]) (let ([c (coerce-contract 'contract c)])
(check-source-location! 'contract loc) (check-source-location! 'contract loc)
(define clnp (contract-late-neg-projection c)) (define clnp (contract-late-neg-projection c))

View File

@ -446,6 +446,10 @@
(define (blame/important-original? blme) (define (blame/important-original? blme)
(define i (blame-important blme)) (define i (blame-important blme))
(cond (cond
[(equal? (blame-positive blme) (blame-negative blme))
;; if the positive and negative parties are the same,
;; we never want to say "broke its own contract"
#f]
[i (equal? (important-sense-swapped? i) (blame-original? blme))] [i (equal? (important-sense-swapped? i) (blame-original? blme))]
[else (blame-original? blme)])) [else (blame-original? blme)]))

View File

@ -1,8 +1,12 @@
#lang racket/base #lang racket/base
(require "guts.rkt" (require "guts.rkt"
"blame.rkt" "blame.rkt"
"prop.rkt") "prop.rkt"
(provide (rename-out [struct-type-property/c* struct-type-property/c])) "base.rkt"
(for-syntax racket/base)
syntax/location)
(provide (rename-out [struct-type-property/c* struct-type-property/c])
struct-guard/c)
(define (get-stpc-late-neg-proj stpc) (define (get-stpc-late-neg-proj stpc)
(define get-late-neg-proj (define get-late-neg-proj
@ -47,3 +51,61 @@
(struct-type-property/c (struct-type-property/c
(coerce-contract 'struct-type-property/c value-contract)))]) (coerce-contract 'struct-type-property/c value-contract)))])
struct-type-property/c)) struct-type-property/c))
(define-syntax (struct-guard/c stx)
(syntax-case stx ()
[(_ . args)
#`(struct-guard/c/proc (quote-srcloc #,stx)
(current-contract-region)
. args)]))
(define (struct-guard/c/proc loc blame-party . ctc-args)
(define ctcs
(for/list ([arg (in-list ctc-args)]
[i (in-naturals)])
(define ctc (coerce-contract/f arg))
(unless ctc
(apply raise-argument-error
'struct-guard/c
"contract?"
i
ctc-args))
ctc))
;; don't want to depend on racket/list, so duplicate this
;; (plus we know that it will always be a non-empty list,
;; so skip some checks)
(define (last l)
(let loop ([l l])
(if (pair? (cdr l)) (loop (cdr l)) (car l))))
(define number-of-contracts (length ctcs))
;; would like to have this be specialized to the number of
;; arguments there actually are, but given the fact that
;; we're creating blame objects and projections after getting
;; the arguments it doesn't seem worth bothering for now
;; (we are creating the projections late because we don't
;; get the `name` until later on)
(λ args
(define name (last args))
(unless (= (length args) (+ number-of-contracts 1))
(error 'struct-guard/c
"given ~a contracts, but the struct ~s has ~a fields"
number-of-contracts
name
(- (length args) 1)))
(define ctc-projs
(for/list ([ctc (in-list ctcs)]
[i (in-naturals 1)])
(make-apply-contract ctc
blame-party blame-party
(if (= number-of-contracts 1)
name
(format "~a, field ~a" name i))
loc
#f)))
(apply values
(for/list ([arg (in-list args)]
[proj (in-list ctc-projs)])
(proj arg)))))