#lang racket/base
(require (for-syntax racket/base)
         racket/runtime-path
         racket/promise)
(provide lazy-require)

(define-syntax (lazy-require stx)
  (syntax-case stx ()
    [(lazy-require [modpath (thing ...)] ...)
     #`(begin (define-namespace-anchor anchor)
              (lazy-require1 modpath (thing ...) anchor #,stx)
              ...)]))

(define-syntax (lazy-require1 stx)
  (syntax-case stx ()
    [(lazy-require1 modpath (name ...) anchor orig-stx)
     (with-syntax ([(defn ...)
                    (for/list ([name (in-list (syntax->list #'(name ...)))])
                      (unless (identifier? name)
                        (raise-syntax-error #f "expected identifier" #'orig-stx name))
                      #`(define #,name (make-lazy-function '#,name get-sym)))])
       ;; implicit quasiquote, so can use normal module-path syntax'
       ;; or escape to compute a the module-path via expression
       #'(begin (define-runtime-module-path-index mpi-var (quasiquote modpath))
                (define (get-sym sym)
                  (parameterize ((current-namespace (namespace-anchor->namespace anchor)))
                    (dynamic-require mpi-var sym)))
                defn ...))]))

(define (make-lazy-function name get-sym)
  ;; Use 'delay/sync' because 'delay' promise is not reentrant.
  ;; FIXME: OTOH, 'delay/sync' promise is not kill-safe.
  (let ([fun-p (delay/sync (get-sym name))])
    (procedure-rename
     (make-keyword-procedure
      (lambda (kws kwargs . args)
        (keyword-apply (force fun-p) kws kwargs args)))
     name)))
