;; A kind of lambda-lifting
;; (c) 1997-2011 PLT Scheme Inc

;; Finds liftable procedures, sets their `liftable' field, and
;;  replaces lexical variables for liftable procedures with
;;  globals variables. This transformation can be applied
;;  anytime (and multiple times) after "analyze.rkt" and before
;;  "closure.rkt".
;; Liftable lambdas are procedures whose free variables include only
;;  lexical variables bound to known-lifted procedures, primitive
;;  globals, and statics. (No namespace-sentsitive globals and no
;;  per-load statics.) Per-load liftable lambdas are lifted to per-load
;;  allocation; per-load liftable lambdas can have other per-load
;;  liftable lambdas and values in their closure.

;; TODO: get rid of let{rec}-bound variables that are useless
;;  because they are bound to lifted procedures.

;;; Annotatitons: ----------------------------------------------
;;    lambda - sets `liftable' in the procedure-code structure
;;; ------------------------------------------------------------

(module lift mzscheme
  (require mzlib/unit
           mzlib/list
           mzlib/etc)

  (require syntax/zodiac-sig)

  (require "sig.rkt"
           "../sig.rkt")

  (provide lift@)
  (define-unit lift@
      (import (prefix compiler:option: compiler:option^)
	      compiler:library^
	      compiler:cstructs^
	      (prefix zodiac: zodiac^)
	      compiler:zlayer^
	      compiler:known^
	      compiler:top-level^
	      compiler:analyze^
	      compiler:const^
	      compiler:closure^
	      compiler:driver^)
      (export compiler:lift^)

      (define lifting-allowed? #t)
      (define mutual-lifting-allowed? #t)
      (define per-load-lifting-only? #f)

      (define procedures null)

      (define single-module-mode? #f)
      (define (set-single-module-mode! v)
	(set! single-module-mode? v))

      ;; find-all-procedures!
      (define find-all-procedures!
	(letrec ([find!
		  (lambda (ast)
		    (cond
		     ;;-----------------------------------------------------------------
		     ;; CONSTANTS (A-VALUES)
		     [(zodiac:quote-form? ast) (void)]
		     
		     ;;-----------------------------------------------------------------
		     ;; VARIABLE REFERENCES (A-VALUES)
		     ;;
		     [(zodiac:bound-varref? ast) (void)]

		     [(zodiac:top-level-varref? ast) (void)]
		     
		     ;;--------------------------------------------------------------------
		     ;; LAMBDA EXPRESSIONS
		     ;;
		     [(zodiac:case-lambda-form? ast)
		      (set! procedures (cons (cons ast #f) procedures))
		      (for-each find! (zodiac:case-lambda-form-bodies ast))]
		     
		     ;;--------------------------------------------------------------
		     ;; LET EXPRESSIONS
		     [(zodiac:let-values-form? ast)
		      (find! (car (zodiac:let-values-form-vals ast)))
		      (find! (zodiac:let-values-form-body ast))]

		     ;;-----------------------------------------------------------------
		     ;; LETREC EXPRESSIONS
		     ;;
		     [(zodiac:letrec-values-form? ast)
		      (for-each find! (zodiac:letrec-values-form-vals ast))
		      (find! (zodiac:letrec-values-form-body ast))]
		     
		     ;;-----------------------------------------------------
		     ;; IF EXPRESSIONS
		     ;;
		     ;;  analyze the 3 branches.
		     ;;
		     [(zodiac:if-form? ast)
		      (find! (zodiac:if-form-test ast))
		      (find! (zodiac:if-form-then ast))
		      (find! (zodiac:if-form-else ast))]

		     ;;--------------------------------------------------------
		     ;; BEGIN EXPRESSIONS
		     ;;
		     ;; analyze the branches
		     [(zodiac:begin-form? ast)
		      (for-each find! (zodiac:begin-form-bodies ast))]
		     
		     ;;--------------------------------------------------------
		     ;; BEGIN0 EXPRESSIONS
		     ;;
		     ;; analyze the branches
		     [(zodiac:begin0-form? ast)
		      (find! (zodiac:begin0-form-first ast))
		      (find! (zodiac:begin0-form-rest ast))]
		     
		     ;;--------------------------------------------------------
		     ;; SET! EXPRESSIONS
		     ;;
		     ;; we analyze the target, which will register it as being
		     ;; mutable or used, as necessary.  Then we analyze the value.
		     ;;
		     [(zodiac:set!-form? ast)
		      (find! (zodiac:set!-form-val ast))]
		     
		     ;;---------------------------------------------------------
		     ;; DEFINE EXPRESSIONS
		     ;;
		     [(zodiac:define-values-form? ast)
		      (find! (zodiac:define-values-form-val ast))]
		     
		     ;;---------------------------------------------------------
		     ;; DEFINE-SYNTAXES
		     ;;
		     [(zodiac:define-syntaxes-form? ast)
		      (find! (zodiac:define-syntaxes-form-expr ast))]
		     
		     ;;-------------------------------------------------------------------
		     ;; APPLICATIONS
		     ;;  analyze all the parts, and note whether the rator is
		     ;;  a primitive;
		     ;;  if this is a call to a primitive, check the arity.
		     ;;
		     [(zodiac:app? ast)
		      (find! (zodiac:app-fun ast))
		      (for-each find! (zodiac:app-args ast))]
		     
		     ;;-------------------------------------------------------------------
		     ;; WITH-CONTINUATION-MARK
		     ;;
		     ;; analyze the key, val, and body
		     ;;
		     [(zodiac:with-continuation-mark-form? ast)
		      (find! (zodiac:with-continuation-mark-form-key ast))
		      (find! (zodiac:with-continuation-mark-form-val ast))
		      (find! (zodiac:with-continuation-mark-form-body ast))]

		     ;;-----------------------------------------------------------
		     ;; GLOBALS
		     ;;
		     [(zodiac:global-prepare? ast)
		      (find! (zodiac:global-prepare-vec ast))]
		     [(zodiac:global-lookup? ast)
		      (find! (zodiac:global-lookup-vec ast))]
		     [(zodiac:global-assign? ast)
		      (find! (zodiac:global-assign-vec ast))
		      (find! (zodiac:global-assign-expr ast))]
		     [(zodiac:safe-vector-ref? ast)
		      (find! (zodiac:safe-vector-ref-vec ast))]
		     
		     [else (compiler:internal-error
			    ast
			    (format "unsupported syntactic form (~a)"
				    (if (struct? ast)
					(vector-ref (struct->vector ast) 0)
					ast)))]))])
	  
	  (lambda (ast)
	    (set! procedures null)
	    (find! ast))))

      ;; Recursively determines the `liftable' field in the procedure
      ;; record. If a cycle is encountered, return 'cycle or 'pls-cycle
      ;; (the latter of any part of the cycle had "globals" in its
      ;; "closure")
      (define (get-liftable! lambda)
	(let ([code (get-annotation lambda)])
	  (if (eq? (procedure-code-liftable code) 'unknown-liftable)
	      (if (and lifting-allowed?
		       (or mutual-lifting-allowed? 
			   (set-empty? (code-free-vars code)))
		       (or single-module-mode?
			   (andmap (case-lambda 
				    [(var)
				     (or (not (mod-glob? var))
					 (let ([modname (mod-glob-modname var)])
					   (if (kernel-modname? modname)
					       #t
					       (not modname))))])
				   (set->list (code-global-vars code)))))
		  ;; Liftable only if there are no free (non-pls) global vars
		  (begin
		    ;; Mark this one in case we encounter a cycle:
		    (set-procedure-code-liftable! code 'cycle)
		    ;; Check each free variable
		    (let ([r (let loop ([l (set->list (code-free-vars code))]
					[pls? (or per-load-lifting-only?
						  (not (set-empty? (code-global-vars code))))]
					[cycle? #f])
			       (if (null? l)
				   ;; It's liftable, assuming any cycles are resolved
				   (cond
				    [(and pls? cycle?) 'pls-cycle]
				    [cycle? 'cycle]
				    [pls? 'pls]
				    [else 'static])
				   
				   ;; Check the free variable - references a liftable proc?
				   (let ([v (extract-ast-known-value (extract-varref-known-val (car l)))])
				     (if (zodiac:case-lambda-form? v)
					 (let ([vl (let ([l (get-liftable! v)])
						     (cond
						      [(top-level-varref/bind-from-lift? l)
						       ;; lifted in a previous phase
						       (if (top-level-varref/bind-from-lift-pls? l)
							   'pls
							   'static)]
						      [(pair? l)
						       ;; lifted already in this phase
						       (if (top-level-varref/bind-from-lift-pls? (car l))
							   'pls
							   'static)]
						      [else l]))])
					   (if vl
					       (loop (cdr l)
						     (or pls? (eq? vl 'pls) (eq? vl 'pls-cycle))
						     (or cycle? (eq? vl 'cycle) (eq? vl 'pls-cycle)))
					       #f))
					 #f))))])
		      (cond
		       [(not r) (set-procedure-code-liftable! code #f) 
			#f]
		       [(or (eq? r 'cycle) (eq? r 'pls-cycle))
			(set-procedure-code-liftable! code 'unknown-liftable) 
			r]
		       [(eq? r 'pls) (set-procedure-code-liftable! code 'pls) 
			'pls]
		       [else (set-procedure-code-liftable! code 'static) 
			     'static])))
		  (begin
		    (set-procedure-code-liftable! code #f)
		    #f))
	      (procedure-code-liftable code))))

      (define (set-liftable! lambda)
	(unless (top-level-varref/bind-from-lift? (procedure-code-liftable (get-annotation lambda)))
	  (let ([v (get-liftable! lambda)])
	    (when v
	      (let ([pls? (or (eq? v 'pls) (eq? v 'pls-cycle))])
		(when (compiler:option:verbose)
		  (compiler:warning lambda (format "found static procedure~a"
						   (if pls? " (per-load)" ""))))
		(compiler:add-lifted-lambda! lambda pls?))))))

      ;; lift-lambdas! uses the `liftable' procedure annotation with known-value
      ;;  analysis to replace lexical variables referencing known liftable
      ;;  procedures with top-level-varrefs referencing the lifted
      ;;  procedure
      ;; Since per-load lifting rarranges global variable sets, we
      ;;  recompute them.
      ;; Returns (cons ast global-var-set)
      (define lift-lambdas!
	(letrec ([lift!
		  (lambda (ast code)
		    (when (compiler:option:debug)
		      (zodiac:print-start! (debug:get-port) ast)
		      (newline (debug:get-port)))
		    
		    (cond
		     
		     ;;-----------------------------------------------------------------
		     ;; CONSTANTS (A-VALUES)
		     [(zodiac:quote-form? ast) ast]
		     
		     ;;-----------------------------------------------------------------
		     ;; VARIABLE REFERENCES (A-VALUES)
		     ;;
		     [(zodiac:bound-varref? ast)
		      (let ([v (extract-ast-known-value (extract-varref-known-val ast))])
						
			(if (zodiac:case-lambda-form? v)

			    (let ([lifted (let ([l (procedure-code-liftable (get-annotation v))])
					    (if (pair? l) (car l) l))])
			      (if lifted
				  
				  ;; The procedure was lifted
				  (begin
				    (when code
				      (remove-code-free-vars! code (make-singleton-set 
								    (zodiac:bound-varref-binding ast))))
				    (when (top-level-varref/bind-from-lift-pls? lifted)
				      (add-global! const:the-per-load-statics-table))
				    lifted)
				  
				  ;; No change
				  ast))

			    ;; No change
			    ast))]
		     
		     [(zodiac:top-level-varref? ast)

		      (cond
		       [(varref:has-attribute? ast varref:primitive)
			(void)]
		       [(varref:has-attribute? ast varref:per-load-static)
			(add-global! const:the-per-load-statics-table)]
		       [(varref:has-attribute? ast varref:static)
			(void)]
		       [else (add-global! (compiler:add-global-varref! ast))])
		      
		      ast]
		     
		     ;;--------------------------------------------------------------------
		     ;; LAMBDA EXPRESSIONS
		     ;;    analyze the bodies, and set binding info for the binding vars
		     ;;
		     [(zodiac:case-lambda-form? ast)
		      (let ([code (get-annotation ast)]
			    [save-globals globals])
			;; We're recomputing globals...
			(set-code-global-vars! code empty-set)

			(zodiac:set-case-lambda-form-bodies! 
			 ast 
			 (map (lambda (b ccode)
				(set! globals empty-set)
				;; Analyze case
				(let ([b (lift! b ccode)])
				  ;; Set and merge globals
				  (set-code-global-vars! ccode globals)
				  (set-code-global-vars! code
							 (set-union globals
								    (code-global-vars code)))

				  b))
			      (zodiac:case-lambda-form-bodies ast)
			      (procedure-code-case-codes code)))
			
			;; If it being lifted ON THIS PASS, the value of `lifted' will be a list;
			;;  in that case, return the new static varref
			(let ([lifted (procedure-code-liftable code)])
			  (if (or (not lifted) (top-level-varref/bind-from-lift? lifted))
			      
			      ;; Not lifted (or not on this pass)
			      (begin
				(set! globals (set-union save-globals (code-global-vars code)))
				ast)
			      
			      ;; Lifting on this pass
			      (let ([lifted (car lifted)])
				(set-procedure-code-liftable! code lifted)
				
				(if (top-level-varref/bind-from-lift-pls? lifted)
				    (set! globals (set-union-singleton 
						   save-globals
						   const:the-per-load-statics-table))
				    (set! globals save-globals))
				
				lifted))))]

		     ;;--------------------------------------------------------------
		     ;; LET EXPRESSIONS
		     ;;
		     [(zodiac:let-values-form? ast)
		      (let* ([val (lift! (car (zodiac:let-values-form-vals ast)) code)])
                        (zodiac:set-let-values-form-vals!
                         ast
                         (cons val
                               (cdr (zodiac:let-values-form-vals ast))))
			
			;; lift in body expressions
			(let ([body (lift! (zodiac:let-values-form-body ast) code)])

			  (if (and (= 1 (length (car (zodiac:let-values-form-vars ast))))
				   (top-level-varref/bind-from-lift? val)
				   (not (binding-mutable? (get-annotation (caar (zodiac:let-values-form-vars ast))))))

			      ;; Let binding value is a lifted procedure, drop the variable
			      (let ([var (caar (zodiac:let-values-form-vars ast))])
				(remove-local-var! code var)
				body)

			      (begin
				(zodiac:set-let-values-form-body! ast body)
				ast))))]

		     ;;-----------------------------------------------------------------
		     ;; LETREC EXPRESSIONS
		     ;;
		     [(zodiac:letrec-values-form? ast)
		      
		      (let* ([varses (zodiac:letrec-values-form-vars ast)]
			     [vals (zodiac:letrec-values-form-vals ast)])
			
			(zodiac:set-letrec-values-form-vals! 
			 ast 
			 (map (lambda (val) (lift! val code)) vals))
			
			(zodiac:set-letrec-values-form-body!
			 ast
			 (lift! (zodiac:letrec-values-form-body ast) code))
			
			(let loop ([varses varses][vals (zodiac:letrec-values-form-vals ast)]
				   [vss-accum null][vs-accum null])
			  (if (null? varses)

			      (begin
				(zodiac:set-letrec-values-form-vars! ast (reverse vss-accum))
				(zodiac:set-letrec-values-form-vals! ast (reverse vs-accum)))

			      (let ([vars (car varses)]
				    [val (car vals)])
				(if (and (= 1 (length vars))
					 (top-level-varref/bind-from-lift? val))
				    ;; Let binding value is a lifted procedure, drop the variable
				    (begin
				      (remove-local-var! code (car vars))
				      (loop (cdr varses) (cdr vals) vss-accum vs-accum))

				    ;; Normal binding
				    (loop (cdr varses) (cdr vals) (cons vars vss-accum) (cons val vs-accum))))))

			(if (null? (zodiac:letrec-values-form-vars ast))

			    ;; All binding values were lifted; return the body
			    (zodiac:letrec-values-form-body ast)
			    
			    ast))]
		     
		     ;;-----------------------------------------------------
		     ;; IF EXPRESSIONS
		     ;;
		     ;;  analyze the 3 branches.
		     ;;
		     [(zodiac:if-form? ast)
		      (zodiac:set-if-form-test! ast (lift! (zodiac:if-form-test ast) code))
		      (let ([then (lift! (zodiac:if-form-then ast) code)]
			    [else (lift! (zodiac:if-form-else ast) code)])
			(zodiac:set-if-form-then! ast then)
			(zodiac:set-if-form-else! ast else)
			
			ast)]
		     
		     ;;--------------------------------------------------------
		     ;; BEGIN EXPRESSIONS
		     ;;
		     [(zodiac:begin-form? ast)
		      
		      (zodiac:set-begin-form-bodies!
		       ast
		       (map (lambda (b) (lift! b code))
			    (zodiac:begin-form-bodies ast)))
		      
		      ast]
		     
		     
		     ;;--------------------------------------------------------
		     ;; BEGIN0 EXPRESSIONS
		     ;;
		     ;; analyze the branches
		     [(zodiac:begin0-form? ast)
		      (zodiac:set-begin0-form-first! ast (lift! (zodiac:begin0-form-first ast) code))
		      (zodiac:set-begin0-form-rest! ast (lift! (zodiac:begin0-form-rest ast) code))

		      ast]
		     
		     ;;--------------------------------------------------------
		     ;; SET! EXPRESSIONS
		     ;;
		     ;;
		     [(zodiac:set!-form? ast)

		      ;; Possibly a top-level-varref; put it in the global-var set
		      (lift! (zodiac:set!-form-var ast) code)

		      (zodiac:set-set!-form-val! 
		       ast 
		       (lift! (zodiac:set!-form-val ast) code))
		      
		      ast]
		     
		     ;;---------------------------------------------------------
		     ;; DEFINE EXPRESSIONS
		     ;;
		     [(zodiac:define-values-form? ast)
		      
		      ;; Top-level-varrefs; put them in the global-var set
		      (for-each (lambda (v) (lift! v code)) (zodiac:define-values-form-vars ast))

		      (zodiac:set-define-values-form-val! 
		       ast
		       (lift! (zodiac:define-values-form-val ast) code))
		      
		      ast]
		     
		     ;;---------------------------------------------------------
		     ;; DEFINE-SYNTAXES
		     ;;
		     [(zodiac:define-syntaxes-form? ast)
		      
		      (zodiac:set-define-syntaxes-form-expr!
		       ast
		       (lift! (zodiac:define-syntaxes-form-expr ast) code))
		      
		      ast]
		     
		     ;;-------------------------------------------------------------------
		     ;; APPLICATIONS
		     ;;  analyze all the parts, and note whether the rator is
		     ;;  a primitive;
		     ;;  if this is a call to a primitive, check the arity.
		     ;;
		     [(zodiac:app? ast)
		      
		      (let* ([fun (lift! (zodiac:app-fun ast) code)]
			     [args (map (lambda (arg) (lift! arg code))
					(zodiac:app-args ast))])
			(zodiac:set-app-fun! ast fun)
			(zodiac:set-app-args! ast args))

		      ast]
		     
		     ;;-------------------------------------------------------------------
		     ;; WITH-CONTINUATION-MARK
		     ;;
		     ;; analyze the key, val, and body
		     ;;
		     [(zodiac:with-continuation-mark-form? ast)

		      (zodiac:set-with-continuation-mark-form-key!
		       ast
		       (lift! (zodiac:with-continuation-mark-form-key ast) code))
		      
		      (zodiac:set-with-continuation-mark-form-val!
		       ast
		       (lift! (zodiac:with-continuation-mark-form-val ast) code))
		      
		      (zodiac:set-with-continuation-mark-form-body!
		       ast
		       (lift! (zodiac:with-continuation-mark-form-body ast) code))

		      ast]

		     ;;-----------------------------------------------------------
		     ;; GLOBALS
		     ;;
		     [(zodiac:global-prepare? ast)
		      (zodiac:set-global-prepare-vec!
		       ast
		       (lift! (zodiac:global-prepare-vec ast) code))
		      ast]
		     [(zodiac:global-lookup? ast)
		      (zodiac:set-global-lookup-vec!
		       ast
		       (lift! (zodiac:global-lookup-vec ast) code))
		      ast]
		     [(zodiac:global-assign? ast)
		      (zodiac:set-global-assign-vec!
		       ast
		       (lift! (zodiac:global-assign-vec ast) code))
		      (zodiac:set-global-assign-expr!
		       ast
		       (lift! (zodiac:global-assign-expr ast) code))
		      ast]
		     [(zodiac:safe-vector-ref? ast)
		      (zodiac:set-safe-vector-ref-vec!
		       ast
		       (lift! (zodiac:safe-vector-ref-vec ast) code))
		      ast]

		     [else (compiler:internal-error
			    ast
			    (format "unsupported syntactic form (~a)"
				    (if (struct? ast)
					(vector-ref (struct->vector ast) 0)
					ast)))]))]
		 [remove-local-var! (lambda (code var)
				      (let ([vars (make-singleton-set var)])
					(set-code-local-vars! code (set-minus (code-local-vars code) vars))
					(set-code-captured-vars! code (set-minus (code-captured-vars code) vars))
					;; Is it a case code?
					(when (case-code? code)
					  (let ([code (code-parent code)])
					    (remove-local-var! code var)))))]
		 [globals empty-set]
		 [add-global! (lambda (v) (set! globals (set-union-singleton globals v)))])
	  
	  (lambda (ast code)
	    ;; Find all the procedures
	    (find-all-procedures! ast)

	    ;; If we marked it as unliftable before, mark it as unknown
	    ;;  now because we'll check again:
	    (for-each
	     (lambda (l)
	       (let ([l (car l)])
		 (let ([c (get-annotation l)])
		   (unless (procedure-code-liftable c)
		     (set-procedure-code-liftable! c 'unknown-liftable)))))
	     procedures)

	    ;; Set liftable flags
	    (for-each (lambda (l)
			(let ([l (car l)])
			  (set-liftable! l)))
		      procedures)

	    (set! globals empty-set)
	    (let ([ast (lift! ast code)])
	      (cons ast globals)))))))
