(asdf:load-system :cl-annot) (asdf:load-system :alexandria) (defpackage net.phorni.contract (:use :cl) (:export ;; annotation contract ;; condtion contract-error pre-contract-error post-contract-error ;; macro with-contract def/contract)) (in-package :net.phorni.contract) (define-condition contract-error (error) ((expr :reader contract-expr :initarg :expr)) (:report (lambda (condition stream) (format stream "~A: ~A" (type-of condition) (contract-expr condition))))) (define-condition pre-contract-error (contract-error) ()) (define-condition post-contract-error (contract-error) ()) ;; type-specifierの判別は処理系依存らしい (defun type-specifier-p (x) (or #+CCL (ccl:type-specifier-p x))) (defmacro contract-check (contract-type expr) (let ((gsym (gensym))) `(let ((,gsym ,expr)) (if ,gsym ,gsym (error ,contract-type :expr ',expr))))) (defun parse-contract-body (body) (loop :for expr in body :if (and (listp expr) (eq :pre (car expr))) :collect expr into pre :else :if (and (listp expr) (eq :post (car expr))) :collect expr into post :else :collect expr into parsed-body :finally (return (list pre post parsed-body)))) (defun pre-contract-expand (pre-list) `(progn ,@(loop :for pre in pre-list :collect `(contract-check 'pre-contract-error ,(second pre))))) (defun post-contract-expand (vars post-list) `(progn ,@(loop :for post in post-list :collect `(contract-check 'post-contract-error (apply (lambda ,(cadr post) ,@(cddr post)) ,vars))))) (defmacro with-contract (&body body) (destructuring-bind (pre post parsed-body) (parse-contract-body body) (let ((gtmp (gensym))) `(progn ,(pre-contract-expand pre) (let ((,gtmp (multiple-value-list (progn ,@parsed-body)))) ,(post-contract-expand gtmp post) (values-list ,gtmp)))))) (defun multiple-value-result-contract-spec? (x) (and (listp x) (eq :values (car x)) (every #'type-specifier-p (cdr x)))) (defun check-multiple-value-num (result num) (= (length result) num)) (defmacro def/contract ((&rest args-contract) result-contract orig-def) (dolist (a args-contract) (unless (and (listp a) (= (length a) 2) (symbolp (first a)) (type-specifier-p (second a))) (error "invalid pre-contract spec: ~A" a))) (when (and (not (multiple-value-result-contract-spec? result-contract)) (type-specifier-p result-contract)) (setf result-contract (list :values result-contract))) (unless (multiple-value-result-contract-spec? result-contract) (error "invalid post-contract spec: ~A" result-contract)) (let ((def (nth 0 orig-def)) (name (nth 1 orig-def)) (args (nth 2 orig-def)) (body (nthcdr 3 orig-def)) (gresult (gensym))) (multiple-value-bind (parsed-body declares doc) (alexandria:parse-body body :documentation t) `(,def ,name ,args ,doc ,@declares (with-contract ,@(loop :for a in args-contract :collect `(:pre (typep ,(first a) ',(second a)))) (:post (&rest ,gresult) (and (check-multiple-value-num ,gresult ,(length (cdr result-contract))) (every #'identity (mapcar #'typep ,gresult ',(cdr result-contract))))) ,@parsed-body))))) (cl-annot:defannotation contract (args result def) (:arity 3) `(def/contract ,args ,result ,def)) ;; ex (def/contract ((n (integer 0 *)) (m (integer 1 *))) number (defun my-div-1 (n m) (/ n m))) ;; (my-div-1 2 3) => 2/3 ;; (my-div-1 -1 1) => PRE-CONTRACT-ERROR: (TYPEP N '(INTEGER 0 *)) (cl-annot:enable-annot-syntax) @contract ((n (integer 0 *)) (m (integer 1 *))) number (defun my-div-2 (n m) (/ n m)) ;; (my-div-2 4 2) => 2 ;; (my-div-2 4 0) => PRE-CONTRACT-ERROR: (TYPEP M '(INTEGER 1 *))
2012年6月2日土曜日
[Common Lisp] cl-annotで契約による設計のようななにか
cl-annotのアノテーションを利用して、契約による設計(Design By Contrat)っぽいことを行ってみます。
登録:
コメントの投稿 (Atom)
0 件のコメント:
コメントを投稿