(module kw '#%kernel (#%require "define.ss" "small-scheme.ss" "more-scheme.ss" (for-syntax '#%kernel "stx.ss" "small-scheme.ss" "stxcase-scheme.ss" "name.ss" "norm-define.ss" "qqstx.ss" "sort.ss")) (#%provide new-lambda new-λ new-define new-app (rename *make-keyword-procedure make-keyword-procedure) keyword-apply procedure-keywords procedure-reduce-keyword-arity new-prop:procedure) ;; ---------------------------------------- (define-values (struct:keyword-procedure make-keyword-procedure keyword-procedure? keyword-procedure-ref keyword-procedure-set!) (make-struct-type 'keyword-procedure #f 4 0 #f (list (cons prop:checked-procedure #t)) (current-inspector) #f '(0 1 2 3))) (define keyword-procedure-checker (make-struct-field-accessor keyword-procedure-ref 0)) (define keyword-procedure-proc (make-struct-field-accessor keyword-procedure-ref 1)) (define keyword-procedure-required (make-struct-field-accessor keyword-procedure-ref 2)) (define keyword-procedure-allowed (make-struct-field-accessor keyword-procedure-ref 3)) (define-values (struct:keyword-method make-km keyword-method? km-ref km-set!) (make-struct-type 'procedure struct:keyword-procedure 0 0 #f)) (define (generate-arity-string proc) (let-values ([(req allowed) (procedure-keywords proc)] [(a) (procedure-arity proc)] [(keywords-desc) (lambda (opt req) (format "~a with keyword~a~a" (if (null? (cdr req)) (format "an ~aargument" opt) (format "~aarguments" opt)) (if (null? (cdr req)) "" "s") (case (length req) [(1) (format " ~a" (car req))] [(2) (format " ~a and ~a" (car req) (cadr req))] [else (let loop ([req req]) (if (null? (cdr req)) (format " and ~a" (car req)) (format " ~a,~a" (car req) (loop (cdr req)))))])))] [(method-adjust) (lambda (a) (if (or (okm? proc) (keyword-method? proc)) (if (zero? a) 0 (sub1 a)) a))]) (string-append (cond [(number? a) (let ([a (method-adjust a)]) (format "~a argument~a" a (if (= a 1) "" "s")))] [(arity-at-least? a) (let ([a (method-adjust (arity-at-least-value a))]) (format "at least ~a argument~a" a (if (= a 1) "" "s")))] [else "a different number of arguments"]) (if (null? req) "" (format " plus ~a" (keywords-desc "" req))) (if allowed (let ([others (let loop ([req req][allowed allowed]) (cond [(null? req) allowed] [(eq? (car req) (car allowed)) (loop (cdr req) (cdr allowed))] [else (cons (car allowed) (loop req (cdr allowed)))]))]) (if (null? others) "" (format " plus ~a" (keywords-desc "optional " others)))) " plus arbitrary keyword arguments")))) ;; Constructor for a procedure with only optional keywords. ;; The `procedure' property dispatches to a procedure in the ;; struct (which has exactly the right arity). (define-values (struct:okp make-optional-keyword-procedure okp? okp-ref okp-set!) (make-struct-type 'procedure struct:keyword-procedure 1 0 #f (list (cons prop:arity-string generate-arity-string)) (current-inspector) 0)) ;; A ``method'' (for arity reporting) (define-values (struct:okm make-optional-keyword-method okm? okm-ref okm-set!) (make-struct-type 'procedure struct:okp 0 0 #f)) (define-values (prop:named-keyword-procedure named-keyword-procedure? keyword-procedure-name) (make-struct-type-property 'named-keyword-procedure)) ;; Constructor generator for a procedure with a required keyword. ;; (This is used with lift-expression, so that the same constructor ;; is used for each evaluation of a keyword lambda.) ;; The `procedure' property is a per-type method that has exactly ;; the right arity, and that sends all arguments to `missing-kw'. (define (make-required name fail-proc method?) (let-values ([(s: mk ? -ref -set!) (make-struct-type (string->symbol (format "procedure:~a" name)) (if method? struct:keyword-method struct:keyword-procedure) 0 0 #f (list (cons prop:arity-string generate-arity-string) (cons prop:named-keyword-procedure name)) (current-inspector) fail-proc)]) mk)) ;; Allows keyword application to see into a "method"-style procedure attribute: (define-values (new-prop:procedure new-procedure? new-procedure-ref) (make-struct-type-property 'procedure #f (list (cons prop:procedure values)))) ;; ---------------------------------------- (define *make-keyword-procedure (letrec ([make-keyword-procedure (case-lambda [(proc) (make-keyword-procedure proc (lambda args (apply proc null null args)))] [(proc plain-proc) (make-optional-keyword-procedure (make-keyword-checker null #f (procedure-arity proc)) proc null #f plain-proc)])]) make-keyword-procedure)) (define (keyword-apply proc kws kw-vals . normal-argss) (let ([type-error (lambda (what which) (apply raise-type-error 'keyword-apply what which proc kws kw-vals normal-argss))]) (unless (procedure? proc) (type-error "procedure" 0)) (let loop ([ks kws]) (cond [(null? ks) (void)] [(or (not (pair? ks)) (not (keyword? (car ks)))) (type-error "list of keywords" 1)] [(null? (cdr ks)) (void)] [(or (not (pair? (cdr ks))) (not (keyword? (cadr ks)))) (loop (cdr ks))] [(keywordlist #'(id ... . rest)))]) (when dup-id (raise-syntax-error #f "duplicate argument identifier" stx dup-id))) (let* ([kws (syntax->list #'(kw ...))] [opts (syntax->list #'(opt-id ...))] [ids (syntax->list #'(id ...))] [plain-ids (syntax->list #'(plain-id ...))] [kw-reqs (syntax->list #'(kw-req ...))] [kw-args (generate-temporaries kws)] ; to hold supplied value [kw-arg?s (generate-temporaries kws)] ; to indicated whether it was supplied [opt-args (generate-temporaries opts)] ; supplied value [opt-arg?s (generate-temporaries opts)] ; whether supplied [needed-kws (sort (syntax->list #'need-kw) (lambda (a b) (keyword= . min)] [(_ v min max) (if (equal? (syntax-e #'min) (syntax-e #'max)) #'(= v min) #'(and (v . >= . min) (v . <= . max)))])) (define-syntax (subset?/static stx) (syntax-case stx (quote) [(_ l1-expr '()) #'(null? l1-expr)] [(_ '() l2-expr) #'#t] [(_ l1-expr '(kw . kws)) #'(let ([l1 l1-expr]) (let ([l1 (if (null? l1) l1 (if (eq? (car l1) 'kw) (cdr l1) l1))]) (subset?/static l1 'kws)))] [(_ l1-expr l2-expr) #'(subset? l1-expr l2-expr)])) (define-syntax (subsets?/static stx) (syntax-case stx (quote) [(_ '() l2-expr l3-expr) #'(subset?/static l2-expr l3-expr)] [(_ l1-expr l2-expr '()) #'(subset?/static l1-expr l2-expr)] [(_ 'l1-elems l2-expr 'l3-elems) (if (equal? (map syntax-e (syntax->list #'l1-elems)) (map syntax-e (syntax->list #'l3-elems))) ;; l2 must be equal to l1/l3: #'(equal?/static 'l1-elems l2-expr) #'(subsets? 'l1-elems l2-expr 'l3-elems))])) (define-syntax (equal?/static stx) ;; Unroll loop at expansion time (syntax-case stx (quote) [(_ '() l2-expr) #'(null? l2-expr)] [(_ '(kw . kw-rest) l2-expr) #'(let ([l2 l2-expr]) (and (pair? l2) (eq? (car l2) 'kw) (equal?/static 'kw-rest (cdr l2))))])) ;; ---------------------------------------- ;; `define' with keyword arguments (define-syntax (new-define stx) (let-values ([(id rhs) (normalize-definition stx #'new-lambda #t #t)]) (quasisyntax/loc stx (define #,id #,rhs)))) ;; ---------------------------------------- ;; `#%app' with keyword arguments (define-syntax (new-app stx) (let ([l (syntax->list stx)]) (if (not (and l (pair? (cdr l)) (not (keyword? (syntax-e (cadr l)))) (ormap (lambda (x) (keyword? (syntax-e x))) l))) ;; simple or erroneous app: (if (identifier? stx) (raise-syntax-error #f "illegal use" stx) (if (and (pair? l) (null? (cdr l))) (raise-syntax-error #f "missing procedure expression; probably originally (), which is an illegal empty application" stx) (quasisyntax/loc stx (#%app . #,(cdr (syntax-e stx)))))) ;; keyword app (maybe) (let ([exprs (let ([kw-ht (make-hasheq)]) (let loop ([l (cddr l)]) (cond [(null? l) null] [(keyword? (syntax-e (car l))) (when (hash-ref kw-ht (syntax-e (car l)) #f) (raise-syntax-error 'application "duplicate keyword in application" stx (car l))) (hash-set! kw-ht (syntax-e (car l)) #t) (cond [(null? (cdr l)) (raise-syntax-error 'application "missing argument expression after keyword" stx (car l))] [(keyword? (cadr l)) (raise-syntax-error 'application "keyword in expression possition (immediately after another keyword)" stx (cadr l))] [else (cons (cadr l) (loop (cddr l)))])] [else (cons (car l) (loop (cdr l)))])))]) (let ([ids (cons (or (syntax-local-infer-name stx) 'procedure) (generate-temporaries exprs))]) (let loop ([l (cdr l)] [ids ids] [bind-accum null] [arg-accum null] [kw-pairs null]) (cond [(null? l) (let* ([args (reverse arg-accum)] [sorted-kws (sort kw-pairs (lambda (a b) (keyword all keywords are allowed (loop (cdr kws) required #f)] [(pair? allowed) (if (eq? (car allowed) (car kws)) (loop (cdr kws) required (cdr allowed)) (loop kws required (cdr allowed)))] [else (values #f (car kws))]))) ;; Generates a keyword an arity checker dynamically: (define (make-keyword-checker req-kws allowed-kws arity) ;; If min-args is #f, then max-args is an arity value. ;; If max-args is #f, then >= min-args is accepted. (define-syntax (arity-check-lambda stx) (syntax-case stx () [(_ (kws) kw-body) #'(cond [(integer? arity) (lambda (kws a) (and kw-body (= a arity)))] [(arity-at-least? arity) (let ([arity (arity-at-least-value arity)]) (lambda (kws a) (and kw-body (a . >= . arity))))] [else (lambda (kws a) (and kw-body (arity-includes? arity a)))])])) (cond [(not allowed-kws) ;; All allowed (cond [(null? req-kws) ;; None required (arity-check-lambda (kws) #t)] [else (arity-check-lambda (kws) (subset? req-kws kws))])] [(null? allowed-kws) ;; None allowed (arity-check-lambda (kws) (null? kws))] [else (cond [(null? req-kws) ;; None required, just allowed (arity-check-lambda (kws) (subset? kws allowed-kws))] [else ;; Some required, some allowed (if (equal? req-kws allowed-kws) (arity-check-lambda (kws) ;; All allowed are required, so check equality (let loop ([kws kws][req-kws req-kws]) (if (null? req-kws) (null? kws) (and (eq? (car kws) (car req-kws)) (loop (cdr kws) (cdr req-kws)))))) (arity-check-lambda (kws) ;; Required is a subset of allowed (subsets? req-kws kws allowed-kws)))])])) (define (arity-includes? arity a) (cond [(integer? arity) (= arity a)] [(arity-at-least? arity) (a . >= . (arity-at-least-value a))] [else (ormap (lambda (ar) (arity-includes? ar a)) arity)])) (define (subset? l1 l2) ;; l1 and l2 are sorted (cond [(null? l1) #t] [(null? l2) #f] [(eq? (car l1) (car l2)) (subset? (cdr l1) (cdr l2))] [else (subset? l1 (cdr l2))])) (define (subsets? l1 l2 l3) ;; l1, l2, and l3 are sorted, and l1 is a subset of l3 (cond [(null? l1) (subset? l2 l3)] [(null? l2) #f] [(null? l3) #f] [else (let ([v2 (car l2)]) (cond [(eq? (car l1) v2) (subsets? (cdr l1) (cdr l2) (cdr l3))] [(eq? v2 (car l3)) (subsets? l1 (cdr l2) (cdr l3))] [else (subsets? l1 l2 (cdr l3))]))])) ;; Extracts the procedure using the keyword-argument protocol. ;; If `p' doesn't accept keywords, make up a procedure that ;; reports an error. (define (keyword-procedure-extract/method kws n p method-n) (if (and (keyword-procedure? p) ((keyword-procedure-checker p) kws n)) ;; Ok: (keyword-procedure-proc p) ;; Not ok, so far: (let ([p2 (and (not (keyword-procedure? p)) (procedure? p) (or (procedure-extract-target p) (and (new-procedure? p) 'method)))]) (if p2 ;; Maybe the target is ok: (if (eq? p2 'method) ;; Build wrapper method: (let ([p3 (keyword-procedure-extract/method kws (add1 n) (new-procedure-ref p) (add1 method-n))]) (lambda (kws kw-args . args) (apply p3 kws kw-args (cons p args)))) ;; Recur: (keyword-procedure-extract/method kws n p2 method-n)) ;; Not ok, period: (lambda (kws kw-args . args) (define-values (missing-kw extra-kw) (if (keyword-procedure? p) (check-kw-args p kws) (values #f (car kws)))) (let ([n (let ([method-n (+ method-n (if (or (keyword-method? p) (okm? p)) 1 0))]) (if (n . >= . method-n) (- n method-n) n))] [args-str (if (and (null? args) (null? kws)) "no arguments supplied" ;; Hack to format arguments: (with-handlers ([exn:fail? (lambda (exn) ;; the message can end with: ;; ..., given: x; given 117 arguments total ;; ..., given: x; other arguments were: 1 2 3 (regexp-replace #rx"^.*? given: x; (other )?" (exn-message exn) ""))]) (apply raise-type-error 'x "x" 0 'x (append args (apply append (map list kws kw-args))))))] [proc-name (lambda (p) (or (and (named-keyword-procedure? p) (keyword-procedure-name p)) (object-name p) p))]) (raise (make-exn:fail:contract (if extra-kw (if (keyword-procedure? p) (format (string-append "~a: does not expect an argument with keyword ~a; ~a") (proc-name p) extra-kw args-str) (format (string-append "~a: does not accept keyword arguments; ~a") (proc-name p) args-str)) (if missing-kw (format (string-append "~a: requires an argument with keyword ~a, not supplied; ~a") (proc-name p) missing-kw args-str) (format (string-append "~a: no case matching ~a non-keyword" " argument~a; ~a") (proc-name p) (- n 2) (if (= 1 (- n 2)) "" "s") args-str))) (current-continuation-marks))))))))) (define (keyword-procedure-extract p kws n) (keyword-procedure-extract/method kws n p 0)) ;; setting procedure arity (define (procedure-reduce-keyword-arity proc arity req-kw allowed-kw) (let ([plain-proc (procedure-reduce-arity (if (okp? proc) (okp-ref proc 0) proc) arity)]) (define (sorted? kws) (let loop ([kws kws]) (cond [(null? kws) #t] [(null? (cdr kws)) #t] [(keyword