diff --git a/pkgs/racket-pkgs/racket-doc/scribblings/reference/contracts.scrbl b/pkgs/racket-pkgs/racket-doc/scribblings/reference/contracts.scrbl index ebf3970c16..259fb7c6b8 100644 --- a/pkgs/racket-pkgs/racket-doc/scribblings/reference/contracts.scrbl +++ b/pkgs/racket-pkgs/racket-doc/scribblings/reference/contracts.scrbl @@ -1174,8 +1174,8 @@ earlier fields.}} #:literals (struct rename) (contract-out p/c-item ...) ([p/c-item - (struct id ((id contract-expr) ...)) - (struct (id identifier) ((id contract-expr) ...)) + (struct id ((id contract-expr) ...) struct-option) + (struct (id identifier) ((id contract-expr) ...) struct-option) (rename orig-id id contract-expr) (id contract-expr) (code:line #:∃ poly-variables) @@ -1183,7 +1183,9 @@ earlier fields.}} (code:line #:∀ poly-variables) (code:line #:forall poly-variables)] [poly-variables identifier - (identifier ...)])]{ + (identifier ...)] + [struct-option (code:line) + #:omit-constructor])]{ A @racket[_provide-spec] for use in @racket[provide] (currently only for the same @tech{phase level} as the @racket[provide] form; for example, @@ -1222,6 +1224,8 @@ parent are only used in the contract for the sub-struct's constructor, and the selector or mutators for the super-struct are not provided. The exported structure-type name always doubles as a constructor, even if the original structure-type name does not act as a constructor. +If the @racket[#:omit-constructor] option is present, the constructor +is not provided. The @racket[#:∃], @racket[#:exists], @racket[#:∀], and @racket[#:forall] clauses define new abstract contracts. The variables are bound in the diff --git a/pkgs/racket-pkgs/racket-test/tests/racket/contract/contract-out.rkt b/pkgs/racket-pkgs/racket-test/tests/racket/contract/contract-out.rkt index 346807bac8..eec9e457c4 100644 --- a/pkgs/racket-pkgs/racket-test/tests/racket/contract/contract-out.rkt +++ b/pkgs/racket-pkgs/racket-test/tests/racket/contract/contract-out.rkt @@ -871,6 +871,49 @@ (eval '(require 'provide/contract44-m2)))) + (test/spec-passed/result + 'provide/contract45 + '(begin + (eval '(module provide/contract45-m1 racket/base + (require racket/contract) + (struct heap (v) #:transparent) + (provide + (contract-out + (struct heap ([v integer?]) #:omit-constructor))) + (define a-heap (heap 11)) + (provide a-heap))) + + (eval '(module provide/contract45-m2 racket/base + (require racket/contract 'provide/contract45-m1) + (define provide/contract45-x (heap-v a-heap)) + (provide provide/contract45-x))) + + (eval '(require 'provide/contract45-m2)) + (eval 'provide/contract45-x)) + 11) + + (test/spec-passed/result + 'provide/contract46 + '(begin + (eval '(module provide/contract46-m1 racket/base + (require racket/contract/base) + (provide + (contract-out + (struct s ([x any/c]) #:omit-constructor))) + (struct s (x)) + (define an-s (s 123)) + (provide an-s))) + (eval '(module provide/contract46-m2 racket/base + (require 'provide/contract46-m1 + racket/match) + (define provide/contract46-x + (match an-s + [(s x) x])) + (provide provide/contract46-x))) + (eval '(require 'provide/contract46-m2)) + (eval 'provide/contract46-x)) + 123) + (contract-error-test 'contract-error-test8 #'(begin @@ -1019,6 +1062,36 @@ ;; testing that the error says "contract-out" and not "provide/contract" (regexp-match #rx"contract-out: found 2 fields" (exn-message x))))) + (contract-error-test + 'contract-error-test19 + #'(begin + (eval '(module pce19-bug racket + (struct point (x y)) + (provide (contract-out + (struct point ([x integer?] [y integer?]) + #:omit-constructor))))) + (eval '(module pce19-b racket + (require 'pce19-bug) + make-point))) + (λ (x) + (and (exn:fail:syntax? x) + (regexp-match #rx"unbound identifier .* make-point" (exn-message x))))) + + (contract-error-test + 'contract-error-test20 + #'(begin + (eval '(module pce20-bug racket + (struct point (x y)) + (provide (contract-out + (struct point ([x integer?] [y integer?]) + #:omit-constructor))))) + (eval '(module pce20-b racket + (require 'pce20-bug) + point))) + (λ (x) + (and (exn:fail:syntax? x) + (regexp-match #rx"point: .* cannot be used as an expression" (exn-message x))))) + (contract-eval `(,test 'pos @@ -1101,8 +1174,6 @@ (eval 'contract-out-rename1-my-f)) 11) - - (contract-eval '(module contract-test-suite-inferred-name1 racket/base (require racket/contract) @@ -1132,4 +1203,4 @@ (test 'contract-inferred-name-test2b object-name (contract-eval 'contract-inferred-name-test2b)) (test 'contract-inferred-name-test3 object-name (contract-eval 'contract-inferred-name-test3)) (test 'contract-inferred-name-test4 object-name (contract-eval 'contract-inferred-name-test4)) - (test 'contract-inferred-name-test5 object-name (contract-eval 'contract-inferred-name-test5))) \ No newline at end of file + (test 'contract-inferred-name-test5 object-name (contract-eval 'contract-inferred-name-test5))) diff --git a/racket/collects/racket/contract/private/provide.rkt b/racket/collects/racket/contract/private/provide.rkt index 49a85e1c74..df1dab4ece 100644 --- a/racket/collects/racket/contract/private/provide.rkt +++ b/racket/collects/racket/contract/private/provide.rkt @@ -188,8 +188,8 @@ clause)] [else (syntax-case (cadr clauses) () - [x - (identifier? #'x) + [x + (identifier? #'x) (if just-check-errors? (loop (cddr clauses) exists-binders) (with-syntax ([(x-gen) (generate-temporaries #'(x))]) @@ -245,18 +245,27 @@ (syntax this-name))] [(rename . _) (raise-syntax-error who "malformed rename clause" provide-stx clause)] - [(struct struct-name ((field-name contract) ...)) + [(struct struct-name ((field-name contract) ...) options ...) (and (well-formed-struct-name? (syntax struct-name)) (andmap identifier? (syntax->list (syntax (field-name ...))))) - (begin + (let () + (for ([option (in-list (syntax->list #'(options ...)))]) + (unless (member (syntax-e option) '(#:omit-constructor)) + (raise-syntax-error who + "malformed struct option" + provide-stx + option))) (add-to-dups-table #'struct-name) + (define omit-constructor? + (member '#:omit-constructor (map syntax-e (syntax->list #'(options ...))))) (if just-check-errors? (loop (cdr clauses) exists-binders) (let ([sc (build-struct-code provide-stx (syntax struct-name) (syntax->list (syntax (field-name ...))) (map (λ (x) (add-exists-binders x exists-binders)) - (syntax->list (syntax (contract ...)))))]) + (syntax->list (syntax (contract ...)))) + omit-constructor?)]) (cons sc (loop (cdr clauses) exists-binders)))))] [(struct name) (identifier? (syntax name)) @@ -270,26 +279,25 @@ "name must be an identifier or two identifiers with parens around them" provide-stx (syntax name))] - [(struct name (fields ...)) - (for-each (λ (field) - (syntax-case field () - [(x y) - (identifier? (syntax x)) - (void)] - [(x y) - (raise-syntax-error who - "malformed struct field, expected identifier" - provide-stx - (syntax x))] - [else - (raise-syntax-error who - "malformed struct field" - provide-stx - field)])) - (syntax->list (syntax (fields ...)))) - - ;; if we didn't find a bad field something is wrong! - (raise-syntax-error who "internal error.1" provide-stx clause)] + [(struct name (fields ...) options ...) + (let () + (for ([field [in-list (syntax->list (syntax (fields ...)))]]) + (syntax-case field () + [(x y) + (identifier? (syntax x)) + (void)] + [(x y) + (raise-syntax-error who + "malformed struct field, expected identifier" + provide-stx + (syntax x))] + [else + (raise-syntax-error who + "malformed struct field" + provide-stx + field)])) + ;; if we didn't find a bad field something is wrong! + (raise-syntax-error who "internal error.1" provide-stx clause))] [(struct name . fields) (raise-syntax-error who "malformed struct fields" @@ -331,7 +339,8 @@ ;; build-struct-code : syntax syntax (listof syntax) (listof syntax) -> syntax ;; constructs the code for a struct clause ;; first arg is the original syntax object, for source locations - (define (build-struct-code stx struct-name-position field-names field-contracts) + (define (build-struct-code stx struct-name-position field-names field-contracts + omit-constructor?) (let* ([struct-name (syntax-case struct-name-position () [(a b) (syntax a)] [else struct-name-position])] @@ -372,12 +381,13 @@ field-names field-contracts)] [struct:struct-name - (datum->syntax - struct-name - (string->symbol - (string-append - "struct:" - (symbol->string (syntax-e struct-name)))))] + (or (list-ref the-struct-info 0) + (datum->syntax + struct-name + (string->symbol + (string-append + "struct:" + (symbol->string (syntax-e struct-name))))))] [-struct:struct-name (datum->syntax @@ -508,15 +518,17 @@ [(predicate-code predicate-new-name) (code-for-one-id/new-name stx predicate-id #f (syntax predicate/c) #f)] [(constructor-code constructor-new-name) - (code-for-one-id/new-name - stx - chaperone-constructor-id struct-name - (build-constructor-contract stx - field-contract-ids - predicate-id) - constructor-id - #t - (not type-is-only-constructor?))] + (if omit-constructor? + #'((void) (void)) + (code-for-one-id/new-name + stx + chaperone-constructor-id struct-name + (build-constructor-contract stx + field-contract-ids + predicate-id) + constructor-id + #t + (not type-is-only-constructor?)))] [(field-contract-id-definitions ...) (filter values (map (λ (field-contract-id field-contract) @@ -555,25 +567,26 @@ [else #f])) (syntax->list #'(mutator-codes/mutator-new-names ...)))] [(exported-selector-ids ...) (reverse selector-ids)]) + (define proc + #`(λ () + (list (quote-syntax -struct:struct-name) + #,(if type-is-only-constructor? + #'(quote-syntax id-rename) + #'(quote-syntax constructor-new-name)) + (quote-syntax predicate-new-name) + (list (quote-syntax rev-selector-new-names) ... + (quote-syntax rev-selector-old-names) ...) + (list mutator-id-info ...) + super-id))) #`(begin (provide (rename-out [id-rename struct-name])) (define-syntax id-rename - #,(let ([proc - #`(lambda () - (list (quote-syntax -struct:struct-name) - #,(if type-is-only-constructor? - #'(quote-syntax id-rename) - #'(quote-syntax constructor-new-name)) - (quote-syntax predicate-new-name) - (list (quote-syntax rev-selector-new-names) ... - (quote-syntax rev-selector-old-names) ...) - (list mutator-id-info ...) - super-id))]) - (if type-is-constructor? - #`(make-applicable-struct-info #,proc - (lambda () - (quote-syntax constructor-new-name))) - #`(make-struct-info #,proc))))))] + #,(if (and type-is-constructor? (not omit-constructor?)) + #`(make-applicable-struct-info + #,proc + (lambda () + (quote-syntax constructor-new-name))) + #`(make-struct-info #,proc)))))] [struct:struct-name struct:struct-name] [-struct:struct-name -struct:struct-name] [struct-name struct-name] @@ -633,7 +646,8 @@ ;; get-field-counts/struct-names : syntax syntax -> (listof (cons number symbol)) ;; returns a list of numbers corresponding to the numbers of fields for each of the parent structs (define (get-field-counts/struct-names struct-name provide-stx) - (let loop ([parent-info-id struct-name]) + (let loop ([parent-info-id struct-name] + [orig-struct? #t]) (let ([parent-info (and (identifier? parent-info-id) (a:lookup-struct-info parent-info-id provide-stx))]) @@ -647,12 +661,13 @@ (not (last fields))) (raise-syntax-error who - "cannot determine the number of fields in super struct" + (format "cannot determine the number of fields in ~astruct" + (if orig-struct? "" "parent ")) provide-stx struct-name)] [else (cons (cons (length fields) (predicate->struct-name provide-stx predicate)) - (loop (list-ref parent-info 5)))]))])))) + (loop (list-ref parent-info 5) #f))]))])))) (define (predicate->struct-name orig-stx stx) (and stx @@ -822,10 +837,10 @@ [else (for ([clause (in-list p/c-clauses)]) (syntax-case* clause (struct) (λ (x y) (eq? (syntax-e x) (syntax-e y))) - [(struct a ((fld ctc) ...)) + [(struct a ((fld ctc) ...) options ...) (identifier? #'a) (add-struct-clause-to-struct-id-mapping #'a #f #'(fld ...))] - [(struct (a b) ((fld ctc) ...)) + [(struct (a b) ((fld ctc) ...) options ...) (add-struct-clause-to-struct-id-mapping #'a #'b #'(fld ...))] [_ (void)])) (with-syntax ([(bodies ...) (code-for-each-clause p/c-clauses)])