aboutsummaryrefslogtreecommitdiff
;;; SPDX-License-Identifier: CC0-1.0
;;;
;;; Copyright (C) 2023 Wojtek Kosior <koszko@koszko.org>

(define-module (de-paul-records)
  #:use-module ((srfi srfi-1) #:select (append-map))
  #:use-module ((srfi srfi-9 gnu)
                #:select (define-immutable-record-type set-field set-fields))
  #:use-module ((srfi srfi-26) #:select (cut))
  #:use-module ((ice-9 format) #:select (format))
  #:use-module ((ice-9 match) #:select (match match-lambda))
  #:use-module ((ice-9 match) #:prefix ice-9-match:)
  #:re-export (set-field set-fields)
  #:export (format-identifiers

            (match-override . match)
            (match-lambda-override . match-lambda)
            (match-lambda*-override . match-lambda*)
            (match-let-override . match-let)
            (match-let*-override . match-let*)
            (match-letrec-override . match-letrec)

            define-immutable-record-type*))

(eval-when (compile load eval)
  (define (get-first-identifier list-of-identifiers-and-other)
    (let loop ((items list-of-identifiers-and-other))
      (match items
        (()
         (throw 'programming-error))
        (((? identifier? item) . _)
         item)
        ((_ . rest)
         (loop rest)))))

  (define* (format-identifiers fmt #:rest args)
    (let* ((first-identifier (get-first-identifier args))
           (processed-args (map (match-lambda
                                  ((? identifier? identifier)
                                   (symbol->string (syntax->datum identifier)))
                                  (other
                                   other))
                                args))
           (formatted-string (apply format #f fmt processed-args)))
      (datum->syntax first-identifier (string->symbol formatted-string))))

  (define-immutable-record-type <field-spec> (make-field-spec) field-spec?
    (name    field-spec-name    field-spec-set-name)
    (default field-spec-default field-spec-set-default))

  (define %null-field-spec
    (set-field (make-field-spec)
      (field-spec-default) #'#f))

  (define (syntax->field-spec x)
    (syntax-case x ()
      (field-name
       (identifier? #'field-name)
       (syntax->field-spec #'(field-name)))
      ((field-name . field-args)
       (let loop ((field-spec %null-field-spec)
                  (args #'field-args))
         (syntax-case args ()
           (()
            (field-spec-set-name field-spec #'field-name))
           ((#:default default . args-rest)
            (eq? (field-spec-default field-spec)
                 (field-spec-default %null-field-spec))
            (loop (field-spec-set-default field-spec #'default)
                  #'args-rest)))))))

  (define-immutable-record-type <record-spec> (make-record-spec) record-spec?
    (name     record-spec-name     record-spec-set-name)
    (fields   record-spec-fields   record-spec-set-fields)
    (export?  record-spec-export?  record-spec-set-export?)
    (finalize record-spec-finalize record-spec-set-finalize))

  (define %null-record-spec
    (set-fields (make-record-spec)
      ((record-spec-fields) '())
      ((record-spec-export?) 'false-by-default)))

  (define (syntax->record-spec x)
    (syntax-case x ()
      ((_ record-name . record-args)
       (identifier? #'record-name)
       (let loop ((record-spec %null-record-spec)
                  (args #'record-args))
         (syntax-case args ()
           (()
            (set-fields record-spec
              ((record-spec-name) #'record-name)
              ((record-spec-fields) (reverse (record-spec-fields
                                              record-spec)))))
           ((#:export? export? . args-rest)
            (eq? (record-spec-export? record-spec) 'false-by-default)
            (loop (record-spec-set-export? record-spec
                    (and (syntax->datum #'export?) #t))
                  #'args-rest))
           ((#:finalize finalizer . args-rest)
            (not (record-spec-finalize record-spec))
            (loop (record-spec-set-finalize record-spec
                    #'finalizer)
                  #'args-rest))
           ((field-def . args-rest)
            (loop (record-spec-set-fields record-spec
                    (cons (syntax->field-spec #'field-def)
                          (record-spec-fields record-spec)))
                  #'args-rest)))))))

  (define-immutable-record-type <field-init> (make-field-init) field-init?
    (name            field-init-name            field-init-set-name)
    (make-value-expr field-init-make-value-expr field-init-set-make-value-expr))

  (define %null-field-init
    (make-field-init))

  (define (syntax->field-init x)
    (syntax-case x ()
      ((field-name value-expr)
       (identifier? #'field-name)
       (set-fields %null-field-init
         ((field-init-name) #'field-name)
         ((field-init-make-value-expr) (const #'value-expr))))

      (field-name
       (identifier? #'field-name)
       (syntax->field-init #'(field-name field-name)))

      ((field-name #:=> value-update-proc)
       (identifier? #'field-name)
       (set-fields %null-field-init
         ((field-init-name) #'field-name)
         ((field-init-make-value-expr)
          (lambda (record-name record-base)
            #`(value-update-proc
               (#,(format-identifiers "~a-~a" record-name #'field-name)
                #,record-base))))))

      ((field-name #:-> value-update-expr value-update-expr-rest ...)
       (identifier? #'field-name)
       (syntax->field-init #'(field-name #:=> (lambda (field-name)
                                                value-update-expr
                                                value-update-expr-rest ...))))

      ((field-name #:list value-item-expr value-item-expr-rest ...)
       (syntax->field-init #'(field-name
                              (list value-item-expr
                                    value-item-expr-rest ...))))))

  (define-immutable-record-type <record-init> (make-record-init) record-init?
    (inherit record-init-inherit record-init-set-inherit)
    (fields  record-init-fields  record-init-set-fields))

  (define %null-record-init
    (set-fields (make-record-init)
      ((record-init-inherit) #f)
      ((record-init-fields) '())))

  (define (syntax->record-init x)
    (syntax-case x ()
      ((_ . init-args)
       (let loop ((record-init %null-record-init)
                  (args #'init-args))
         (syntax-case args ()
           (()
            (set-field record-init
              (record-init-fields) (reverse (record-init-fields record-init))))
           ((#:<- base . args-rest)
            (not (record-init-inherit record-init))
            (loop (record-init-set-inherit record-init #'base)
                  #'args-rest))
           ((field-init . args-rest)
            (loop (record-init-set-fields record-init
                    (cons (syntax->field-init #'field-init)
                          (record-init-fields record-init)))
                  #'args-rest)))))))

  (define (transform-record-init record-name %null-record init-form)
    (match init-form
      ((= syntax->record-init (and (= record-init-inherit inherit)
                                   (= record-init-fields  field-inits)))
       #`(let* ((base #,(or inherit %null-record))
                #,@(map (match-lambda
                          ((and (= field-init-name            field-name)
                                (= field-init-make-value-expr make-value-expr))
                           #`(#,field-name
                              #,(make-value-expr record-name #'base))))
                        field-inits))
           (set-fields base
             #,@(map (match-lambda
                       ((= field-init-name field-name)
                        #`((#,(format-identifiers "~a-~a"
                                record-name field-name))
                           #,field-name)))
                     field-inits))))))

  (define-syntax define-immutable-record-type*
    (match-lambda
      ((= syntax->record-spec (and my-record-spec
                                   (= record-spec-name     record-name)
                                   (= record-spec-fields   field-specs)
                                   (= record-spec-export?  export?)
                                   (= record-spec-finalize finalize)))
       (with-syntax ((<record> (format-identifiers "<~a>" record-name))
                     (--make-record (format-identifiers "--make-~a"
                                      record-name))
                     (record? (format-identifiers "~a?" record-name))
                     (%null-record (format-identifiers "%null-~a" record-name))
                     (*record (format-identifiers "*~a" record-name))
                     (--finalize-record (format-identifiers "--finalize-~a"
                                          record-name)))
         #`(begin
             (define-immutable-record-type <record> (--make-record) record?
               #,@(map (match-lambda
                         ((= field-spec-name field-name)
                          #`(#,field-name
                             #,@(map (cut format-identifiers <>
                                          record-name field-name)
                                     '("~a-~a" "~a-set-~a")))))
                       field-specs))

             (define %null-record
               (set-fields (--make-record)
                 #,@(map (match-lambda
                           ((and (= field-spec-name field-name)
                                 (= field-spec-default default))
                            #`((#,(format-identifiers "~a-~a"
                                    record-name field-name))
                               #,default)))
                         field-specs)))

             (define-syntax *record
               (cut transform-record-init #'#,record-name #'%null-record <>))

             (define --finalize-record
               #,(or finalize #'identity))

             (define-syntax-rule (#,record-name . body-rest)
               (--finalize-record (*record . body-rest)))

             #,@(if (eq? export? #t)
                    (list
                     #`(export <record> #,record-name record? %null-record
                               #,@(append-map
                                   (match-lambda
                                     ((= field-spec-name field-name)
                                      (map (cut format-identifiers <>
                                                record-name field-name)
                                           '("~a-~a" "~a-set-~a"))))
                                   field-specs)))
                    #'()))))))

  (define (map-syntax proc syntax-list)
    (syntax-case syntax-list ()
      (()
       '())
      ((item . rest)
       (cons (proc #'item) (map-syntax proc #'rest)))))

  (define (transform-match-field record-name-base field-form)
    (syntax-case field-form ()
      (field-name
       (identifier? #'field-name)
       (transform-match-field record-name-base #'(field-name field-name)))
      ((field-name pat pats ...)
       (identifier? #'field-name)
       (with-syntax ((record-field (format-identifiers "~a-~a"
                                     record-name-base #'field-name)))
         #`(= record-field
              (and #,@(map-syntax transform-match-pat #'(pat pats ...))))))))

  (define (transform-match-pat pat-form)
    (syntax-case pat-form ($* = ? quote quasiquote)
      ((quote something)
       #'(quote something))
      ((quasiquote something)
       #`(quasiquote #,(transform-quasiquoted-match-pat #'something)))
      (#(pats ...)
       (with-syntax (((pats* ...) #`(#,@(map-syntax transform-match-pat
                                                    #'(pats ...)))))
         #'#(pats* ...)))
      (($* record-name-base pats ...)
       (identifier? #'record-name-base)
       (with-syntax ((record? (format-identifiers "~a?" #'record-name-base)))
         #`(? record? #,@(map-syntax (cut transform-match-field
                                          #'record-name-base <>)
                                     #'(pats ...)))))
      ((= field pat)
       #`(= field #,(transform-match-pat #'pat)))
      ((? pred pats ...)
       #`(? pred #,@(map-syntax transform-match-pat #'(pats ...))))
      ((head . tail)
       #`(#,(transform-match-pat #'head) . #,(transform-match-pat #'tail)))
      (other
       #'other)))

  (define (transform-quasiquoted-match-pat pat-form)
    (syntax-case pat-form (unquote unquote-splicing)
      ((unquote something)
       #`(unquote #,(transform-match-pat #'something)))
      ((unquote-splicing something)
       #`(unquote-splicing #,(transform-match-pat #'something)))
      ((head . tail)
       #`(#,(transform-quasiquoted-match-pat #'head)
          . #,(transform-quasiquoted-match-pat #'tail)))
      (#(pats ...)
       (with-syntax (((pats* ...) #`(#,@(map-syntax
                                         transform-quasiquoted-match-pat
                                         #'(pats ...)))))
         #'#(pats* ...)))
      (other
       #'other)))

  (define (transform-match-clauses clauses-forms)
    #`(#,@(map-syntax (lambda (clause-form)
                        (syntax-case clause-form ()
                          ((pat . body)
                           #`(#,(transform-match-pat #'pat) . body))))
                      clauses-forms)))

  (define-syntax match-override
    (lambda (x)
      (syntax-case x ()
        ((_ expr . clauses)
         #`(ice-9-match:match expr . #,(transform-match-clauses #'clauses))))))

  (define-syntax match-lambda-override
    (lambda (x)
      (syntax-case x ()
        ((_ . clauses)
         #`(ice-9-match:match-lambda
            . #,(transform-match-clauses #'clauses))))))

  (define-syntax match-lambda*-override
    (lambda (x)
      (syntax-case x ()
        ((_ . clauses)
         #`(ice-9-match:match-lambda*
            . #,(transform-match-clauses #'clauses))))))

  (define transform-match-bindings
    transform-match-clauses)

  (define-syntax match-let-override
    (lambda (x)
      (syntax-case x ()
        ((_ variable bindings . body)
         (identifier? #'variable)
         #`(ice-9-match:match-let variable
                                  #,(transform-match-bindings #'bindings)
                                  . body))
        ((_ bindings . body)
         #`(ice-9-match:match-let #,(transform-match-bindings #'bindings)
                                  . body)))))

  (define-syntax match-let*-override
    (lambda (x)
      (syntax-case x ()
        ((_ bindings . body)
         #`(ice-9-match:match-let* #,(transform-match-bindings #'bindings)
                                   . body)))))

  (define-syntax match-letrec-override
    (lambda (x)
      (syntax-case x ()
        ((_ bindings . body)
         #`(ice-9-match:match-letrec #,(transform-match-bindings #'bindings)
                                     . body))))))

;;; Local Variables:
;;; eval: (put 'format-identifiers 'scheme-indent-function 1)
;;; eval: (put 'record-spec-set-finalize 'scheme-indent-function 1)
;;; eval: (put 'record-spec-set-export? 'scheme-indent-function 1)
;;; eval: (put 'record-spec-set-fields 'scheme-indent-function 1)
;;; eval: (put 'record-init-set-fields 'scheme-indent-function 1)
;;; End: