diff --git a/collects/scheme/private/contract.ss b/collects/scheme/private/contract.ss index a9ea375649..3f452cfbad 100644 --- a/collects/scheme/private/contract.ss +++ b/collects/scheme/private/contract.ss @@ -1366,7 +1366,8 @@ improve method arity mismatch contract violation error messages? check-between/c check-unary-between/c - parameter/c) + parameter/c + hash/c) (define-syntax (flat-rec-contract stx) (syntax-case stx () @@ -2483,3 +2484,106 @@ improve method arity mismatch contract violation error messages? (contract-stronger? (parameter/c-ctc that) (parameter/c-ctc this))))) +(define (hash/c dom rng #:immutable [immutable 'dont-care]) + (unless (memq immutable '(#t #f dont-care)) + (error 'hash/c "expected #:immutable argument to be either #t, #f, or 'dont-care, got ~s" immutable)) + (cond + [(eq? immutable #t) + (make-immutable-hash/c (coerce-contract 'hash/c dom) + (coerce-contract 'hash/c rng))] + [else + (make-hash/c (coerce-flat-contract 'hash/c dom) + (coerce-flat-contract 'hash/c rng) + immutable)])) + +;; hash-test : hash/c -> any -> bool +(define (hash-test ctc) + (let ([dom-proc ((flat-get (hash/c-dom ctc)) (hash/c-dom ctc))] + [rng-proc ((flat-get (hash/c-rng ctc)) (hash/c-rng ctc))] + [immutable (hash/c-immutable ctc)]) + (λ (val) + (and (hash? val) + (case immutable + [(#t) (immutable? val)] + [(#f) (not (immutable? val))] + [(dont-care) #t]) + (let/ec k + (hash-for-each + val + (λ (dom rng) + (unless (dom-proc dom) (k #f)) + (unless (rng-proc rng) (k #f)))) + #t))))) + +(define-struct hash/c (dom rng immutable) + #:omit-define-syntaxes + + #:property flat-prop hash-test + #:property proj-prop + (λ (ctc) + (let ([dom-proc ((proj-get (hash/c-dom ctc)) (hash/c-dom ctc))] + [rng-proc ((proj-get (hash/c-rng ctc)) (hash/c-rng ctc))] + [immutable (hash/c-immutable ctc)]) + (λ (pos-blame neg-blame src-info orig-str) + (let ([partial-dom-contract (dom-proc pos-blame neg-blame src-info orig-str)] + [partial-rng-contract (rng-proc pos-blame neg-blame src-info orig-str)]) + (λ (val) + (unless (hash? val) + (raise-contract-error val src-info pos-blame orig-str + "expected a hash")) + (case immutable + [(#t) (unless (immutable? val) + (raise-contract-error val src-info pos-blame orig-str + "expected an immutable hash"))] + [(#f) (when (immutable? val) + (raise-contract-error val src-info pos-blame orig-str + "expected a mutable hash"))] + [(dont-care) (void)]) + + (hash-for-each + val + (λ (key val) + (partial-dom-contract key) + (partial-rng-contract val))) + + val))))) + + #:property name-prop (λ (ctc) (apply + build-compound-type-name + 'hash/c (hash/c-dom ctc) (hash/c-rng ctc) + (if (eq? 'dont-care (hash/c-immutable ctc)) + '() + (list '#:immutable (hash/c-immutable ctc))))) + #:property stronger-prop + (λ (this that) + #f)) + +(define-struct immutable-hash/c (dom rng) + #:omit-define-syntaxes + + #:property first-order-prop (λ (ctc) (λ (val) (and (hash? val) (immutable? val)))) + #:property proj-prop + (λ (ctc) + (let ([dom-proc ((proj-get (immutable-hash/c-dom ctc)) (immutable-hash/c-dom ctc))] + [rng-proc ((proj-get (immutable-hash/c-rng ctc)) (immutable-hash/c-rng ctc))]) + (λ (pos-blame neg-blame src-info orig-str) + (let ([partial-dom-contract (dom-proc pos-blame neg-blame src-info orig-str)] + [partial-rng-contract (rng-proc pos-blame neg-blame src-info orig-str)]) + (λ (val) + (unless (and (hash? val) + (immutable? val)) + (raise-contract-error val src-info pos-blame orig-str + "expected an immutable hash")) + (make-immutable-hash + (hash-map + val + (λ (k v) + (cons (partial-dom-contract k) + (partial-rng-contract v)))))))))) + + #:property name-prop (λ (ctc) (build-compound-type-name + 'hash/c (immutable-hash/c-dom ctc) (immutable-hash/c-rng ctc) + '#:immutable #t)) + #:property stronger-prop + (λ (this that) + #f)) diff --git a/collects/scribblings/reference/contracts.scrbl b/collects/scribblings/reference/contracts.scrbl index df92ee2ad1..652cdd21b2 100644 --- a/collects/scribblings/reference/contracts.scrbl +++ b/collects/scribblings/reference/contracts.scrbl @@ -301,6 +301,23 @@ type named by @scheme[struct-id], and whose field values match the Produces a contract on parameters whose values must match @scheme[contract].} +@defproc[(hash/c [key contract?] + [val contract?] + [#:immutable immutable (or/c #t #f 'dont-care) 'dont-care]) + contract?]{ +Produces a contract that recognizes @scheme[hash] tables with keys and values +as specified by the @scheme[key] and @scheme[val] arguments. + +If the @scheme[immutable] argument is @scheme[#f] or +@scheme['dont-care], then the resulting contract is a flat contract, +and the @scheme[key] and @scheme[val] arguments must also be flat +contracts. + +If @scheme[immtable] is @scheme[#t], then the other arguments do not +have to be flat contracts, the result is not a flat contract, and +checking this contract involves making a copy of the hash-table. +} + @defform[(flat-rec-contract id flat-contract-expr ...)] diff --git a/collects/tests/mzscheme/contract-test.ss b/collects/tests/mzscheme/contract-test.ss index d07859fa18..f239b5ec3a 100644 --- a/collects/tests/mzscheme/contract-test.ss +++ b/collects/tests/mzscheme/contract-test.ss @@ -2161,7 +2161,127 @@ 'pos 'neg))) +; +; +; +; +; +; +; ;; ;; ; +; ;; ;; ; +; ;;;;;; ;;;; ;;;;; ;;;;;; ;; ;;;; +; ;;;;;; ;; ;; ;; ;; ;;;;;; ; ;;;;;; +; ;; ;; ;;;; ;;;;; ;; ;; ; ;;; +; ;; ;; ;;; ;; ;;;; ;; ;; ; ;;; +; ;; ;; ;;; ;; ;; ;;; ;; ;; ;; ;;;;;; +; ;; ;; ;;;;;; ;;;;; ;; ;; ; ;;;; +; +; +; + (test/spec-passed + 'hash/c1 + '(contract (hash/c symbol? boolean?) + (make-hash) + 'pos + 'neg)) + + (test/spec-passed + 'hash/c2 + '(contract (hash/c symbol? boolean?) + (let ([h (make-hash)]) + (hash-set! h 'x #t) + h) + 'pos + 'neg)) + + (test/pos-blame + 'hash/c3 + '(contract (hash/c symbol? boolean?) + (let ([h (make-hash)]) + (hash-set! h 'x 'x) + h) + 'pos + 'neg)) + + (test/pos-blame + 'hash/c4 + '(contract (hash/c symbol? boolean?) + (let ([h (make-hash)]) + (hash-set! h #t #f) + h) + 'pos + 'neg)) + + (test/pos-blame + 'hash/c5 + '(contract (hash/c symbol? boolean? #:immutable #t) + (let ([h (make-hash)]) + (hash-set! h 'x #f) + h) + 'pos + 'neg)) + + (test/spec-passed + 'hash/c6 + '(contract (hash/c symbol? boolean? #:immutable #t) + (make-immutable-hash '((x . #f))) + 'pos + 'neg)) + + (test/spec-passed + 'hash/c7 + '(contract (hash/c symbol? boolean? #:immutable #f) + (let ([h (make-hash)]) + (hash-set! h 'x #f) + h) + 'pos + 'neg)) + + (test/pos-blame + 'hash/c8 + '(contract (hash/c symbol? boolean? #:immutable #f) + (make-immutable-hash '((x . #f))) + 'pos + 'neg)) + + (test/spec-passed + 'hash/c9 + '(contract (hash/c symbol? boolean? #:immutable 'dont-care) + (make-immutable-hash '((x . #f))) + 'pos + 'neg)) + + (test/spec-passed + 'hash/c10 + '(contract (hash/c symbol? boolean? #:immutable 'dont-care) + (let ([h (make-hash)]) + (hash-set! h 'x #f) + h) + 'pos + 'neg)) + + (test/spec-passed/result + 'hash/c11 + '(hash-ref (contract (hash/c symbol? number? #:immutable #t) + (make-immutable-hash '((x . 1))) + 'pos + 'neg) + 'x) + 1) + + (test/spec-passed/result + 'hash/c12 + '(hash-ref (contract (hash/c symbol? number?) + (let ([ht (make-hash)]) + (hash-set! ht 'x 1) + ht) + 'pos + 'neg) + 'x) + 1) + + ; ; ; @@ -5074,6 +5194,10 @@ so that propagation occurs. (define-struct s (a b)) (struct/c s any/c any/c))) + (ctest #t flat-contract? (hash/c any/c any/c #:immutable #f)) + (ctest #f flat-contract? (hash/c any/c any/c #:immutable #t)) + (ctest #t flat-contract? (hash/c any/c any/c)) + (ctest #t contract? 1) (ctest #t contract? (-> 1 1)) @@ -5170,6 +5294,23 @@ so that propagation occurs. even1) '(1 2 3 4) '(1 2 3)) + + (test-flat-contract '(hash/c symbol? boolean?) (make-hash) 1) + (test-flat-contract '(hash/c symbol? boolean?) + (let ([ht (make-hash)]) + (hash-set! ht 'x #t) + ht) + (let ([ht (make-hash)]) + (hash-set! ht 'x 1) + ht)) + (test-flat-contract '(hash/c symbol? boolean?) + (let ([ht (make-hash)]) + (hash-set! ht 'x #t) + ht) + (let ([ht (make-hash)]) + (hash-set! ht 'x 1) + ht)) + (test #t 'malformed-binder (with-handlers ((exn? exn:fail:syntax?)) (contract-eval '(flat-murec-contract ([(x) y]) x)) @@ -5397,6 +5538,11 @@ so that propagation occurs. (test-name '(parameter/c integer?) (parameter/c integer?)) + (test-name '(hash/c symbol? boolean?) (hash/c symbol? boolean?)) + (test-name '(hash/c symbol? boolean? #:immutable #t) (hash/c symbol? boolean? #:immutable #t)) + (test-name '(hash/c symbol? boolean? #:immutable #f) (hash/c symbol? boolean? #:immutable #f)) + (test-name '(hash/c symbol? boolean?) (hash/c symbol? boolean? #:immutable 'dont-care)) + (test-name '(box/c boolean?) (box/c boolean?)) (test-name '(box/c boolean?) (box/c (flat-contract boolean?))) (test-name 'the-name (flat-rec-contract the-name)) @@ -5703,6 +5849,18 @@ so that propagation occurs. (-> integer? integer?)) 1) + (ctest #t contract-first-order-passes? (hash/c any/c any/c) (make-hash)) + (ctest #f contract-first-order-passes? (hash/c any/c any/c) #f) + (ctest #f contract-first-order-passes? (hash/c symbol? boolean?) (let ([ht (make-hash)]) + (hash-set! ht 'x 1) + ht)) + (ctest #f contract-first-order-passes? (hash/c symbol? boolean?) (let ([ht (make-hash)]) + (hash-set! ht 1 #f) + ht)) + (ctest #t contract-first-order-passes? (hash/c symbol? boolean?) (let ([ht (make-hash)]) + (hash-set! ht 'x #t) + ht)) + (test-name '(or/c) (or/c)) (test-name '(or/c integer? gt0?) (or/c integer? (let ([gt0? (lambda (x) (> x 0))]) gt0?))) (test-name '(or/c integer? boolean?)