; Record.scm.  This more or less implements the records that are
; proposed for R5RS - unfortunately, all records created in this
; manner look like vectors.  I believe the original record proposal
; was made by Jonathan Rees.  This implementation defines some symbols
; other than those that are part of the record proposal - this
; wouldn't be a problem if Scheme had a module system, but it doesn't.

; Written by david carlton, carlton@husc.harvard.edu.  This code is in
; the public domain.

; Tags to help identify rtd's.  (A record is identified by the rtd
; that begins it.)
(define record:*rtd-tag* (cons 'rtd '()))

; Length of the extra junk that we stick on the beginning of a vector
; representing a record object.
(define record:*header-length* 1)

; Checks to see if a list has any duplicates.  Also checks to see if
; it a list, for that matter.
(define record:has-duplicates?
  (lambda (list)
    (let loop ((list list))
      (cond
       ((null? list) #f)
       ((not (pair? list)) #t)
       ((memq (car list) (cdr list)) #t)
       (else (loop (cdr list)))))))

; Checks to see it all of the elments of a list satisfy a certain
; predicate.
(define record:satisfies-predicate
  (lambda (list predicate?)
    (let loop ((list list))
      (cond
       ((null? list) #t)
       ((predicate? (car list)) (loop (cdr list)))
       (else #f)))))

; Determines whether or not a certain element is in a list.  If not,
; it returns #f; if so, it returns its (zero-based) position in the
; list, adding to the position the length of the header of a record.
(define record:corrected-index
  (lambda (element list)
    (let index-loop ((list list)
		     (n 0))
      (cond
       ((null? list) #f)
       ((eq? element (car list)) (+ n record:*header-length*))
       (else (index-loop (cdr list) (+ n 1)))))))

; May or may not be the best way to define this; at least it's tail
; recursive, and probably does a reasonable job on many systems.  And
; it's certainly an easy way to do things.
(define record:list-copy
  (lambda (list)
    (reverse (reverse list))))
;(define record:list-copy list-copy)

; Various accessor functions.  No error checking; if you call these,
; you should know that they will work.
(define record:rtd-name (lambda (rtd) (vector-ref rtd 1)))
(define record:rtd-fields (lambda (rtd) (vector-ref rtd 2)))
(define record:rtd-length (lambda (rtd) (vector-ref rtd 3)))
(define record:get-tag (lambda (x) (vector-ref x 0)))
(define record:record-rtd (lambda (x) (vector-ref x 0)))

(define make-record-type
  (lambda (type-name field-names)
    (if (not (string? type-name))
	(perror "make-record-type: non-string type-name argument."))
    (if (or (record:has-duplicates? field-names)
	    (not (record:satisfies-predicate field-names symbol?)))
	(perror "make-record-type: illegal field-names argument."))
    (vector record:*rtd-tag* type-name field-names
	    (length field-names))))

; Determines whether or not a certain object looks like an rtd.
; Doesn't do as much error-checking as it could, but it would be quite
; unlikely for somebody to accidentally fool this function.
(define record:rtd?
  (lambda (object)
    (and (vector? object)
	 ; Could check for the exact value here, but then I'd have to
	 ; keep changing this as I change the format of a rtd.  This
	 ; is good enough to get the vector-ref to work.
	 (not (= (vector-length object) 0))
	 (eq? (record:get-tag object) record:*rtd-tag*))))

(define record-constructor
  (lambda (rtd . field-names)
    (if (not (record:rtd? rtd))
	(perror "record-constructor: illegal rtd argument."))
    (if (null? field-names)
	(let ((record-length (record:rtd-length rtd)))
	  (lambda elts
	    (if (not (= (length elts) record-length))
		(perror "record-constructor: "
			(record:rtd-name rtd)
			": wrong number of arguments."))
	    (apply vector rtd elts)))
	(let ((record-fields (record:rtd-fields rtd))
	      (corrected-record-length
	       (+ (record:rtd-length rtd)
		  record:*header-length*))
	      (field-names (car field-names)))
	  (if (or (record:has-duplicates? field-names)
		  (not (record:satisfies-predicate
			field-names
			(lambda (x)
			  (memq x record-fields)))))
	      (perror
	       "record-constructor: invalid field-names argument."))
	  (let ((field-length (length field-names))
		(record-offsets
		 (let r-o-loop ((offsets '())
				(names field-names))
		   (if (null? names)
		       (reverse offsets)
		       (r-o-loop
			(cons (record:corrected-index (car names)
						      record-fields)
			      offsets)
			(cdr names))))))
	    (lambda elts
	      (if (not (= (length elts) field-length))
		  (perror "record-constructor: "
			  (record:rtd-name rtd)
			  ": wrong number of arguments."))
	      (let ((result (make-vector corrected-record-length)))
		(vector-set! result 0 rtd)
		(let r-c-loop ((offsets record-offsets)
			       (elts elts))
		  (if (null? elts) result
		      (begin
			(vector-set! result
				     (car offsets)
				     (car elts))
			(r-c-loop (cdr offsets) (cdr elts))))))))))))

(define record-predicate
  (lambda (rtd)
    (if (not (record:rtd? rtd))
	(perror "record-predicate: invalid argument."))
    (let ((corrected-length (+ (record:rtd-length rtd)
			       record:*header-length*)))
      (lambda (x)
	(and (vector? x)
	     (= (vector-length x) corrected-length)
	     (eq? (record:record-rtd x) rtd))))))

(define record-accessor
  (lambda (rtd field-name)
    (if (not (record:rtd? rtd))
	(perror "record-accessor: invalid rtd argument."))
    (let ((name-index (record:corrected-index field-name
					      (record:rtd-fields rtd)))
	  (predicate (record-predicate rtd)))
      (if (not name-index)
	  (perror "record-accessor: invalid field-name argument."))
      (lambda (x)
	(if (not (predicate x))
	    (perror "record-accessor: "
		    (record:rtd-name rtd)
		    " "
		    field-name
		    ": invalid argument."))
	(vector-ref x name-index)))))

(define record-updater
  (lambda (rtd field-name)
    (if (not (record:rtd? rtd))
	(perror "record-updater: invalid rtd argument."))
    (let ((name-index (record:corrected-index field-name
					      (record:rtd-fields rtd)))
	  (predicate (record-predicate rtd)))
      (if (not name-index)
	  (perror "record-updater: invalid field-name argument."))
      (lambda (x y)
	(if (not (predicate x))
	    (perror "record-updater: "
		    (record:rtd-name rtd)
		    " "
		    field-name
		    ": invalid argument."))
	(vector-set! x name-index y)))))

(define record?
  (lambda (obj)
    (and (vector? obj)
	 (>= (vector-length obj) 1)
	 (record:rtd? (record:record-rtd obj))
	 (= (vector-length obj)
	    (+ record:*header-length*
	       (record:rtd-length (record:record-rtd obj)))))))

(define record-type-descriptor
  (lambda (record)
    (if (not (record? record))
	(perror "record-type-descriptor: invalid argument."))
    (record:record-rtd record)))

(define record-type-name
  (lambda (rtd)
    (if (not (record:rtd? rtd))
	(perror "record-type-name: invalid argument."))
    (record:rtd-name rtd)))

; For this function, make a copy of the value returned in order to
; make it a bit harder for the user to screw things up.
(define record-type-field-names
  (lambda (rtd)
    (if (not (record:rtd? rtd))
	(perror "record-type-field-names: invalid argument."))
    (record:list-copy (record:rtd-fields rtd))))
