;; meta.scm -- meta language for describing modules
;; Copyright (c) 2009-2014 Alex Shinn.  All rights reserved.
;; BSD-style license: http://synthcode.com/license.txt

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; modules

(define *this-module* '())
(define *this-path* '())

(define (make-module exports env meta) (vector exports env meta #f))
(define (%module-exports mod) (vector-ref mod 0))
(define (module-env mod) (vector-ref mod 1))
(define (module-env-set! mod env) (vector-set! mod 1 env))
(define (module-meta-data mod) (vector-ref mod 2))
(define (module-meta-data-set! mod x) (vector-set! mod 2 x))

(define (module-exports mod)
  (or (%module-exports mod)
      (if (module-env mod)
          (env-exports (module-env mod))
          '())))

(define (module-name->strings ls res)
  (if (null? ls)
      res
      (let ((str (cond ((symbol? (car ls)) (symbol->string (car ls)))
                       ((number? (car ls)) (number->string (car ls)))
                       ((string? (car ls)) (car ls))
                       (else (error "invalid module name" (car ls))))))
        (module-name->strings (cdr ls) (cons "/" (cons str res))))))

(define (module-name->file name)
  (string-concatenate
   (reverse (cons ".sld" (cdr (module-name->strings name '()))))))

(define (module-name-prefix name)
  (string-concatenate (reverse (cdr (cdr (module-name->strings name '()))))))

(define load-module-definition
  (let ((meta-env (current-environment)))
    (lambda (name)
      (let* ((file (module-name->file name))
             (path (find-module-file file)))
        (if path (load path meta-env))))))

(define (find-module name)
  (cond
   ((assoc name *modules*) => cdr)
   (else
    (load-module-definition name)
    (cond ((assoc name *modules*) => cdr)
          (else #f)))))

(define (add-module! name module)
  (set! *modules* (cons (cons name module) *modules*)))

(define (delete-module! name)
  (let lp ((ls *modules*) (prev #f))
    (cond ((null? ls))
          ((equal? name (car (car ls)))
           (if prev
               (set-cdr! prev (cdr ls))
               (set! *modules* (cdr ls))))
          (else (lp (cdr ls) ls)))))

(define (symbol-append a b)
  (string->symbol (string-append (symbol->string a) (symbol->string b))))

(define (symbol-drop a b)
  (let ((as (symbol->string a))
        (bs (symbol->string b)))
    (if (and (> (string-length bs) (string-length as))
             (string=? as (substring bs 0 (string-length as))))
        (string->symbol (substring bs (string-length as)))
        b)))

(define (warn msg . args)
  (display msg (current-error-port))
  (display ":" (current-error-port))
  (for-each (lambda (a)
              (display " " (current-error-port))
              (write a (current-error-port)))
            args)
  (newline (current-error-port)))

(define (to-id id) (if (pair? id) (car id) id))
(define (from-id id) (if (pair? id) (cdr id) id))
(define (id-filter pred ls)
  (cond ((null? ls) '())
        ((pred (to-id (car ls))) (cons (car ls) (id-filter pred (cdr ls))))
        (else (id-filter pred (cdr ls)))))

(define (%resolve-import x)
  (cond
   ((not (and (pair? x) (list? x)))
    (error "invalid import syntax" x))
   ((and (memq (car x) '(prefix drop-prefix))
         (symbol? (car (cddr x))) (list? (cadr x)))
    (let ((mod-name+imports (%resolve-import (cadr x))))
      (cons (car mod-name+imports)
            (map (lambda (i)
                   (cons ((if (eq? (car x) 'drop-prefix)
                              symbol-drop
                              symbol-append)
                          (car (cddr x))
                          (to-id i))
                         (from-id i)))
                 (or (cdr mod-name+imports)
                     (module-exports (find-module (car mod-name+imports))))))))
   ((and (pair? (cdr x)) (pair? (cadr x)))
    (if (memq (car x) '(only except rename))
        (let* ((mod-name+imports (%resolve-import (cadr x)))
               (imp-ids (or (cdr mod-name+imports)
                            (module-exports (find-module (car mod-name+imports))))))
          (cons (car mod-name+imports)
                (case (car x)
                  ((only)
                   (map (lambda (imp)
                          (if (or (boolean? imp-ids) (memq imp imp-ids))
                              imp
                              (error "importing unknown binding" imp imp-ids)))
                        (cddr x)))
                  ((except)
                   (id-filter (lambda (i) (not (memq i (cddr x)))) imp-ids))
                  ((rename)
                   ;; TODO: warn about renaming an unimported id
                   (map (lambda (i)
                          (let ((rename (assq (to-id i) (cddr x))))
                            (if rename (cons (cadr rename) (from-id i)) i)))
                        imp-ids)))))
        (error "invalid import modifier" x)))
   ((find-module x)
    => (lambda (mod) (cons x (%module-exports mod))))
   (else
    (error "couldn't find import" x))))

;; slightly roundabout, using eval since we don't have env-define! here
(define (auto-generate-bindings ls)
  (let ((bound (env-exports *auto-env*))
        (def-aux
          (make-syntactic-closure *chibi-env* '() 'define-auxiliary-syntax)))
    (let lp ((ls ls) (new '()))
      (cond
       ((null? ls)
        (if (pair? new)
            (eval `(,(make-syntactic-closure *chibi-env* '() 'begin) ,@new)
                  *auto-env*)))
       (else
        (let ((from-id (if (pair? (car ls)) (cdar ls) (car ls))))
          (if (memq from-id bound)
              (lp (cdr ls) new)
              (lp (cdr ls) `((,def-aux ,from-id) ,@new)))))))))

(define (resolve-import x)
  (let ((x (%resolve-import x)))
    (if (equal? '(auto) (car x))
        (auto-generate-bindings (cdr x)))
    x))

(define (resolve-module-imports env meta)
  (for-each
   (lambda (x)
     (case (and (pair? x) (car x))
       ((import import-immutable)
        (for-each
         (lambda (m)
           (let* ((mod2-name+imports (resolve-import m))
                  (mod2 (load-module (car mod2-name+imports))))
             (%import env (module-env mod2) (cdr mod2-name+imports) #t)))
         (cdr x)))))
   meta))

(define (eval-module name mod . o)
  (let ((env (if (pair? o) (car o) (make-environment)))
        (meta (module-meta-data mod))
        (dir (module-name-prefix name)))
    (define (load-modules files extension fold? . o)
      (for-each
       (lambda (f)
         (let ((f (string-append dir f extension)))
           (cond
            ((find-module-file f)
             => (lambda (path)
                  (cond (fold?
                         (let ((in (open-input-file path)))
                           (set-port-fold-case! in #t)
                           (load in env)))
                        (else
                         (load path env)))))
            ((and (pair? o) (car o)) ((car o)))
            (else (error "couldn't find include" f)))))
       files))
    ;; catch cyclic references
    (cond
     ((procedure? meta)
      (meta env))
     (else
      (module-meta-data-set!
       mod
       `((error "module attempted to reference itself while loading" ,name)))
      (resolve-module-imports env meta)
      (protect
          (exn (else
                (module-meta-data-set! mod meta)
                (if (not (any (lambda (x)
                                (and (pair? x)
                                     (memq (car x) '(import import-immutable))))
                              meta))
                    (warn "WARNING: exception inside module with no imports - did you forget to (import (scheme base)) in" name))
                (raise-continuable exn)))
        (for-each
         (lambda (x)
           (case (and (pair? x) (car x))
             ((include)
              (load-modules (cdr x) "" #f))
             ((include-ci)
              (load-modules (cdr x) "" #t))
             ((include-shared)
              (load-modules (cdr x) *shared-object-extension* #f))
             ((include-shared-optionally)
              (load-modules (list (cadr x)) *shared-object-extension* #f
                            (lambda () (load-modules (cddr x) "" #f))))
             ((body begin)
              (for-each (lambda (expr) (eval expr env)) (cdr x)))
             ((error)
              (apply error (cdr x)))))
         meta))
      (module-meta-data-set! mod meta)
      (warn-undefs env #f)
      env))))

(define (mutable-environment . ls)
  (let ((env (make-environment)))
    (for-each
     (lambda (m)
       (let* ((mod2-name+imports (resolve-import m))
              (mod2 (load-module (car mod2-name+imports))))
         (%import env (module-env mod2) (cdr mod2-name+imports) #t)))
     ls)
    env))

(define (environment . ls)
  (let ((env (apply mutable-environment ls)))
    (make-immutable! env)
    env))

(define (load-module name)
  (let ((mod (find-module name)))
    (if (and mod (not (module-env mod)))
        (module-env-set! mod (eval-module name mod)))
    mod))

(define-syntax meta-begin begin)
(define-syntax meta-define define)

(define define-library-transformer
  (er-macro-transformer
   (lambda (expr rename compare)
     (cond
      ((find (lambda (x) (and (pair? x) (compare (car x) (rename 'alias-for))))
             (cddr expr))
       => (lambda (alias)
            (if (not (= 1 (length (cddr expr))))
                (error "alias must be the only library declaration" expr))
            ;; we need to load the original module first, not just find it,
            ;; or else the includes would happen relative to the alias
            (let ((name (cadr expr))
                  (orig (load-module (cadr alias))))
              (if (not orig)
                  (error "couldn't find library to alias" (cadr alias))
                  `(,(rename 'add-module!) (,(rename 'quote) ,name)
                    (,(rename 'quote) ,orig))))))
      (else
       (let ((name (cadr expr))
             (body (cddr expr))
             (tmp (rename 'tmp))
             (this-module (rename '*this-module*))
             (_add-module! (rename 'add-module!))
             (_make-module (rename 'make-module))
             (_define (rename 'meta-define))
             (_lambda (rename 'lambda))
             (_let (rename 'let))
             (_map (rename 'map))
             (_if (rename 'if))
             (_cond (rename 'cond))
             (_set! (rename 'set!))
             (_quote (rename 'quote))
             (_and (rename 'and))
             (_= (rename '=))
             (_eq? (rename 'eq?))
             (_pair? (rename 'pair?))
             (_null? (rename 'null?))
             (_reverse (rename 'reverse))
             (_append (rename 'append))
             (_assq (rename 'assq))
             (_=> (rename '=>))
             (_else (rename 'else))
             (_length (rename 'length))
             (_identifier->symbol (rename 'identifier->symbol))
             (_error (rename 'error))
             (_cons (rename 'cons))
             (_car (rename 'car))
             (_cdr (rename 'cdr))
             (_caar (rename 'caar))
             (_cadr (rename 'cadr))
             (_cdar (rename 'cdar))
             (_cddr (rename 'cddr)))
         ;; Check for suspicious defines.
         (for-each
          (lambda (x)
            (if (and (pair? x) (memq (strip-syntactic-closures (car x))
                                     '(define define-syntax)))
                (warn "suspicious use of define in library declarations - did you forget to wrap it in begin?" x)))
          (cdr expr))
         ;; Generate the library wrapper.
         (set! *this-path*
               (cons (string-concatenate
                      (module-name->strings (cdr (reverse name)) '()))
                     *this-path*))
         `(,_let ((,tmp ,this-module))
            (,_define (rewrite-export x)
              (,_if (,_pair? x)
                  (,_if (,_and (,_= 3 (,_length x))
                               (,_eq? (,_quote rename)
                                      (,_identifier->symbol (,_car x))))
                      (,_cons (,_car (,_cddr x)) (,_cadr x))
                      (,_error "invalid module export" x))
                  x))
            (,_define (extract-exports)
              (,_cond
               ((,_assq (,_quote export-all) ,this-module)
                ,_=> (,_lambda (x)
                       (,_if (,_pair? (,_cdr x))
                           (,_error "export-all takes no parameters" x))
                       #f))
               (,_else
                (,_let lp ((ls ,this-module) (res (,_quote ())))
                  (,_cond
                   ((,_null? ls) res)
                   ((,_and (,_pair? (,_car ls))
                           (,_eq? (,_quote export) (,_caar ls)))
                    (lp (,_cdr ls)
                        (,_append (,_map rewrite-export (,_cdar ls)) res)))
                   (,_else (lp (,_cdr ls) res)))))))
            (,_set! ,this-module (,_quote ()))
            ,@body
            (,_add-module! (,_quote ,name)
                           (,_make-module (extract-exports)
                                          #f
                                          (,_reverse ,this-module)))
            (,_set! ,this-module ,tmp)
            (,(rename 'pop-this-path)))))))))

(define-syntax define-library define-library-transformer)
(define-syntax module define-library-transformer)

(define-syntax pop-this-path
  (er-macro-transformer
   (lambda (expr rename compare)
     (if (pair? *this-path*)
         (set! *this-path* (cdr *this-path*)))
     #f)))

(define-syntax include-library-declarations
  (er-macro-transformer
   (lambda (expr rename compare)
     (let lp1 ((ls (cdr expr)) (res '()))
       (cond
        ((pair? ls)
         (let* ((file (car ls))
                (rel-path (if (pair? *this-path*)
                              (string-append (car *this-path*) "/" file)
                              file)))
           (cond
            ((find-module-file rel-path)
             => (lambda (path)
                  (call-with-input-file path
                    (lambda (in)
                      (let lp2 ((res res))
                        (let ((x (read in)))
                          (if (eof-object? x)
                              (lp1 (cdr ls) res)
                              (lp2 (cons x res)))))))))
            (else
             (error "couldn't find include-library-declarations file" file)))))
        (else
         `(,(rename 'meta-begin)
           ,@(reverse res)
           (,(rename 'set!) ,(rename '*this-module*)
            (,(rename 'cons) (,(rename 'quote)
                              ,(cons 'include-library-declarations (cdr expr)))
             ,(rename '*this-module*))))))))))

(define-syntax define-meta-primitive
  (er-macro-transformer
   (lambda (expr rename compare)
     (let ((name (cadr expr)))
       `(define-syntax ,name
          (er-macro-transformer
           (lambda (expr rename compare)
             (let ((this-module (rename '*this-module*))
                   (_set! (rename 'set!))
                   (_cons (rename 'cons))
                   (_quote (rename 'syntax-quote)))
               `(,_set! ,this-module
                        (,_cons (,_quote ,(cons ',name (cdr expr)))
                                ,this-module))))))))))

(define-meta-primitive import)
(define-meta-primitive import-immutable)
(define-meta-primitive export)
(define-meta-primitive export-all)
(define-meta-primitive include)
(define-meta-primitive include-ci)
(define-meta-primitive include-shared)
(define-meta-primitive include-shared-optionally)
(define-meta-primitive body)
(define-meta-primitive begin)

;; The `import' binding used by (chibi) and (scheme base), etc.
(define-syntax repl-import
  (er-macro-transformer
   (let ((meta-env (current-environment)))
     (lambda (expr rename compare)
       (let lp ((ls (cdr expr)) (res '()))
         (cond
          ((null? ls)
           (cons (rename 'meta-begin) (reverse res)))
          (else
           (let ((mod+imps (resolve-import (car ls))))
             (cond
              ((pair? mod+imps)
               (lp (cdr ls)
                   (cons `(,(rename '%import)
                           #f
                           (,(rename 'module-env)
                            (,(rename 'load-module)
                             (,(rename 'quote) ,(car mod+imps))))
                           (,(rename 'quote) ,(cdr mod+imps))
                           #f)
                         res)))
              (else
               (error "couldn't find module" (car ls))))))))))))

;; This will be redefined in main.c.
(define raw-script-file #f)

;; capture a static copy of the current environment to serve
;; as the (chibi) module
(define *chibi-env*
  (let ((env (make-environment)))
    (%import env (interaction-environment) #f #t)
    (env-parent env)))

(define *auto-env*
  (let ((env (make-environment)))
    (%import env (interaction-environment)
             '(_ => ... else unquote unquote-splicing) #t)
    (env-parent env)))

(define *modules*
  (list
   (cons '(chibi)
         (make-module #f *chibi-env* '((include "init-7.scm"))))
   (cons '(chibi primitive)
         (make-module #f #f (lambda (env) (primitive-environment 7))))
   (cons '(meta)
         (make-module #f (current-environment) '((include "meta-7.scm"))))
   (cons '(auto)
         (make-module #f *auto-env* '()))
   (cons '(srfi 0)
         (make-module (list 'cond-expand)
                      (current-environment)
                      (list (list 'export 'cond-expand))))))
