cl-ds / data /repos /cl-reader /reader.lisp
j14i's picture
3375 CL macro transformation examples from 85 libraries
43203b4 verified
(defpackage :reader
(:use :cl :iterate :split-sequence)
(:import-from :fiveam :is :is-true :is-false)
(:export
:args
:get-val
:enable-reader-syntax
:disable-reader-syntax
:*array-function*
:*hash-table-function*
:*get-val-function*
:*get-val-array-function*
:*alists-are-lists*
:*plists-are-lists*
:*set-function*))
(in-package :reader)
(5am:def-suite :reader)
(5am:in-suite :reader)
;;;;;;;;;;;;;;;;;;;;;;;; CONFIGURATION VARIABLES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defparameter *array-function*
(defun make-t-array-from-initial-contents (initial-contents)
(labels ((dimensions (contents)
(if (listp contents)
(cons (length contents)
(dimensions (car contents)))
())))
(cl:make-array (dimensions initial-contents)
:element-type t
:initial-contents initial-contents)))
"A symbol bound (at read-time) to a function that takes INITIAL-CONTENTS
as its argument and returns the array. INITIAL-CONTENTS is a list of nested-lists.")
(defparameter *hash-table-function* 'alexandria:plist-hash-table
"A symbol bound (at read-time) to a function that takes
(PLIST &KEY TEST)
as arguments and returns the hash-table.")
(defparameter *get-val-function* 'get-val
"A symbol bound (at read-time) to a function that takes
(OBJECT &REST KEY/S)
as arguments and returns the value corresponding to the KEY/S.")
(defparameter *get-val-array-function* 'cl:aref
"A symbol bound (at read-time) to a function that takes
(ARRAY &REST SUBSCRIPTS)
as arguments and returns the corresponding value.
This is assumed to have a SETF defined.
This variable is significant if READER:*GET-VAL-FUNCTION* is bound to READER:GET-VAL.")
(defparameter *set-function*
'cl:remove-duplicates
"A symbol bound (at read-time) to a function that takes
(LIST &KEY TEST)
as arguments and returns the set object.")
(defparameter *alists-are-lists* nil
"If T, ALISTS will not be treated specially by READER:GET-VAL method for lists.")
(defparameter *plists-are-lists* nil
"If T, PLISTS will not be treated specially by READER:GET-VAL method for lists.")
(defmacro with-reader-syntax (reader-macro-identifiers &body body)
"This macro is only made for use by read-and-eval functionality,
and rather directed towards tests than users. So, do not export."
(let ((identifiers (loop :for identifier :in reader-macro-identifiers
:collect `(quote ,identifier))))
`(let ((*array-function* 'make-t-array-from-initial-contents)
(*hash-table-function* 'alexandria:plist-hash-table)
(*get-val-function* 'get-val)
(*get-val-array-function* 'cl:aref)
(*set-function* 'cl:remove-duplicates)
(*alists-are-lists* nil)
(*plists-are-lists* nil)
(*readtable* (%enable-reader-syntax *readtable* ,@identifiers)))
,@body)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;; READTABLE HANDLERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar *previous-readtables* nil)
(defparameter *reader-macro-activation-functions*
(list (cons "GET-VAL" (lambda ()
(set-macro-character #\[ 'get-val-reader-macro)
(set-macro-character #\] (lambda (stream char)
(declare (ignore stream char))
(error "No matching [ for ]")))))
(cons "HASH-TABLE" (lambda ()
(set-macro-character #\{ 'hash-table-reader-macro)
(set-macro-character #\}
(lambda (stream char)
(declare (ignore stream char))
(error "No matching { for }")))))
(cons "NOT" (lambda () (set-macro-character #\! 'not-reader-macro)))
(cons "STRING" (lambda () (set-macro-character #\$ 'string-reader-macro)))
(cons "DESCRIBE" (lambda () (set-macro-character #\? 'describe-reader-macro)))
(cons "ARRAY" (lambda ()
(set-dispatch-macro-character #\# #\[ 'array-reader-macro)
(set-macro-character #\] (lambda (stream char)
(declare (ignore stream char))
(error "No matching [ for ]")))))
(cons "SET" (lambda ()
(set-dispatch-macro-character #\# #\{
'set-reader-macro)
(set-macro-character #\}
(lambda (stream char)
(declare (ignore stream char))
(error "No matching { for }")))))
(cons "RUN-PROGRAM"
(lambda ()
(set-dispatch-macro-character #\# #\!
'run-program-reader-macro)))))
(alexandria:define-constant +reader-macro-doc+
"READER-MACRO-IDENTIFIERS are any of the following symbols:
GET-VAL, HASH-TABLE, NOT, STRING, DESCRIBE, ARRAY, SET, RUN-PROGRAM"
:test 'string=)
(defun %enable-reader-syntax (readtable &rest reader-macro-identifiers)
(let ((*readtable* readtable))
(mapcar (lambda (identifier)
(funcall (get-val *reader-macro-activation-functions*
identifier
:test #'string=)))
reader-macro-identifiers)
*readtable*))
(defmacro enable-reader-syntax (&rest reader-macro-identifiers)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(push *readtable* *previous-readtables*)
(setq *readtable* (copy-readtable))
(%enable-reader-syntax *readtable* ,@reader-macro-identifiers)))
(defmacro disable-reader-syntax ()
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setq *readtable* (if *previous-readtables*
(pop *previous-readtables*)
(copy-readtable nil)))))
(setf (documentation 'enable-reader-syntax 'function) +reader-macro-doc+)
(defun er (string) (eval (read-from-string string)))
(defmacro setp (new-value place)
`(progn
(setf ,place ,new-value)
,place))
(defun read-stream-until (stream until-char)
(iter (for next-char = (peek-char t stream nil))
(unless next-char (next-iteration))
(until (char= until-char next-char))
(collect (read stream nil))
(finally (read-char stream nil))))
;;;;;;;;;;;;;;;;;;;;;;;; ARRAY READER MACRO ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun read-stream-as-string-until (stream until-char)
(iter (for next-char = (peek-char nil stream nil))
(while next-char)
(until (char= until-char next-char))
(collect (read-char stream nil)
into string result-type 'string)
(finally (read-char stream nil)
(return string))))
(defun read-row-from-string (string)
(with-input-from-string (str string)
(iter (for obj = (read str nil))
(while obj)
(collect obj))))
(defun read-array (stream)
(let (array-dimensions)
(iter (for next-char = (peek-char t stream nil))
(alexandria:switch (next-char)
(#\[ (read-char stream)
(collect (multiple-value-bind (array dimensions) (read-array stream)
(setq array-dimensions dimensions)
array)
into list))
(#\] (read-char stream)
(setq array-dimensions
(cons (length list) array-dimensions))
(return-from read-array (values (cons 'list list)
array-dimensions)))
(t (let* ((element-list (split-sequence #\newline
(read-stream-as-string-until stream #\])
:remove-empty-subseqs t))
(num-rows (length element-list))
(num-cols (length (read-row-from-string (first element-list)))))
(return-from read-array
(cond ((null (rest element-list))
(values (cons 'list (read-row-from-string (first element-list)))
(list num-cols)))
(t (values (cons 'list (mapcar (lambda (list) (cons 'list list))
(mapcar #'read-row-from-string element-list)))
(list num-rows num-cols)))))))))))
(defun array-reader-macro (stream char n)
(declare (ignore char n))
(multiple-value-bind (initial-contents array-dimensions) (read-array stream)
(declare (ignorable array-dimensions))
`(,*array-function* ,initial-contents)))
(5am:def-test array ()
(with-reader-syntax (array)
(is (equalp #(1 2) (eval (read-from-string "#[1 2]"))))
(is (equalp #2A((1 2 3) (4 5 6))
(eval (read-from-string "#[1 2 3
4 5 6]"))))
(is (equalp #2A((1 2 3) (4 5 6))
(eval (read-from-string "#[[1 2 3] [4 5 6]]"))))
(is (equalp #(1 2)
(eval (read-from-string "(let ((a 1) (b 2)) #[a b])"))))))
(defmacro defmethods-with-setf (fun-name lambda-list &rest methods)
`(progn
,@(let ((new-value (gensym)))
`(,(let ((methods methods))
`(defgeneric ,fun-name ,lambda-list
,@(loop for args = (first methods)
for body = (second methods)
do (setq methods (cddr methods))
while methods
collect `(:method ,args
(assert (= 1 (length key/s)))
,body))))
,@(let ((methods methods))
(loop for args = (first methods)
for body = (second methods)
while methods
collect `(defmethod (setf ,fun-name) (,new-value ,@args)
(setf ,body ,new-value))
do (setq methods (cddr methods))))))))
#+sbcl (declaim (sb-ext:maybe-inline get-val))
(defmethods-with-setf get-val (object &rest key/s)
((object hash-table) &rest key/s) (gethash (car key/s) object)
((object sequence) &rest key/s) (elt object (car key/s))
((object structure-object) &rest key/s) (slot-value object (car key/s))
((object standard-object) &rest key/s) (slot-value object (car key/s)))
;;; An attempt may be made using specializing on trivial-types:association-list
;;; However, an error that this is not a class is raised.
;;; Further, we also need to allow the use of ['(a b c d) 3] => d rather
;;; than nil.
(defmethod get-val ((object list) &rest key/s)
(let ((key (car key/s)))
(cond ((and (not *alists-are-lists*)
(trivial-types:association-list-p object))
(let ((pair (apply #'assoc key object (rest key/s))))
(if pair (cdr pair) nil)))
(t
(assert (null (rest key/s)))
(if (and (not *plists-are-lists*)
(trivial-types:property-list-p object)
(not (integerp key)))
(getf object key)
(nth key object))))))
;; TODO: SETF expansions with generic-functions to set object when bound to null?
(defmethod (setf get-val) (new-value (object list) &rest key/s)
(assert (not (null object)))
(let ((key (car key/s)))
(cond ((and (not *alists-are-lists*)
(trivial-types:association-list-p object))
(let ((pair (apply #'assoc key object (rest key/s))))
(if pair
(setf (cdr pair) new-value)
(setf (cdr object)
(cons (cons key new-value)
(cdr object))))))
(t
(assert (null (rest key/s)))
(if (and (not *plists-are-lists*)
(trivial-types:property-list-p object)
(not (integerp key)))
(let ((value (getf object key)))
(if value
(setf (getf object key) new-value)
(setf (cddr object)
(cons key (cons new-value (cddr object))))))
(setf (nth key object) new-value))))))
(defmethod get-val ((object array) &rest key/s)
(apply *get-val-array-function* object key/s))
(defmethod (setf get-val) (new-value (object array) &rest key/s)
(case *get-val-array-function*
(aref (setf (apply #'aref object key/s) new-value))
(t (apply (fdefinition (list 'setf *get-val-array-function*))
new-value object key/s))))
;; (defmethod (setf get-val) (new-value (object array) &rest key/s)
;; (setf (apply *get-val-array-function* object key/s) new-value))
;;;;;;;;;;;;;;;;;;;;;;;; the remaining reader macros ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun get-val-reader-macro (stream char)
(declare (ignore char))
`(,*get-val-function* ,@(read-stream-until stream #\])))
(defmacro with-env (&body body)
(let ((body-value (gensym)))
`(let (,body-value)
(defclass foo () ((a :initform 3)))
(defmethod get-val ((object foo) &rest slot/s) (slot-value object (car slot/s)))
(defmethod (setf get-val) (new-value (object foo) &rest slot/s)
(setf (slot-value object (car slot/s)) new-value))
(defstruct bar a)
(setq ,body-value
(let ((str (alexandria:copy-array "abcde"))
(vec (alexandria:copy-array #(a b c d e)))
(arr (alexandria:copy-array #2A((1 2 3)
(4 5 6))))
(list (copy-list '(a b c d e)))
(ht (eval (read-from-string "{'a 'b 'c 'd}")))
(ht-eq (eval (read-from-string "{eq \"a\" 1
\"b\" 2}")))
(ht-equalp (eval (read-from-string "{equalp '(1 2 3) \"a\"
'(4 5 6) \"b\"}")))
(assoc-list (copy-tree '((a . 1) (b . 2))))
(plist (copy-list '(:a 4 :c 5)))
(clos-object (make-instance 'foo))
(struct (make-bar :a 3)))
(declare (ignorable str vec arr list ht ht-eq ht-equalp
assoc-list plist clos-object struct))
,@body))
(setf (find-class 'foo nil) nil)
(setf (find-class 'bar nil) nil)
,body-value)))
(5am:def-test get-val (:depends-on (and array hash-table))
(with-reader-syntax (get-val array hash-table)
(with-output-to-string (*error-output*)
(is (char= #\a (er "(with-env [str 0])")))
(is (eq 'a (er "(with-env [vec 0])")))
(is (= 3 (er "(with-env [arr 0 2])")))
(is (eq 'd (er "(with-env [list 3])")))
(is (eq 'b (er "(with-env [ht 'a])")))
(is (eq nil (er "(with-env [ht-eq (string (alexandria:copy-array \"a\"))])")))
(is (equal "b" (er "(with-env [ht-equalp (copy-list '(4 5 6))])")))
(is (= 1 (er "(with-env [assoc-list 'a])")))
(is (eq nil (er "(with-env [assoc-list 'c])")))
(is (= 5 (er "(with-env [plist :c])")))
(is (eq nil (er "(with-env [plist :e])")))
(is (eq 3 (er "(with-env [clos-object 'a])")))
(is (eq 3 (er "(with-env [struct 'a])"))))))
(5am:def-test setf-get-val (:depends-on (and array hash-table))
(with-reader-syntax (get-val array hash-table)
(with-output-to-string (*error-output*)
(is (char= #\f (er "(with-env (setp #\\f [str 0]))")))
(is (eq 'f (er "(with-env (setp 'f [vec 0]))")))
(is (= 4 (er "(with-env (setp 4 [arr 0 2]))")))
(is (eq 'f (er "(with-env (setp 'f [list 3]))")))
(is (eq 'f (er "(with-env (setp 'f [ht 'a]))")))
(is (= 2 (er "(with-env (setp 2 [assoc-list 'a]))")))
(is (eq 3 (er "(with-env (setp 3 [assoc-list 'c]))")))
(is (= 2 (er "(with-env (setp 2 [plist :c]))")))
(is (eq 3 (er "(with-env (setp 3 [plist :e]))")))
(is (eq 5 (er "(with-env (setp 5 [clos-object 'a]))")))
(is (eq 7 (er "(with-env (setp 7 [struct 'a]))"))))))
(defun hash-table-reader-macro (stream char)
(declare (ignore char))
(let* ((inputs (read-stream-until stream #\}))
(len (length inputs))
(test (if (oddp len)
(first inputs)
'equalp))
(inputs (if (oddp len)
(rest inputs)
inputs)))
`(,*hash-table-function* (list ,@inputs) :test ',test)))
(5am:def-test hash-table ()
(with-reader-syntax (hash-table)
(is (equalp (alexandria:plist-hash-table '(:a 1 :b 2) :test 'equalp)
(er "{:a 1 :b 2}")))
(is (equalp (alexandria:plist-hash-table '(:a 1 :b 2) :test 'eq)
(er "{eq :a 1 :b 2}")))
(is (equalp (alexandria:plist-hash-table '("a" 1.0 "b" 2) :test 'equalp)
(er "{\"a\" 1 \"b\" 2}")))))
(defun set-reader-macro (stream char n)
(declare (ignore char n))
(let* ((items (iter (until (char= (peek-char t stream nil)
#\}))
(collect (read stream))
(finally (read-char stream nil))))
(num-items (length items)))
(cond ((< num-items 2)
`(,*set-function* (list ,@items)))
((eq :test (car (last items 2)))
`(,*set-function* (list ,@(butlast items 2))
,@(last items 2)))
(t
`(,*set-function* (list ,@items))))))
(5am:def-test set ()
(with-reader-syntax (set)
(let ((set (er "#{'a 'b 1 '(1) '(1) :test #'equal}")))
(is-true (alexandria:setp set))
(is-true (member 'a set))
(is-true (member 'b set))
(is-true (member 1 set))
(is-false (member 2 set))
(is (= 1 (count '(1) set :test #'equal))))))
(defun run-program-reader-macro (stream char n)
(declare (ignore char n))
`(uiop:run-program ,(read-line stream) :output t))
(defun not-reader-macro (stream char)
(declare (ignore char))
`(not ,(read stream)))
(defun string-reader-macro (stream char)
(declare (ignore char))
`(write-to-string ,(read stream)))
(defun describe-reader-macro (stream char)
(declare (ignore char))
`(describe (quote ,(read stream))))
(5am:def-test run-program ()
(with-reader-syntax (run-program)-
(is (string= "hello world"
(er "(with-output-to-string (*standard-output*)
#!echo -n hello world
)")))))
(5am:def-test string ()
(with-reader-syntax (string)
(is (string-equal "5.0d0" (er "(let ((a 5.0d0)) $a)")))))
(5am:def-test describe ()
(with-reader-syntax (describe)
(is (string= (with-output-to-string (*standard-output*)
(describe 'reader:enable-reader-syntax))
(er "(with-output-to-string (*standard-output*)
?reader:enable-reader-syntax
)")))))