#lang racket/base (require (only-in '#%flfxnum flvector? fxvector?) (only-in '#%extfl extflonum? extflvector?)) (provide 2d-stx? check-datum) ;; Checks for 3D syntax (syntax that contains unwritable values, etc) (define INIT-FUEL #e1e6) ;; TO DO: ;; - extension via proc (any -> list/#f), ;; value considered good if result is list, all values in list are good ;; -- #| Some other predicates one might like to have: - would (read (write x)) succeed and be equal/similar to x? - would (datum->syntax #f x) succeed? - would (syntax->datum (datum->syntax #f x)) succeed and be equal/similar to x? - would (eval (read (write (compile `(quote ,x))))) succeed and be equal/similar to x? where equal/similar could mean one of the following: - equal?, which equates eg (vector 1 2 3) and (vector-immutable 1 2 3) - equal? relaxed to equate eg mutable and immutable hashes (but not prefabs) - equal? but also requiring same mutability at every point Some aux definitions: (define (rt x) (define-values (in out) (make-pipe)) (write x out) (close-output-port out) (read in)) (define (wrsd x) (define-values (in out) (make-pipe)) (write x out) (close-output-port out) (syntax->datum (read-syntax #f in))) (define (dsd x) (syntax->datum (datum->syntax #f x))) (define (evalc x) ;; mimics compiled zo-file constraints (eval (rt (compile `(quote ,x))))) How mutability behaves: - for vectors, boxes: - read always mutable - read-syntax always immutable - (dsd x) always immutable - (evalc x) always immutable - for hashes: - read always immutable - (dsd x) same as x - (evalc x) always immutable (!!!) - for prefab structs: - read same as x - read-syntax same as x - (dsd x) same as x - (evalc x) same as x Symbols - (dsd x) same as x - (evalc x) preserves interned, unreadable, makes new uninterned (loses eq-ness) Chaperones allow the lazy generation of infinite trees of data undetectable by eq?-based cycle detection. Might be helpful to have chaperone-eq? (not recursive, just chaperones of same object) and chaperone-eq?-hash-code, to use with make-custom-hash.) Impersonators allow the lazy generation of infinite trees of data, period. |# ;; ---- ;; 2d-stx? : any ... -> boolean ;; Would (write (compile `(quote-syntax ,x))) succeed? ;; If traverse-syntax? is #t, recurs into existing syntax ;; If traverse-syntax? is #f, assumes existing stxobjs are 2d, and only ;; checks if *new* 3d syntax would be created. (define (2d-stx? x #:traverse-syntax? [traverse-syntax? #t] #:irritant [irritant-box #f]) (check-datum x #:syntax-mode (if traverse-syntax? 'compound 'atomic) #:allow-impersonators? #f #:allow-mutable? 'no-hash/prefab #:allow-unreadable-symbols? #t #:allow-cycles? #t #:irritant irritant-box)) ;; ---- ;; check-datum : any ... -> boolean ;; where StxMode = (U 'atomic 'compound #f) ;; Returns nat if x is "good", #f if "bad" ;; If irritant-b is a box, the first bad subvalue found is put in the box. ;; If visited-t is a hash, it is used to detect cycles. (define (check-datum x #:syntax-mode [stx-mode #f] #:allow-impersonators? [allow-impersonators? #f] #:allow-mutable? [allow-mutable? #f] #:allow-unreadable-symbols? [allow-unreadable? #f] #:allow-cycles? [allow-cycles? #f] #:irritant [irritant-b #f]) ;; Try once with some fuel. If runs out of fuel, try again with cycle checking. (define (run fuel visited-t) (check* x fuel visited-t stx-mode allow-impersonators? allow-mutable? allow-unreadable? allow-cycles? irritant-b)) (let ([result (run INIT-FUEL #f)]) (cond [(not (equal? result 0)) ;; nat>0 or #f (and result #t)] [else ;; (eprintf "out of fuel, restarting\n") (and (run +inf.0 (make-hasheq)) #t)]))) ;; check* : any nat/+inf.0 StxMode boolean boolean boolean box -> nat/#f ;; Returns #f if bad, positive nat if good, 0 if ran out of fuel ;; If bad, places bad subvalue in irritant-b, if box (define (check* x0 fuel0 visited-t stx-mode allow-impersonators? allow-mutable? allow-unreadable? allow-cycles? irritant-b) (define no-mutable? (not allow-mutable?)) (define no-mutable-hash/prefab? (or no-mutable? (eq? allow-mutable? 'no-hash/prefab))) (define no-cycle? (not allow-cycles?)) (define no-impersonator? (not allow-impersonators?)) (define (loop x fuel) (if (and fuel (not (zero? fuel))) (loop* x fuel) fuel)) (define (loop* x fuel) (define (bad) (when irritant-b (set-box! irritant-b x)) #f) (define-syntax-rule (with-mutable-check mutable? body ...) ;; don't use for hash or prefab (cond [(and no-mutable? mutable?) (bad)] [else body ...])) (define-syntax-rule (with-cycle-check body ...) (cond [(and visited-t (hash-ref visited-t x #f)) => (lambda (status) (cond [(and no-cycle? (eq? status 'traversing)) (bad)] [else fuel]))] [else (when visited-t (hash-set! visited-t x 'traversing)) (begin0 (begin body ...) (when visited-t (hash-remove! visited-t x)))])) ;; (eprintf "-- checking ~s, fuel ~s\n" x fuel) (cond ;; Immutable compound [(and visited-t (list? x)) ;; space optimization: if list (finite), no need to store all cdr pairs in cycle table ;; don't do unless visited-t present, else expands fuel by arbitrary factors (with-cycle-check (for/fold ([fuel (sub1 fuel)]) ([e (in-list x)] #:break (not fuel)) (loop e fuel)))] [(pair? x) (with-cycle-check (let ([fuel (loop (car x) (sub1 fuel))]) (loop (cdr x) fuel)))] ;; Atomic [(or (null? x) (boolean? x) (number? x) (char? x) (keyword? x) (regexp? x) (byte-regexp? x) (extflonum? x)) fuel] [(symbol? x) (cond [(symbol-interned? x) fuel] [(symbol-unreadable? x) (if allow-unreadable? fuel (bad))] [else ;; uninterned (if (eq? allow-unreadable? #t) fuel (bad))])] ;; Mutable flat [(or (string? x) (bytes? x)) (with-mutable-check (not (immutable? x)) fuel)] [(or (fxvector? x) (flvector? x) (extflvector? x)) (with-mutable-check (not (immutable? x)) fuel)] ;; Syntax [(syntax? x) (case stx-mode ((atomic) fuel) ((compound) (loop (syntax-e x) fuel)) (else (bad)))] ;; Impersonators and chaperones [(and no-impersonator? (impersonator? x)) ;; else continue to chaperoned type (bad)] [(and no-impersonator? (chaperone? x)) ;; else continue to impersonated type (bad)] [else (with-cycle-check (cond ;; Mutable (maybe) compound [(vector? x) (with-mutable-check (not (immutable? x)) (for/fold ([fuel fuel]) ([e (in-vector x)] #:break (not fuel)) (loop e fuel)))] [(box? x) (with-mutable-check (not (immutable? x)) (loop (unbox x) (sub1 fuel)))] [(prefab-struct-key x) => (lambda (key) (cond [(and no-mutable-hash/prefab? (mutable-prefab-key? key)) (bad)] [else ;; traverse key, since contains arbitrary auto-value (let ([fuel (loop key fuel)]) (loop (struct->vector x) fuel))]))] [(hash? x) (cond [(and no-mutable-hash/prefab? (not (immutable? x))) (bad)] [else (for/fold ([fuel fuel]) ([(k v) (in-hash x)] #:break (not fuel)) (let ([fuel (loop k fuel)]) (loop v fuel)))])] ;; Bad [else (bad)]))])) (loop x0 fuel0)) ;; mutable-prefab-key? : prefab-key -> boolean (define (mutable-prefab-key? key) ;; A prefab-key is either ;; - symbol ;; - (list* symbol maybe-nat maybe-list maybe-vector prefab-key) ;; where mutable fields indicated by vector ;; This code is probably overly general; racket seems to normalize keys. (let loop ([k key]) (and (pair? k) (or (and (vector? (car k)) (positive? (vector-length (car k)))) (loop (cdr k))))))