private: Yes


;;;; guile-shadow.scm - warn about shadowed variables in Guile code
;;;;
;;;; To the extent possible under law, the author(s) have dedicated all
;;;; copyright and related and neighboring rights to this software to
;;;; the public domain worldwide. This software is distributed without
;;;; any warranty.
;;;;
;;;; You should have received a copy of the CC0 Public Domain
;;;; Dedication along with this software. If not, see
;;;; <http://creativecommons.org/publicdomain/zero/1.0/>.

(use-modules (language tree-il)
             (language tree-il analyze)
             (ice-9 receive)
             (srfi srfi-1)
             (srfi srfi-69)
             (system base compile)
             (system base message)
             (system base language)
             (system base syntax))

;; This program is meant to warn its user about shadowed and unbound
;; variables in their Guile code. Unbound variable analysis is already
;; done by Guile's compiler on its Tree-IL (Tree Intermediate
;; Language). This is reused and a new analysis for shadowed variables
;; is implemented, along with a simple loop that compiles Scheme to
;; Tree-IL and runs the analysis.

;; Tree-IL analyses are represented by the `tree-analysis' record
;; type, however this is not exported. This extracts it from the
;; module.
(define make-tree-analysis
  (let ((module (resolve-module '(language tree-il analyze) #f #f #:ensure #f)))
    (module-ref module '%make-tree-analysis-procedure)))

;; For reporting warnings, Guile has its own system implemented in
;; (system base message). Unfortunately it cannot be easily extended,
;; therefore we have to reimplement its parts.

;; This really should be available, but it is not exported. Again,
;; same trick as above.
(define location-string
  (let ((module (resolve-module '(system base message) #f #f #:ensure #f)))
    (module-ref module 'location-string)))

(define (warn-shadow name location)
  (format (current-warning-port)
          "~A~A: warning: shadowed variable `~S'~%"
          (fluid-ref *current-warning-prefix*)
          (location-string location)
          name))

;; The actual analysis is done by traversing the Tree-IL tree. We keep
;; track of how many times a name is used in the current environment
;; by recording it in a hash table. It is very simple.

(define (name-removed tbl name loc)
  (hash-table-update!/default tbl name 1- 1))

(define (name-add tbl name loc)
  (hash-table-update!/default
   tbl
   name
   (lambda (v)
     (when (> v 0)
       (warn-shadow name loc))
     (1+ v))
   0))

(define (handle-names f tbl names loc)
  (for-each (lambda (name) (f tbl name loc)) names))

(define (dispatch-tree-il-names f tbl node locs)
  (let ((src (find pair? locs)))
    (record-case node
      ((<lambda-case> req opt)
       (handle-names f tbl req src)
       (when opt
         (handle-names f tbl opt src)))
      ((<let> names)
       (handle-names f tbl names src))
      ((<letrec> names)
       (handle-names f tbl names src))
      ((<fix> names)
       (handle-names f tbl names src))
      (else node))
    tbl))

;; A Tree-IL analysis is made of four elements:
;; 1. A procedure invoked upon entering a node.
;; 2. A procedure invoked after every children of the node has been visited.
;; 3. A procedure invoked after every node has been visited.
;; 4. An initial value.
(define shadowed-variable-analysis
  (make-tree-analysis
   (lambda (node tbl env locs)
     (dispatch-tree-il-names name-add tbl node locs))
   (lambda (node tbl env locs)
     (dispatch-tree-il-names name-removed tbl node locs))
   (lambda (tbl env)
     (for-each (lambda (k) (hash-table-delete! tbl k))
               (hash-table-keys tbl))
     #t)
   (make-hash-table)))

;; Main loop that reads in Scheme expressions, compiles them to
;; Tree-IL and when it is all done, runs the analysers on it.

(define (parse-compile-analyze)
  (let ((compiler (compute-compiler
                   'scheme
                   'tree-il
                   (default-optimization-level)
                   (default-warning-level)
                   '())))
    (let loop ((x (read))
               (exps '())
               (env (default-environment 'scheme)))
      (if (eof-object? x)
          (let ((tree ((language-joiner (lookup-language 'tree-il)) (reverse exps) env)))
            (with-fluids ((*current-warning-prefix* ">>> "))
              (analyze-tree (list shadowed-variable-analysis
                                  shadowed-toplevel-analysis
                                  unbound-variable-analysis)
                            tree
                            env)))
          (receive (exp env cenv) (compiler x env)
            (loop (read) (cons exp exps) cenv))))))

(if (pair? (cdr (command-line)))
    (for-each
     (lambda (file)
       (with-input-from-file file
         parse-compile-analyze))
     (cdr (command-line)))
    (parse-compile-analyze))

;;; Commentary
;;
;; Source locations in the warnings point to the start of the
;; expression, not the actual offending names. As far as I can tell,
;; this is the closest position to the error that is saved while
;; reading and expanding the Scheme expressions.
;;
;; With more accurate source positions, highlighting the shadowed
;; variable could be helpful too.
;;
;; This program was written for Scheme but since the actual analysis
;; is done on Tree-IL, therefore any language that compiles to it
;; should work with minimal changes (changing the reader and the input
;; language of `compute-compiler').
;;
;; It would be great if `guild' had a command that compiles code to
;; Tree-IL without producing an output file. This way the existing
;; analyses could be used for easy diagnosis. It would be even better
;; if there was a way to load extra modules that extended the
;; available analyses.