#lang racket/base
(require (for-syntax racket/base
                     unstable/lazy-require
                     "sc.rkt"
                     "lib.rkt"
                     "kws.rkt"
                     racket/syntax
                     syntax/private/keyword)
         syntax/parse/private/residual-ct ;; keep abs. path
         syntax/parse/private/residual    ;; keep abs. path
         (only-in unstable/syntax phase-of-enclosing-module))
(begin-for-syntax
 (lazy-require
  [syntax/parse/private/rep ;; keep abs. path
   (parse-kw-formals
    check-conventions-rules
    create-aux-def)]))
;; FIXME: workaround for phase>0 bug in racket/runtime-path (and thus lazy-require)
;; Without this, dependencies don't get collected.
(require racket/runtime-path (for-meta 2 '#%kernel))
(define-runtime-module-path-index _unused_ 'syntax/parse/private/rep)

(provide define-conventions
         define-literal-set
         literal-set->predicate
         kernel-literals)

(define-syntax (define-conventions stx)

  (define-syntax-class header
    #:description "name or name with formal parameters"
    (pattern name:id
             #:with formals #'()
             #:attr arity (arity 0 0 null null))
    (pattern (name:id . formals)
             #:attr arity (parse-kw-formals #'formals #:context stx)))

  (syntax-parse stx
    [(define-conventions h:header rule ...)
     (let ()
       (define rules (check-conventions-rules #'(rule ...) stx))
       (define rxs (map car rules))
       (define dens0 (map cadr rules))
       (define den+defs-list
         (for/list ([den0 (in-list dens0)])
           (let-values ([(den defs) (create-aux-def den0)])
             (cons den defs))))
       (define dens (map car den+defs-list))
       (define defs (apply append (map cdr den+defs-list)))

       (define/with-syntax (rx ...) rxs)
       (define/with-syntax (def ...) defs)
       (define/with-syntax (parser ...)
         (map den:delayed-parser dens))
       (define/with-syntax (class-name ...)
         (map den:delayed-class dens))

       ;; FIXME: could move make-den:delayed to user of conventions
       ;; and eliminate from residual.rkt
       #'(begin
           (define-syntax h.name
             (make-conventions
              (quote-syntax get-parsers)
              (lambda ()
                (let ([class-names (list (quote-syntax class-name) ...)])
                  (map list
                       (list 'rx ...)
                       (map make-den:delayed
                            (generate-temporaries class-names)
                            class-names))))))
           (define get-parsers
             (lambda formals
               def ...
               (list parser ...)))))]))

(define-for-syntax (check-phase-level stx ctx)
  (unless (or (exact-integer? (syntax-e stx))
              (eq? #f (syntax-e stx)))
    (raise-syntax-error #f "expected phase-level (exact integer or #f)" ctx stx))
  stx)

(define-for-syntax (check-litset-list stx ctx)
  (syntax-case stx ()
    [(litset-id ...)
     (for/list ([litset-id (syntax->list #'(litset-id ...))])
       (let* ([val (and (identifier? litset-id)
                        (syntax-local-value/record litset-id literalset?))])
         (if val
             (cons litset-id val)
             (raise-syntax-error #f "expected literal set name" ctx litset-id))))]
    [_ (raise-syntax-error #f "expected list of literal set names" ctx stx)]))

;; check-literal-entry/litset : stx stx -> (list id id)
(define-for-syntax (check-literal-entry/litset stx ctx)
  (syntax-case stx ()
    [(internal external)
     (and (identifier? #'internal) (identifier? #'external))
     (list #'internal #'external)]
    [id
     (identifier? #'id)
     (list #'id #'id)]
    [_ (raise-syntax-error #f "expected literal entry" ctx stx)]))

(define-for-syntax (check-duplicate-literals stx imports lits)
  (let ([lit-t (make-hasheq)]) ;; sym => #t
    (define (check+enter! key blame-stx)
      (when (hash-ref lit-t key #f)
        (raise-syntax-error #f (format "duplicate literal: ~a" key) stx blame-stx))
      (hash-set! lit-t key #t))
    (for ([id+litset (in-list imports)])
      (let ([litset-id (car id+litset)]
            [litset (cdr id+litset)])
        (for ([entry (in-list (literalset-literals litset))])
          (check+enter! (car entry) litset-id))))
    (for ([lit (in-list lits)])
      (check+enter! (syntax-e (car lit)) (car lit)))))

(define-syntax (define-literal-set stx)
  (syntax-case stx ()
    [(define-literal-set name . rest)
     (let-values ([(chunks rest)
                   (parse-keyword-options
                    #'rest
                    `((#:literal-sets ,check-litset-list)
                      (#:phase ,check-phase-level)
                      (#:for-template)
                      (#:for-syntax)
                      (#:for-label))
                    #:incompatible '((#:phase #:for-template #:for-syntax #:for-label))
                    #:context stx
                    #:no-duplicates? #t)])
       (unless (identifier? #'name)
         (raise-syntax-error #f "expected identifier" stx #'name))
       (let ([relphase
              (cond [(assq '#:for-template chunks) -1]
                    [(assq '#:for-syntax chunks) 1]
                    [(assq '#:for-label chunks) #f]
                    [else (options-select-value chunks '#:phase #:default 0)])]
             [lits (syntax-case rest ()
                     [( (lit ...) )
                      (for/list ([lit (in-list (syntax->list #'(lit ...)))])
                        (check-literal-entry/litset lit stx))]
                     [_ (raise-syntax-error #f "bad syntax" stx)])]
             [imports (options-select-value chunks '#:literal-sets #:default null)])
         (check-duplicate-literals stx imports lits)
         (with-syntax ([((internal external) ...) lits]
                       [(litset-id ...) (map car imports)]
                       [relphase relphase])
           #`(begin
               (define phase-of-literals
                 (if 'relphase
                     (+ (phase-of-enclosing-module) 'relphase)
                     'relphase))
               (define-syntax name
                 (make-literalset
                  (append (literalset-literals (syntax-local-value (quote-syntax litset-id)))
                          ...
                          (list (list 'internal
                                      (quote-syntax external)
                                      (quote-syntax phase-of-literals))
                                ...))))
               (begin-for-syntax/once
                (for ([x (in-list (syntax->list #'(external ...)))])
                  (unless (identifier-binding x 'relphase)
                    (raise-syntax-error #f
                                        (format "literal is unbound in phase ~a~a~a"
                                                'relphase
                                                (case 'relphase
                                                  ((1) " (for-syntax)")
                                                  ((-1) " (for-template)")
                                                  ((#f) " (for-label)")
                                                  (else ""))
                                                " relative to the enclosing module")
                                        (quote-syntax #,stx) x))))))))]))

#|
Literal sets: The goal is for literals to refer to their bindings at

  phase 0 relative to the enclosing module

Use cases, explained:
1) module X with def-lit-set is required-for-syntax
     phase-of-mod-inst = 1
     phase-of-def = 0
     literals looked up at abs phase 1
       which is phase 0 rel to module X
2) module X with local def-lit-set within define-syntax
     phase-of-mod-inst = 1 (mod at 0, but +1 within define-syntax)
     phase-of-def = 1
     literals looked up at abs phase 0
       which is phase 0 rel to module X
3) module X with def-lit-set in phase-2 position (really uncommon case!)
     phase-of-mod-inst = 1 (not 2, apparently)
     phase-of-def = 2
     literals looked up at abs phase 0
       (that's why the weird (if (z?) 0 1) term)
|#

;; FIXME: keep one copy of each identifier (?)

(define-syntax (literal-set->predicate stx)
  (syntax-case stx ()
    [(literal-set->predicate litset-id)
     (let ([val (and (identifier? #'litset-id)
                     (syntax-local-value/record #'litset-id literalset?))])
       (unless val (raise-syntax-error #f "expected literal set name" stx #'litset-id))
       (let ([lits (literalset-literals val)])
         (with-syntax ([((_sym lit phase-var) ...) lits])
           #'(make-literal-set-predicate (list (list (quote-syntax lit) phase-var) ...)))))]))

(define (make-literal-set-predicate lits)
  (lambda (x [phase (syntax-local-phase-level)])
    (for/or ([lit (in-list lits)])
      (let ([lit-id (car lit)]
            [lit-phase (cadr lit)])
        (free-identifier=? x lit-id phase lit-phase)))))

;; Literal sets

(define-literal-set kernel-literals
  (begin
   begin0
   define-values
   define-syntaxes
   define-values-for-syntax ;; kept for compat.
   begin-for-syntax
   set!
   let-values
   letrec-values
   #%plain-lambda
   case-lambda
   if
   quote
   quote-syntax
   letrec-syntaxes+values
   with-continuation-mark
   #%expression
   #%plain-app
   #%top
   #%datum
   #%variable-reference
   module #%provide #%require
   #%plain-module-begin))
