#lang racket/base (require racket/contract/base racket/cmdline racket/match syntax/modcollapse "private/get-references.rkt" "private/nom-use-alg.rkt" "private/util.rkt") (provide/contract [check-requires (->* (module-path?) (#:show-keep? any/c #:show-bypass? any/c #:show-drop? any/c #:show-uses? any/c) void?)] [show-requires (-> module-path? list?)] [mpi->key (-> module-path-index? any/c)]) #| ========== Notes Ignore recommendations to DROP or BYPASS modules with side effects. Read the section below (How it works) and also see util/moduledb.rkt for whitelisting side-effectful modules. The script is not intelligent about the language, which causes certain spurious recommendations to appear frequently. For example, DROP scheme/mzscheme at 1 means that the module's language is mzscheme, which automatically inserts (require-for-syntax scheme/mzscheme). It's infeasible to remove it except by rewriting the module in scheme/base or racket/base. ========== How it works Determining whether a require is actually useless is impossible: a module may be required for compile-time side effect only, and there's no way to monitor that. Here are some approximations that are feasible to calculate: NOM-USES = A require R is "used" by a module M if, during the compilation of M, a reference is resolved to a binding exported by R. DEF-USES = A require R is "used" by a module M if, during the compilation of M, a reference is resolved to a binding defined in R. The limitations: - misses side-effects - misses identifiers recognized via 'free-identifier=?' (But those should be recorded as 'disappeared-use anyway.) ========== TODO Handle for-label. Let user provide database of modules that should never be dropped, eg because they have side effects. - wouldn't it be awesome if this db could be a datalog program? - start simpler, though Ambitious mode could analyze module and recommend ways to split module into independent submodules. |# ;; ======================================== #| A recommendation is one of (list 'keep module-path-index phase Refs) (list 'bypass module-path-index phase RefineTable) (list 'drop module-path-index phase) |# ;; analyze-requires : module-path -> (listof recommendation) (define (analyze-requires mod-path) (let-values ([(compiled deriv) (get-module-code/trace mod-path)]) (nom-use-alg (deriv->refs deriv) compiled))) ;; ======================================== #| A displayed-recommendation is one of (list 'keep module-path phase) (list 'bypass module-path phase (listof (list module-path phase))) (list 'drop module-path phase) A displayed-recommendation is similar to a recommendation, but converts the module-path-indexes to module paths, omits the use-lists, and simplifies the replacements lists. |# ;; show-requires: module-path -> (listof displayed-recommendation) (define (show-requires mod-path) (for/list ([entry (in-list (analyze-requires mod-path))]) (match entry [(list 'keep mpi phase uses) (list 'keep (mpi->key mpi) phase)] [(list 'bypass mpi phase bypass) (list 'bypass (mpi->key mpi) phase (let ([bypass (flatten-bypass bypass)]) (for/list ([(modpath+reqphase inner) (in-hash bypass)]) (list (car modpath+reqphase) (cdr modpath+reqphase) (any-renames? (imps->use-table inner))))))] [(list 'drop mpi phase) (list 'drop (mpi->key mpi) phase)]))) ;; ======================================== (define (check-requires mod #:show-keep? [show-keep? #t] #:show-bypass? [show-bypass? #t] #:show-drop? [show-drop? #t] #:show-uses? [show-uses? #f]) (define (show-bypass mpi bypass) (for ([(modname+reqphase inner) (flatten-bypass bypass)]) (let ([modname (car modname+reqphase)] [reqphase (cdr modname+reqphase)] [use-table (imps->use-table inner)]) (printf " TO ~s at ~s~a\n" modname reqphase (cond [(any-renames? use-table) " WITH RENAMING"] [else ""])) (when show-uses? (show-uses use-table 4))))) (let ([recs (analyze-requires mod)]) (for ([rec (in-list recs)]) (match rec [(list 'keep mpi phase uses) (when show-keep? (printf "KEEP ~s at ~s\n" (mpi->key mpi) phase) (when show-uses? (show-uses (imps->use-table uses) 2)))] [(list 'bypass mpi phase bypass) (when show-bypass? (printf "BYPASS ~s at ~s\n" (mpi->key mpi) phase) (show-bypass mpi bypass))] [(list 'drop mpi phase) (when show-drop? (printf "DROP ~s at ~s\n" (mpi->key mpi) phase))])))) ;; ---- ;; flatten-bypass : RefineTable -> hash[(cons module-path int) => Imps] (define (flatten-bypass table) (let ([flat-table (make-hash)]) ;; hash[(cons module-path int) => Imps] (let loop ([table table] [mpi-ctx null]) (for ([(mod+reqphase inner) (in-hash table)]) (let* ([mod (car mod+reqphase)] [reqphase (cdr mod+reqphase)] [mpis (cons mod mpi-ctx)]) (cond [(hash? inner) (loop inner mpis)] [else ;; key may already exist, eg with import diamonds; so append (let* ([modpath (mpi-list->module-path mpis)] [key (cons modpath reqphase)]) (hash-set! flat-table key (append inner (hash-ref flat-table key null))))])))) flat-table)) (define (ref->symbol r) (match r [(ref phase id mode (list dm ds nm ns dp ips np)) (cond [id (syntax-e id)] [else ns])])) ;; imps->use-table : Imps -> hash[(list phase prov-sym ref-sym) => (listof mode)] (define (imps->use-table imps) (let ([table (make-hash)]) (for ([i (in-list imps)]) (match i [(imp _m _p prov-sym _prov-phase r) (let* ([phase (ref-phase r)] [ref-sym (ref->symbol r)] [mode (ref-mode r)] [key (list phase prov-sym ref-sym)] [modes (hash-ref table key null)]) (unless (memq mode modes) (hash-set! table key (cons mode modes))))])) table)) ;; any-renames? : use-table -> boolean (define (any-renames? use-table) (for/or ([key (in-hash-keys use-table)]) (match key [(list phase prov-sym ref-sym) (not (eq? prov-sym ref-sym))]))) ;; show-uses : use-table nat -> void (define (show-uses use-table indent) (let* ([unsorted (for/list ([(key modes) (in-hash use-table)]) (cons key (sort modes < #:key mode->nat)))] [sorted (sort unsorted (lambda (A B) (let ([pA (car A)] [pB (car B)]) (or (< pA pB) (and (= pA pB) (let ([strA (symbol->string (cadr A))] [strB (symbol->string (cadr B))]) (string