;;; optimize.scm -- flic optimizer ;;; ;;; author : Sandra Loosemore ;;; date : 7 May 1992 ;;; ;;; ;;; The optimizer does these kinds of program transformations: ;;; ;;; * remove unreferenced variable bindings. ;;; ;;; * constant folding and various other kinds of compile-time ;;; evaluation. ;;; ;;; * beta reduction (replace references to variables bound to simple ;;; expressions with the expression) ;;; ;;; Since some of the optimizations can make additional transformations ;;; possible, we want to make multiple iteration passes. But since each ;;; pass is likely to have diminishing benefits, we don't want to keep ;;; iterating indefinitely. So establish a fairly arbitrary cutoff point. ;;; The value is based on empirical results from compiling the prelude. (define *max-optimize-iterations* 5) (define *optimize-foldr-iteration* 0) ; when to inline foldr (define *optimize-build-iteration* 0) ; when to inline build (define *current-optimize-iteration* 0) ;;; Flags for enabling various optimizations (define *all-optimizers* '(foldr inline constant lisp)) (define *optimizers* *all-optimizers*) ;;; Used to note whether we are doing the various optimizations (define-local-syntax (do-optimization? o) `(memq ,o (dynamic *optimizers*))) (define *do-foldr-optimizations* (do-optimization? 'foldr)) (define *do-inline-optimizations* (do-optimization? 'inline)) (define *do-constant-optimizations* (do-optimization? 'constant)) ;;; If the foldr optimization is enabled, bind the corresponding ;;; variables to these values instead of the defaults. (define *foldr-max-optimize-iterations* 15) (define *foldr-optimize-foldr-iteration* 8) (define *foldr-optimize-build-iteration* 5) ;;; Some random other variables (define *structured-constants* '()) (define *structured-constants-table* '#f) (define *lambda-depth* 0) (define *local-bindings* '()) ;;; This is for doing some crude profiling. ;;; Comment out the body of the macro to disable profiling. ;;; Here are current counts from compiling the prelude: ;;; (LET-REMOVE-UNUSED-BINDING . 5835) ;;; (REF-INLINE-SINGLE-REF . 2890) ;;; (REF-INLINE . 2692) ;;; (LET-EMPTY-BINDINGS . 2192) ;;; (APP-LAMBDA-TO-LET . 1537) ;;; (APP-MAKE-SATURATED . 416) ;;; (LET-HOIST-RETURN-FROM . 310) ;;; (CASE-BLOCK-IDENTITY . 273) ;;; (CASE-BLOCK-DEAD-CODE . 234) ;;; (CASE-BLOCK-TO-IF . 212) ;;; (SEL-FOLD-VAR . 211) ;;; (APP-HOIST-LET . 190) ;;; (LET-HOIST-LAMBDA . 181) ;;; (FOLDR-INLINE . 176) ;;; (AND-UNARY . 172) ;;; (LAMBDA-COMPRESS . 168) ;;; (APP-FOLD-SELECTOR . 141) ;;; (BUILD-INLINE-LAMBDA . 134) ;;; (LET-COMPRESS . 134) ;;; (IF-FOLD . 128) ;;; (INTEGER-TO-INT-CONSTANT-FOLD . 124) ;;; (AND-COMPRESS . 94) ;;; (APP-COMPRESS . 93) ;;; (FOLDR-CONS-IDENTITY . 69) ;;; (IF-COMPRESS-TEST . 65) ;;; (IF-HOIST-LAMBDA . 61) ;;; (APP-HOIST-STRUCTURED-CONSTANT . 60) ;;; (FOLDR-PRIM-APPEND-INLINE . 55) ;;; (FOLDR-BUILD-IDENTITY . 40) ;;; (CASE-BLOCK-DISCARD-REDUNDANT-TEST . 37) ;;; (FOLDR-NIL-IDENTITY . 36) ;;; (LET-HOIST-INVARIANT-ARGS . 30) ;;; (FOLDR-HOIST-LET . 28) ;;; (CON-NUMBER-FOLD-TUPLE . 21) ;;; (FOLDR-CONS-NIL-IDENTITY . 15) ;;; (AND-CONTAINS-TRUE . 14) ;;; (IF-IDENTITY-INVERSE . 8) ;;; (IF-HOIST-RETURN-FROM . 7) ;;; (CASE-BLOCK-HOIST-LET . 7) ;;; (INTEGER-TO-INT-IDENTITY . 7) ;;; (APP-PACK-IDENTITY . 2) ;;; (CON-NUMBER-FOLD . 2) ;;; (IF-IDENTITY . 2) ;;; (INT-TO-INTEGER-CONSTANT-FOLD . 2) ;;; (LET-HOIST-STRUCTURED-CONSTANT . 1) (define-local-syntax (record-hack type . args) (declare (ignore args)) `',type ; `(record-hack-aux ,type ,@args) ) (define *hacks-done* '()) (define (record-hack-aux type . args) ;; *** debug ;; (format '#t "~s ~s~%" type args) (declare (ignore args)) (let ((stuff (assq type (car (dynamic *hacks-done*))))) (if stuff (incf (cdr stuff)) (push (cons type 1) (car (dynamic *hacks-done*)))))) (define (total-hacks) (let ((totals '())) (dolist (alist *hacks-done*) (dolist (entry alist) (let ((stuff (assq (car entry) totals))) (if stuff (setf (cdr stuff) (+ (cdr stuff) (cdr entry))) (push (cons (car entry) (cdr entry)) totals))))) totals)) ;;; This is the main entry point. (define (optimize-top object) (dynamic-let ((*structured-constants* '()) (*structured-constants-table* (make-table)) (*lambda-depth* 0) (*local-bindings* '()) (*do-inline-optimizations* (do-optimization? 'inline)) (*do-constant-optimizations* (do-optimization? 'constant)) (*max-optimize-iterations* (if (do-optimization? 'foldr) (dynamic *foldr-max-optimize-iterations*) (dynamic *max-optimize-iterations*))) (*optimize-foldr-iteration* (if (do-optimization? 'foldr) (dynamic *foldr-optimize-foldr-iteration*) (dynamic *optimize-foldr-iteration*))) (*optimize-build-iteration* (if (do-optimization? 'foldr) (dynamic *foldr-optimize-build-iteration*) (dynamic *optimize-build-iteration*)))) (setf *hacks-done* '()) (dotimes (i (dynamic *max-optimize-iterations*)) (dynamic-let ((*current-optimize-iteration* i)) ;; debug (*duplicate-object-table* (make-table))) (when (memq 'optimize-extra (dynamic *printers*)) (format '#t "~%Optimize pass ~s:" i) (pprint object)) (push '() *hacks-done*) (setf object (optimize-flic-let-aux object '#t)))) (setf (flic-let-bindings object) (nconc (nreverse (dynamic *structured-constants*)) (flic-let-bindings object)))) (install-uninterned-globals (flic-let-bindings object)) (postoptimize object) object) (define-flic-walker optimize (object)) ;;; debugging stuff ;;; ;;; (define *duplicate-object-table* (make-table)) ;;; ;;; (define (new-optimize object) ;;; (if (table-entry (dynamic *duplicate-object-table*) object) ;;; (error "Duplicate object ~s detected." object) ;;; (begin ;;; (setf (table-entry (dynamic *duplicate-object-table*) object) '#t) ;;; (old-optimize object)))) ;;; ;;; (lisp:setf (lisp:symbol-function 'old-optimize) ;;; (lisp:symbol-function 'optimize)) ;;; (lisp:setf (lisp:symbol-function 'optimize) ;;; (lisp:symbol-function 'new-optimize)) (define (optimize-list objects) (optimize-list-aux objects) objects) (define (optimize-list-aux objects) (if (null? objects) '() (begin (setf (car objects) (optimize (car objects))) (optimize-list-aux (cdr objects))))) ;;; Compress nested lambdas. This hack is desirable because saturating ;;; applications within the lambda body effectively adds additional ;;; parameters to the function. ;;; *** Maybe this should look for hoistable constant lambdas too. (define-optimize flic-lambda (object) (let ((vars (flic-lambda-vars object))) (dynamic-let ((*lambda-depth* (1+ (dynamic *lambda-depth*))) (*local-bindings* (cons vars (dynamic *local-bindings*)))) (dolist (var vars) (setf (var-referenced var) 0)) (let ((new-body (optimize (flic-lambda-body object)))) (setf (flic-lambda-body object) new-body) (cond ((is-type? 'flic-lambda new-body) (record-hack 'lambda-compress) (setf (flic-lambda-vars object) (nconc (flic-lambda-vars object) (flic-lambda-vars new-body))) (setf (flic-lambda-body object) (flic-lambda-body new-body))) (else '#f)) object)))) ;;; For let, first mark all variables as unused and check for "simple" ;;; binding values that permit beta reduction. Then walk the subexpressions. ;;; Finally discard any bindings that are still marked as unused. ;;; *** This fails to detect unused recursive variables. (define-optimize flic-let (object) (optimize-flic-let-aux object '#f)) (define (optimize-flic-let-aux object toplevel?) (let ((bindings (flic-let-bindings object)) (recursive? (flic-let-recursive? object))) ;; *** This handling of *local-bindings* isn't quite right since ;; *** it doesn't account for the sequential nature of bindings ;; *** in a non-recursive let, but it's close enough. We won't ;; *** get any semantic errors, but it might miss a few optimizations. (dynamic-let ((*local-bindings* (if (and recursive? (not toplevel?)) (cons bindings (dynamic *local-bindings*)) (dynamic *local-bindings*)))) (optimize-flic-let-bindings bindings recursive? toplevel?) (dynamic-let ((*local-bindings* (if (and (not recursive?) (not toplevel?)) (cons bindings (dynamic *local-bindings*)) (dynamic *local-bindings*)))) (setf (flic-let-body object) (optimize (flic-let-body object)))) ;; Check for unused bindings and other rewrites. ;; Only do this for non-toplevel lets. (if toplevel? object (optimize-flic-let-rewrite object bindings recursive?))))) (define (optimize-flic-let-bindings bindings recursive? toplevel?) ;; Initialize (dolist (var bindings) (setf (var-referenced var) 0) (setf (var-fn-referenced var) 0) (when (is-type? 'flic-lambda (var-value var)) (dolist (v (flic-lambda-vars (var-value var))) (setf (var-arg-invariant? v) '#t) (setf (var-arg-invariant-value v) '#f)))) ;; Traverse value subforms (do ((bindings bindings (cdr bindings))) ((null? bindings) '#f) (let* ((var (car bindings)) (val (var-value var))) (if (and (is-type? 'flic-app val) (dynamic *do-constant-optimizations*) (let ((fn (flic-app-fn val)) (args (flic-app-args val))) (if recursive? (structured-constant-app-recursive? fn args bindings (list var)) (structured-constant-app? fn args)))) ;; Variable is bound to a structured constant. If this ;; isn't already a top-level binding, replace the value ;; of the constant with a reference to a top-level variable ;; that is in turn bound to the constant expression. ;; binding to top-level if this is a new constant. ;; *** Maybe we should also look for variables bound ;; *** to lambdas, that can also be hoisted to top level. (when (not toplevel?) (multiple-value-bind (con args cvar) (enter-structured-constant-aux val '#t) (record-hack 'let-hoist-structured-constant) (if cvar (setf (var-value var) (make-flic-ref cvar)) (add-new-structured-constant var con args)))) (begin ;; If this is a function that's a candidate for foldr/build ;; optimization, stash the value away prior to ;; inlining the calls. ;; *** We might try to automagically detect functions ;; *** that are candidates for these optimizations here, ;; *** but have to watch out for infinite loops! (when (and (var-force-inline? var) (eqv? (the fixnum (dynamic *current-optimize-iteration*)) (the fixnum (dynamic *optimize-build-iteration*))) (is-type? 'flic-lambda val) (or (is-foldr-or-build-app? (flic-lambda-body val)))) (setf (var-inline-value var) (copy-flic-top val))) ;; Then walk value normally. (let ((new-val (optimize val))) (setf (var-value var) new-val) (setf (var-simple? var) (or (var-force-inline? var) (and (not (var-selector-fn? var)) (can-inline? new-val (if recursive? bindings '()) toplevel?)))))) )))) (define (is-foldr-or-build-app? exp) (typecase exp (flic-app (let ((fn (flic-app-fn exp))) (and (is-type? 'flic-ref fn) (or (eq? (flic-ref-var fn) (core-symbol "foldr")) (eq? (flic-ref-var fn) (core-symbol "build")))))) (flic-let (is-foldr-or-build-app? (flic-let-body exp))) (flic-ref (let ((val (var-value (flic-ref-var exp)))) (and val (is-foldr-or-build-app? val)))) (else '#f))) (define (optimize-flic-let-rewrite object bindings recursive?) ;; Delete unused variables from the list. (setf bindings (list-delete-if (lambda (var) (cond ((var-toplevel? var) ;; This was a structured constant hoisted to top-level. '#t) ((eqv? (the fixnum (var-referenced var)) (the fixnum 0)) (record-hack 'let-remove-unused-binding var) '#t) ((eqv? (the fixnum (var-referenced var)) (the fixnum 1)) (setf (var-single-ref var) (dynamic *lambda-depth*)) '#f) (else (setf (var-single-ref var) '#f) '#f))) bindings)) ;; Add extra bindings for reducing functions with invariant ;; arguments. Hopefully some of the extra bindings will go ;; away in future passes! (setf (flic-let-bindings object) (setf bindings (add-stuff-for-invariants bindings))) ;; Look for other special cases. (cond ((null? bindings) ;; Simplifying the expression by getting rid of the LET may ;; make it possible to do additional optimizations on the ;; next pass. (record-hack 'let-empty-bindings) (flic-let-body object)) ((is-type? 'flic-return-from (flic-let-body object)) ;; Hoist return-from outside of LET. This may permit ;; further optimizations by an enclosing case-block. (record-hack 'let-hoist-return-from) (let* ((body (flic-let-body object)) (inner-body (flic-return-from-exp body))) (setf (flic-return-from-exp body) object) (setf (flic-let-body object) inner-body) body)) ((and (not recursive?) (is-type? 'flic-let (flic-let-body object)) (not (flic-let-recursive? (flic-let-body object)))) ;; This is purely to produce more compact code. (record-hack 'let-compress) (let ((body (flic-let-body object))) (setf (flic-let-bindings object) (nconc bindings (flic-let-bindings body))) (setf (flic-let-body object) (flic-let-body body)) object)) ((is-type? 'flic-lambda (flic-let-body object)) ;; Hoist lambda outside of LET. This may permit ;; merging of nested lambdas on a future pass. (record-hack 'let-hoist-lambda) (let* ((body (flic-let-body object)) (inner-body (flic-lambda-body body))) (setf (flic-lambda-body body) object) (setf (flic-let-body object) inner-body) body)) (else object)) ) ;;; Look for constant-folding and structured constants here. (define-optimize flic-app (object) (optimize-flic-app-aux object)) (define (optimize-flic-app-aux object) (let ((new-fn (optimize (flic-app-fn object))) (new-args (optimize-list (flic-app-args object)))) (typecase new-fn (flic-ref ;; The function is a variable. (let* ((var (flic-ref-var new-fn)) (val (var-value var)) (n (length new-args)) (arity (guess-function-arity var))) (cond ((and arity (< (the fixnum n) (the fixnum arity))) ;; This is a first-class call that is not fully saturated. ;; Make it saturated by wrapping a lambda around it. (setf new-fn (do-app-make-saturated object new-fn new-args arity n)) (setf new-args '())) ((var-selector-fn? var) ;; This is a saturated call to a selector. We might ;; be able to inline the call. (multiple-value-bind (fn args) (try-to-fold-selector var new-fn new-args) (setf new-fn fn) (setf new-args args))) ((and (not (var-toplevel? var)) (is-type? 'flic-lambda val)) ;; This is a saturated call to a local function. ;; Increment its reference count and note if any of ;; the arguments are invariant. (incf (var-fn-referenced var)) (note-invariant-args new-args (flic-lambda-vars val))) (else (let ((magic (magic-optimize-function var))) (when magic (multiple-value-bind (fn args) (funcall magic new-fn new-args) (setf new-fn fn) (setf new-args args))))) ))) (flic-lambda ;; Turn application of lambda into a let. (multiple-value-bind (fn args) (do-lambda-to-let-aux new-fn new-args) (setf new-fn fn) (setf new-args args))) (flic-pack (let ((con (flic-pack-con new-fn)) (temp '#f)) (when (eqv? (length new-args) (con-arity con)) (cond ((and (dynamic *do-constant-optimizations*) (every-1 (function structured-constant?) new-args)) ;; This is a structured constant that ;; can be replaced with a top-level binding. (setf (flic-app-fn object) new-fn) (setf (flic-app-args object) new-args) (record-hack 'app-hoist-structured-constant object) (setf new-fn (enter-structured-constant object '#t)) (setf new-args '())) ((and (setf temp (is-selector? con 0 (car new-args))) (is-selector-list? con 1 temp (cdr new-args))) ;; This is an expression like (cons (car x) (cdr x)). ;; Replace it with just plain x to avoid reconsing. (record-hack 'app-pack-identity new-fn) (setf new-fn (copy-flic-top temp)) (setf new-args '())) )))) (flic-let ;; Hoist let to surround entire application. ;; Simplifying the function being applied may permit further ;; optimizations on next pass. ;; (We might try to hoist lets in the argument expressions, too, ;; but I don't think that would lead to any real simplification ;; of the code.) (record-hack 'app-hoist-let) (setf (flic-app-fn object) (flic-let-body new-fn)) (setf (flic-app-args object) new-args) (setf new-args '()) (setf (flic-let-body new-fn) object) ) (flic-app ;; Try to compress nested applications. ;; This may make the call saturated and permit further optimizations ;; on the next pass. (record-hack 'app-compress) (setf new-args (nconc (flic-app-args new-fn) new-args)) (setf new-fn (flic-app-fn new-fn))) ) (if (null? new-args) new-fn (begin (setf (flic-app-fn object) new-fn) (setf (flic-app-args object) new-args) object)) )) (define (guess-function-arity var) (or (let ((value (var-value var))) (and value (is-type? 'flic-lambda value) (length (flic-lambda-vars value)))) (var-arity var))) (define (do-app-make-saturated app fn args arity nargs) (declare (type fixnum arity nargs)) (record-hack 'app-make-saturated fn args) (let ((newvars '()) (newargs '())) (dotimes (i (- arity nargs)) (declare (type fixnum i)) (let ((v (init-flic-var (create-temp-var 'arg) '#f '#f))) (push v newvars) (push (make-flic-ref v) newargs))) (setf (flic-app-fn app) fn) (setf (flic-app-args app) (nconc args newargs)) (make-flic-lambda newvars app))) ;;; If the function is a selector applied to a literal dictionary, ;;; inline it. (define (try-to-fold-selector var new-fn new-args) (let ((exp (car new-args))) (if (or (and (is-type? 'flic-ref exp) ;; *** should check that var is top-level? (is-bound-to-constructor-app? (flic-ref-var exp))) (and (is-type? 'flic-app exp) (is-constructor-app-prim? exp))) (begin (record-hack 'app-fold-selector) (setf new-fn (copy-flic-top (var-value var))) (do-lambda-to-let-aux new-fn new-args)) (values new-fn new-args)))) ;;; Various primitive functions have special optimizer functions ;;; associated with them, that do constant folding and certain ;;; other identities. The optimizer function is called with the ;;; function expression and list of argument expressions (at least ;;; as many arguments as the arity of the function) and should return ;;; the two values. ;;; *** This should really use some kind of hash table, but we'd ;;; *** have to initialize the table dynamically because core-symbols ;;; *** aren't defined when this file is loaded. (define (magic-optimize-function var) (cond ((eq? var (core-symbol "foldr")) (function optimize-foldr-aux)) ((eq? var (core-symbol "build")) (function optimize-build)) ((eq? var (core-symbol "primIntegerToInt")) (function optimize-integer-to-int)) ((eq? var (core-symbol "primIntToInteger")) (function optimize-int-to-integer)) ((eq? var (core-symbol "primRationalToFloat")) (function optimize-rational-to-float)) ((eq? var (core-symbol "primRationalToDouble")) (function optimize-rational-to-double)) ((or (eq? var (core-symbol "primNegInt")) (eq? var (core-symbol "primNegInteger")) (eq? var (core-symbol "primNegFloat")) (eq? var (core-symbol "primNegDouble"))) (function optimize-neg)) (else '#f))) ;;; Foldr identities for deforestation (define (optimize-foldr fn args) (multiple-value-bind (fn args) (optimize-foldr-aux fn args) (maybe-make-app fn args))) (define (optimize-foldr-aux fn args) (let ((k (car args)) (z (cadr args)) (l (caddr args)) (tail (cdddr args))) (cond ((and (is-type? 'flic-pack k) (eq? (flic-pack-con k) (core-symbol ":")) (is-type? 'flic-pack z) (eq? (flic-pack-con z) (core-symbol "Nil"))) ;; foldr (:) [] l ==> l ;; (We arrange for build to be inlined before foldr ;; so that this pattern can be detected.) (record-hack 'foldr-cons-nil-identity) (values l tail)) ((and (is-type? 'flic-app l) (is-type? 'flic-ref (flic-app-fn l)) (eq? (flic-ref-var (flic-app-fn l)) (core-symbol "build")) (null? (cdr (flic-app-args l)))) ;; foldr k z (build g) ==> g k z (record-hack 'foldr-build-identity) (values (car (flic-app-args l)) (cons k (cons z tail)))) ((and (is-type? 'flic-pack l) (eq? (flic-pack-con l) (core-symbol "Nil"))) ;; foldr k z [] ==> z (record-hack 'foldr-nil-identity) (values z tail)) ((short-string-constant? l) ;; If the list argument is a string constant, expand it inline. ;; Only do this if the string is fairly short, though. (optimize-foldr-aux fn (cons k (cons z (cons (expand-string-constant l) tail))))) ((and (is-type? 'flic-app l) (is-type? 'flic-pack (flic-app-fn l)) (eq? (flic-pack-con (flic-app-fn l)) (core-symbol ":")) (eqv? (length (flic-app-args l)) 2)) ;; foldr k z x:xs ==> let c = k in c x (foldr c z xs) (record-hack 'foldr-cons-identity) (let ((x (car (flic-app-args l))) (xs (cadr (flic-app-args l)))) (values (if (can-inline? k '() '#f) (do-foldr-cons-identity k z x xs) (let ((cvar (init-flic-var (create-temp-var 'c) k '#f))) (make-flic-let (list cvar) (do-foldr-cons-identity (make-flic-ref cvar) z x xs) '#f))) tail))) ((is-type? 'flic-let l) ;; foldr k z (let bindings in body) ==> ;; let bindings in foldr k z body (record-hack 'foldr-hoist-let) (setf (flic-let-body l) (optimize-foldr fn (list k z (flic-let-body l)))) (values l tail)) ((not (eqv? (the fixnum (dynamic *current-optimize-iteration*)) (the fixnum (dynamic *optimize-foldr-iteration*)))) ;; Hope for more optimizations later. (values fn args)) ((and (is-type? 'flic-pack k) (eq? (flic-pack-con k) (core-symbol ":"))) ;; Inline to special case, highly optimized append primitive. ;; Could also look for (++ (++ l1 l2) l3) => (++ l1 (++ l2 l3)) ;; here, but I don't think that happens very often. (record-hack 'foldr-prim-append-inline) (values (make-flic-ref (core-symbol "primAppend")) (cons l (cons z tail)))) (else ;; Default inline. (record-hack 'foldr-inline k z) (let ((new-fn (copy-flic-top (var-value (core-symbol "inlineFoldr"))))) (if (is-type? 'flic-lambda new-fn) (do-lambda-to-let-aux new-fn args) (values new-fn args)))) ))) ;;; Mess with compile-time expansion of short string constants. (define-integrable max-short-string-length 3) (define (short-string-constant? l) (and (is-type? 'flic-const l) (let ((string (flic-const-value l))) (and (string? string) (<= (the fixnum (string-length string)) (the fixnum max-short-string-length)))))) (define (expand-string-constant l) (let* ((string (flic-const-value l)) (length (string-length string))) (expand-string-constant-aux string 0 length))) (define (expand-string-constant-aux string i length) (declare (type fixnum i length)) (if (eqv? i length) (make-flic-pack (core-symbol "Nil")) (make-flic-app (make-flic-pack (core-symbol ":")) (list (make-flic-const (string-ref string i)) (expand-string-constant-aux string (+ 1 i) length)) '#f))) ;;; Helper function for the case of expanding foldr applied to cons call. (define (do-foldr-cons-identity c z x xs) (make-flic-app c (list x (optimize-foldr (make-flic-ref (core-symbol "foldr")) (list (copy-flic-top c) z xs))) '#f)) ;;; Short-circuit build inlining for the usual case where the ;;; argument is a lambda. (It would take several optimizer passes ;;; for this simplification to fall out, otherwise.) (define (optimize-build fn args) (let ((arg (car args))) (cond ((not (eqv? (dynamic *current-optimize-iteration*) (dynamic *optimize-build-iteration*))) (values fn args)) ((is-type? 'flic-lambda arg) (record-hack 'build-inline-lambda) (do-lambda-to-let-aux arg (cons (make-flic-pack (core-symbol ":")) (cons (make-flic-pack (core-symbol "Nil")) (cdr args))))) (else (record-hack 'build-inline-other) (let ((new-fn (copy-flic-top (var-value (core-symbol "inlineBuild"))))) (if (is-type? 'flic-lambda new-fn) (do-lambda-to-let-aux new-fn args) (values new-fn args)))) ))) ;;; Various simplifications on numeric functions. ;;; *** Obviously, could get much fancier about this. (define (optimize-integer-to-int fn args) (let ((arg (car args))) (cond ((is-type? 'flic-const arg) (record-hack 'integer-to-int-constant-fold) (if (is-type? 'integer (flic-const-value arg)) (let ((value (flic-const-value arg))) (when (not (is-type? 'fixnum value)) ;; Overflow is a user error, not an implementation error. (phase-error 'int-overflow "Int overflow in primIntegerToInt: ~s" value)) (values arg (cdr args))) (error "Bad argument ~s to primIntegerToInt." arg))) ((and (is-type? 'flic-app arg) (is-type? 'flic-ref (flic-app-fn arg)) (eq? (flic-ref-var (flic-app-fn arg)) (core-symbol "primIntToInteger")) (null? (cdr (flic-app-args arg)))) (record-hack 'integer-to-int-identity) (values (car (flic-app-args arg)) (cdr args))) (else (values fn args))))) (define (optimize-int-to-integer fn args) (let ((arg (car args))) (cond ((is-type? 'flic-const arg) (record-hack 'int-to-integer-constant-fold) (if (is-type? 'integer (flic-const-value arg)) (values arg (cdr args)) (error "Bad argument ~s to primIntToInteger." arg))) ((and (is-type? 'flic-app arg) (is-type? 'flic-ref (flic-app-fn arg)) (eq? (flic-ref-var (flic-app-fn arg)) (core-symbol "primIntegerToInt")) (null? (cdr (flic-app-args arg)))) (record-hack 'int-to-integer-identity) (values (car (flic-app-args arg)) (cdr args))) (else (values fn args))))) (predefine (prim.rational-to-float-aux n d)) ; in prims.scm (predefine (prim.rational-to-double-aux n d)) ; in prims.scm (define (optimize-rational-to-float fn args) (let ((arg (car args))) (cond ((is-type? 'flic-const arg) (record-hack 'rational-to-float-constant-fold) (if (is-type? 'list (flic-const-value arg)) (let ((value (flic-const-value arg))) (setf (flic-const-value arg) (prim.rational-to-float-aux (car value) (cadr value))) (values arg (cdr args))) (error "Bad argument ~s to primRationalToFloat." arg))) (else (values fn args))))) (define (optimize-rational-to-double fn args) (let ((arg (car args))) (cond ((is-type? 'flic-const arg) (record-hack 'rational-to-double-constant-fold) (if (is-type? 'list (flic-const-value arg)) (let ((value (flic-const-value arg))) (setf (flic-const-value arg) (prim.rational-to-double-aux (car value) (cadr value))) (values arg (cdr args))) (error "Bad argument ~s to primRationalToDouble." arg))) (else (values fn args))))) (define (optimize-neg fn args) (let ((arg (car args))) (cond ((is-type? 'flic-const arg) (record-hack 'neg-constant-fold) (if (is-type? 'number (flic-const-value arg)) (begin (setf (flic-const-value arg) (- (flic-const-value arg))) (values arg (cdr args))) (error "Bad argument ~s to ~s." arg fn))) (else (values fn args))))) ;;; Convert lambda applications to lets. ;;; If application is not saturated, break it up into two nested ;;; lambdas before doing the transformation. ;;; It's better to do this optimization immediately than hoping ;;; the call will become fully saturated on the next pass. ;;; Maybe we could also look for a flic-let with a flic-lambda as ;;; the body to catch the cases where additional arguments can ;;; be found on a later pass. (define (do-lambda-to-let new-fn new-args) (multiple-value-bind (fn args) (do-lambda-to-let-aux new-fn new-args) (maybe-make-app fn args))) (define (maybe-make-app fn args) (if (null? args) fn (make-flic-app fn args '#f))) (define (do-lambda-to-let-aux new-fn new-args) (let ((vars (flic-lambda-vars new-fn)) (body (flic-lambda-body new-fn)) (matched '())) (record-hack 'app-lambda-to-let) (do () ((or (null? new-args) (null? vars))) (let ((var (pop vars)) (arg (pop new-args))) (setf (var-value var) arg) (setf (var-simple? var) (can-inline? arg '() '#t)) (if (eqv? (var-referenced var) 1) (setf (var-single-ref var) (dynamic *lambda-depth*))) (push var matched))) (setf matched (nreverse matched)) (if (not (null? vars)) (setf body (make-flic-lambda vars body))) (setf new-fn (make-flic-let matched body '#f)) (values new-fn new-args))) ;;; For references, check to see if we can beta-reduce. ;;; Don't increment reference count for inlineable vars, but do ;;; traverse the new value expression. (define-optimize flic-ref (object) (optimize-flic-ref-aux object)) (define (optimize-flic-ref-aux object) (let ((var (flic-ref-var object))) (cond ((var-single-ref var) ;; (or (eqv? (var-single-ref var) (dynamic *lambda-depth*))) ;; *** The lambda-depth test is too conservative to handle ;; *** inlining of stuff necessary for foldr/build optimizations. ;; Can substitute value no matter how hairy it is. ;; Note that this is potentially risky; if the single ;; reference detected on the previous pass appeared as ;; the value of a variable binding that is being inlined ;; on the current pass, it might turn into multiple ;; references again! ;; We copy the value anyway to avoid problems with shared ;; structure in the multiple reference case. (record-hack 'ref-inline-single-ref var) (optimize (copy-flic-top (var-value var)))) ((and (var-inline-value var) (dynamic *do-inline-optimizations*)) ;; Use the previously saved value in preference to the current ;; value of the variable. (record-hack 'ref-inline-foldr-hack) (optimize (copy-flic-top (var-inline-value var)))) ((and (var-simple? var) (or (dynamic *do-inline-optimizations*) (not (var-toplevel? var)))) ;; Can substitute, but must copy. (record-hack 'ref-inline var) (optimize (copy-flic-top (var-value var)))) ((eq? var (core-symbol "foldr")) ;; Magic stuff for deforestation (if (> (the fixnum (dynamic *current-optimize-iteration*)) (the fixnum (dynamic *optimize-foldr-iteration*))) (begin (record-hack 'ref-inline-foldr) (optimize (make-flic-ref (core-symbol "inlineFoldr")))) object)) ((eq? var (core-symbol "build")) ;; Magic stuff for deforestation (if (> (the fixnum (dynamic *current-optimize-iteration*)) (the fixnum (dynamic *optimize-build-iteration*))) (begin (record-hack 'ref-inline-build) (optimize (make-flic-ref (core-symbol "inlineBuild")))) object)) ((var-toplevel? var) object) (else (incf (var-referenced var)) object)))) ;;; Don't do anything exciting with constants. (define-optimize flic-const (object) object) (define-optimize flic-pack (object) object) ;;; Various simplifications on and (define-optimize flic-and (object) (maybe-simplify-and object (optimize-and-exps (flic-and-exps object) '()))) (define (maybe-simplify-and object exps) (cond ((null? exps) (record-hack 'and-empty) (make-flic-pack (core-symbol "True"))) ((null? (cdr exps)) (record-hack 'and-unary) (car exps)) (else (setf (flic-and-exps object) exps) object))) (define (optimize-and-exps exps result) (if (null? exps) (nreverse result) (let ((exp (optimize (car exps)))) (typecase exp (flic-pack (cond ((eq? (flic-pack-con exp) (core-symbol "True")) ;; True appears in subexpressions. ;; Discard this test only. (record-hack 'and-contains-true) (optimize-and-exps (cdr exps) result)) ((eq? (flic-pack-con exp) (core-symbol "False")) ;; False appears in subexpressions. ;; Discard remaining tests as dead code. ;; Can't replace the whole and expression with false because ;; of possible strictness side-effects. (record-hack 'and-contains-false) (nreverse (cons exp result))) (else ;; Should never happen. (error "Non-boolean con ~s in and expression!" exp)))) (flic-and ;; Flatten nested ands. (record-hack 'and-compress) (optimize-and-exps (cdr exps) (nconc (nreverse (flic-and-exps exp)) result))) (else ;; No optimization possible. (optimize-and-exps (cdr exps) (cons exp result))) )))) ;;; Case-block optimizations. These optimizations are possible because ;;; of the restricted way this construct is used; return-froms are ;;; never nested, etc. (define-optimize flic-case-block (object) (let* ((sym (flic-case-block-block-name object)) (exps (optimize-case-block-exps sym (flic-case-block-exps object) '()))) (optimize-flic-case-block-aux object sym exps))) (define (optimize-flic-case-block-aux object sym exps) (cond ((null? exps) ;; This should never happen. It means all of the tests were ;; optimized away, including the failure case! (error "No exps left in case block ~s!" object)) ((and (is-type? 'flic-and (car exps)) (is-return-from-block? sym (car (last (flic-and-exps (car exps)))))) ;; The first clause is a simple and. Hoist it out of the ;; case-block and rewrite as if/then/else. (record-hack 'case-block-to-if) (let ((then-exp (car (last (flic-and-exps (car exps)))))) (setf (flic-case-block-exps object) (cdr exps)) (make-flic-if (maybe-simplify-and (car exps) (butlast (flic-and-exps (car exps)))) (flic-return-from-exp then-exp) (optimize-flic-case-block-aux object sym (cdr exps))))) ((is-return-from-block? sym (car exps)) ;; Do an identity reduction. (record-hack 'case-block-identity) (flic-return-from-exp (car exps))) ((is-type? 'flic-let (car exps)) ;; The first clause is a let. Since this clause is going ;; to be executed anyway, hoisting the bindings to surround ;; the entire case-block should not change their strictness ;; properties, and it may permit some further optimizations. (record-hack 'case-block-hoist-let) (let* ((exp (car exps)) (body (flic-let-body exp))) (setf (flic-let-body exp) (optimize-flic-case-block-aux object sym (cons body (cdr exps)))) exp)) (else (setf (flic-case-block-exps object) exps) object) )) (define (optimize-case-block-exps sym exps result) (if (null? exps) (nreverse result) (let ((exp (optimize (car exps)))) (cond ((is-return-from-block? sym exp) ;; Any remaining clauses are dead code and should be removed. (if (not (null? (cdr exps))) (record-hack 'case-block-dead-code)) (nreverse (cons exp result))) ((is-type? 'flic-and exp) ;; See if we can remove redundant tests. (push (maybe-simplify-and exp (look-for-redundant-tests (flic-and-exps exp) result)) result) (optimize-case-block-exps sym (cdr exps) result)) (else ;; No optimization possible. (optimize-case-block-exps sym (cdr exps) (cons exp result))) )))) ;;; Look for case-block tests that are known to be either true or false ;;; because of tests made in previous clauses. ;;; For now, we only look at is-constructor tests. Such a test is known ;;; to be true if previous clauses have eliminated all other possible ;;; constructors. And such a test is known to be false if a previous ;;; clause has already matched this constructor. (define (look-for-redundant-tests exps previous-clauses) (if (null? exps) '() (let ((exp (car exps))) (cond ((and (is-type? 'flic-is-constructor exp) (constructor-test-redundant? exp previous-clauses)) ;; Known to be true. (record-hack 'case-block-discard-redundant-test) (cons (make-flic-pack (core-symbol "True")) (look-for-redundant-tests (cdr exps) previous-clauses))) ((and (is-type? 'flic-is-constructor exp) (constructor-test-duplicated? exp previous-clauses)) ;; Known to be false. (record-hack 'case-block-discard-duplicate-test) (list (make-flic-pack (core-symbol "False")))) (else ;; No optimization. (cons exp (look-for-redundant-tests (cdr exps) previous-clauses))) )))) ;;; In looking for redundant/duplicated tests, only worry about ;;; is-constructor tests that have an argument that is a variable. ;;; It's too hairy to consider any other cases. (define (constructor-test-duplicated? exp previous-clauses) (let ((con (flic-is-constructor-con exp)) (arg (flic-is-constructor-exp exp))) (and (is-type? 'flic-ref arg) (constructor-test-present? con arg previous-clauses)))) (define (constructor-test-redundant? exp previous-clauses) (let ((con (flic-is-constructor-con exp)) (arg (flic-is-constructor-exp exp))) (and (is-type? 'flic-ref arg) (every-1 (lambda (c) (or (eq? c con) (constructor-test-present? c arg previous-clauses))) (algdata-constrs (con-alg con)))))) (define (constructor-test-present? con arg previous-clauses) (cond ((null? previous-clauses) '#f) ((constructor-test-present-1? con arg (car previous-clauses)) '#t) (else (constructor-test-present? con arg (cdr previous-clauses))))) ;;; The tricky thing here is that, even if the constructor test is ;;; present in the clause, we have to make sure that the entire clause won't ;;; fail due to the presence of some other test which fails. So look ;;; for a very specific pattern here, namely ;;; (and (is-constructor con arg) (return-from ....)) (define (constructor-test-present-1? con arg clause) (and (is-type? 'flic-and clause) (let ((exps (flic-and-exps clause))) (and (is-type? 'flic-is-constructor (car exps)) (is-type? 'flic-return-from (cadr exps)) (null? (cddr exps)) (let* ((inner-exp (car exps)) (inner-con (flic-is-constructor-con inner-exp)) (inner-arg (flic-is-constructor-exp inner-exp))) (and (eq? inner-con con) (flic-exp-eq? arg inner-arg))))))) ;;; No fancy optimizations for return-from by itself. (define-optimize flic-return-from (object) (setf (flic-return-from-exp object) (optimize (flic-return-from-exp object))) object) ;;; Obvious simplification on if (define-optimize flic-if (object) (let ((test-exp (optimize (flic-if-test-exp object))) (then-exp (optimize (flic-if-then-exp object))) (else-exp (optimize (flic-if-else-exp object)))) (cond ((and (is-type? 'flic-pack test-exp) (eq? (flic-pack-con test-exp) (core-symbol "True"))) ;; Fold constant test (record-hack 'if-fold) then-exp) ((and (is-type? 'flic-pack test-exp) (eq? (flic-pack-con test-exp) (core-symbol "False"))) ;; Fold constant test (record-hack 'if-fold) else-exp) ((and (is-type? 'flic-is-constructor test-exp) (eq? (flic-is-constructor-con test-exp) (core-symbol "True"))) ;; Remove redundant is-constructor test. ;; Doing this as a general is-constructor identity ;; backfires because it prevents some of the important case-block ;; optimizations from being recognized, but it works fine here. (record-hack 'if-compress-test) (setf (flic-if-test-exp object) (flic-is-constructor-exp test-exp)) (setf (flic-if-then-exp object) then-exp) (setf (flic-if-else-exp object) else-exp) object) ((and (is-type? 'flic-is-constructor test-exp) (eq? (flic-is-constructor-con test-exp) (core-symbol "False"))) ;; Remove redundant is-constructor test, flip branches. (record-hack 'if-compress-test) (setf (flic-if-test-exp object) (flic-is-constructor-exp test-exp)) (setf (flic-if-then-exp object) else-exp) (setf (flic-if-else-exp object) then-exp) object) ((and (is-type? 'flic-return-from then-exp) (is-type? 'flic-return-from else-exp) (eq? (flic-return-from-block-name then-exp) (flic-return-from-block-name else-exp))) ;; Hoist return-from outside of IF. ;; This may permit further case-block optimizations. (record-hack 'if-hoist-return-from) (let ((return-exp then-exp)) (setf (flic-if-test-exp object) test-exp) (setf (flic-if-then-exp object) (flic-return-from-exp then-exp)) (setf (flic-if-else-exp object) (flic-return-from-exp else-exp)) (setf (flic-return-from-exp return-exp) object) return-exp)) ((and (is-type? 'flic-pack then-exp) (is-type? 'flic-pack else-exp) (eq? (flic-pack-con then-exp) (core-symbol "True")) (eq? (flic-pack-con else-exp) (core-symbol "False"))) ;; This if does nothing useful at all! (record-hack 'if-identity) test-exp) ((and (is-type? 'flic-pack then-exp) (is-type? 'flic-pack else-exp) (eq? (flic-pack-con then-exp) (core-symbol "False")) (eq? (flic-pack-con else-exp) (core-symbol "True"))) ;; Inverse of previous case (record-hack 'if-identity-inverse) (make-flic-is-constructor (core-symbol "False") test-exp)) ((or (is-type? 'flic-lambda then-exp) (is-type? 'flic-lambda else-exp)) ;; Hoist lambdas to surround entire if. This allows us to ;; do a better job of saturating them. (record-hack 'if-hoist-lambda) (multiple-value-bind (vars then-exp else-exp) (do-if-hoist-lambda then-exp else-exp) (setf (flic-if-test-exp object) test-exp) (setf (flic-if-then-exp object) then-exp) (setf (flic-if-else-exp object) else-exp) (make-flic-lambda vars object))) (else ;; No optimization possible (setf (flic-if-test-exp object) test-exp) (setf (flic-if-then-exp object) then-exp) (setf (flic-if-else-exp object) else-exp) object) ))) ;;; Try to pull as many variables as possible out to surround the entire ;;; let. (define (do-if-hoist-lambda then-exp else-exp) (let ((vars '()) (then-args '()) (else-args '())) (do ((then-vars (if (is-type? 'flic-lambda then-exp) (flic-lambda-vars then-exp) '()) (cdr then-vars)) (else-vars (if (is-type? 'flic-lambda else-exp) (flic-lambda-vars else-exp) '()) (cdr else-vars))) ((and (null? then-vars) (null? else-vars)) '#f) (let ((var (init-flic-var (create-temp-var 'arg) '#f '#f))) (push var vars) (push (make-flic-ref var) then-args) (push (make-flic-ref var) else-args))) (values vars (if (is-type? 'flic-lambda then-exp) (do-lambda-to-let then-exp then-args) (make-flic-app then-exp then-args '#f)) (if (is-type? 'flic-lambda else-exp) (do-lambda-to-let else-exp else-args) (make-flic-app else-exp else-args '#f))))) ;;; Look for (sel (pack x)) => x (define-optimize flic-sel (object) (optimize-flic-sel-aux object)) (define (optimize-flic-sel-aux object) (let ((new-exp (optimize (flic-sel-exp object)))) (setf (flic-sel-exp object) new-exp) (typecase new-exp (flic-ref ;; Check to see whether this is bound to a pack application (let ((val (is-bound-to-constructor-app? (flic-ref-var new-exp)))) (if val ;; Yup, it is. Now extract the appropriate component, ;; provided it is inlineable. (let* ((i (flic-sel-i object)) (args (flic-app-args val)) (newval (list-ref args i))) (if (can-inline? newval '() '#t) (begin (record-hack 'sel-fold-var) (optimize (copy-flic-top newval))) object)) ;; The variable was bound to something else. object))) (flic-app ;; The obvious optimization. (if (is-constructor-app-prim? new-exp) (begin (record-hack 'sel-fold-app) (list-ref (flic-app-args new-exp) (flic-sel-i object))) object)) (else object)))) ;;; Do similar stuff for is-constructor. (define-optimize flic-is-constructor (object) (let ((con (flic-is-constructor-con object)) (exp (optimize (flic-is-constructor-exp object))) (exp-con '#f)) (cond ((algdata-tuple? (con-alg con)) ;; Tuples have only one constructor, so this is always true (record-hack 'is-constructor-fold-tuple) (make-flic-pack (core-symbol "True"))) ((setf exp-con (is-constructor-app? exp)) ;; The expression is a constructor application. (record-hack 'is-constructor-fold) (make-flic-pack (if (eq? exp-con con) (core-symbol "True") (core-symbol "False")))) (else ;; No optimization possible (setf (flic-is-constructor-exp object) exp) object) ))) (define-optimize flic-con-number (object) (let ((exp (flic-con-number-exp object)) (type (flic-con-number-type object))) ;; ***Maybe ast-to-flic should look for this one. (if (algdata-tuple? type) (begin (record-hack 'con-number-fold-tuple) (make-flic-const 0)) (let* ((new-exp (optimize exp)) (con (is-constructor-app? new-exp))) (if con (begin (record-hack 'con-number-fold) (make-flic-const (con-tag con))) (begin (setf (flic-con-number-exp object) new-exp) object))) ))) (define-optimize flic-void (object) object) ;;;=================================================================== ;;; General helper functions ;;;=================================================================== ;;; Lucid's built-in every function seems to do a lot of unnecessary ;;; consing. This one is much faster. (define (every-1 fn list) (cond ((null? list) '#t) ((funcall fn (car list)) (every-1 fn (cdr list))) (else '#f))) ;;; Equality predicate on flic expressions (define (flic-exp-eq? a1 a2) (typecase a1 (flic-const (and (is-type? 'flic-const a2) (equal? (flic-const-value a1) (flic-const-value a2)))) (flic-ref (and (is-type? 'flic-ref a2) (eq? (flic-ref-var a1) (flic-ref-var a2)))) (flic-pack (and (is-type? 'flic-pack a2) (eq? (flic-pack-con a1) (flic-pack-con a2)))) (flic-sel (and (is-type? 'flic-sel a2) (eq? (flic-sel-con a1) (flic-sel-con a2)) (eqv? (flic-sel-i a1) (flic-sel-i a2)) (flic-exp-eq? (flic-sel-exp a1) (flic-sel-exp a2)))) (else '#f))) ;;; Predicates for testing whether an expression matches a pattern. (define (is-constructor-app? exp) (typecase exp (flic-app ;; See if we have a saturated call to a constructor. (is-constructor-app-prim? exp)) (flic-ref ;; See if we can determine anything about the value the variable ;; is bound to. (let ((value (var-value (flic-ref-var exp)))) (if value (is-constructor-app? value) '#f))) (flic-let ;; See if we can determine anything about the body of the let. (is-constructor-app? (flic-let-body exp))) (flic-pack ;; See if this is a nullary constructor. (let ((con (flic-pack-con exp))) (if (eqv? (con-arity con) 0) con '#f))) (else '#f))) (define (is-return-from-block? sym exp) (and (is-type? 'flic-return-from exp) (eq? (flic-return-from-block-name exp) sym))) (define (is-constructor-app-prim? exp) (let ((fn (flic-app-fn exp)) (args (flic-app-args exp))) (if (and (is-type? 'flic-pack fn) (eqv? (length args) (con-arity (flic-pack-con fn)))) (flic-pack-con fn) '#f))) (define (is-bound-to-constructor-app? var) (let ((val (var-value var))) (if (and val (is-type? 'flic-app val) (is-constructor-app-prim? val)) val '#f))) (define (is-selector? con i exp) (or (and (is-type? 'flic-ref exp) (is-selector? con i (var-value (flic-ref-var exp)))) (and (is-type? 'flic-sel exp) (eq? (flic-sel-con exp) con) (eqv? (the fixnum i) (the fixnum (flic-sel-i exp))) (flic-sel-exp exp)) )) (define (is-selector-list? con i subexp exps) (declare (type fixnum i)) (if (null? exps) subexp (let ((temp (is-selector? con i (car exps)))) (and (flic-exp-eq? subexp temp) (is-selector-list? con (+ 1 i) subexp (cdr exps)))))) ;;;=================================================================== ;;; Inlining criteria ;;;=================================================================== ;;; Expressions that can be inlined unconditionally are constants, variable ;;; references, and some functions. ;;; I've made some attempt here to arrange the cases in the order they ;;; are likely to occur. (define (can-inline? exp recursive-vars toplevel?) (typecase exp (flic-sel ;; Listed first because it happens more frequently than ;; anything else. ;; *** Inlining these is an experiment. ;; *** This transformation interacts with the strictness ;; *** analyzer; if the variable referenced is not strict, then ;; *** it is probably not a good thing to do since it adds extra ;; *** forces. ;; (let ((subexp (flic-sel-exp exp))) ;; (and (is-type? 'flic-ref subexp) ;; (not (memq (flic-ref-var subexp) recursive-vars)))) '#f) (flic-lambda ;; Do not try to inline lambdas if the fancy inline optimization ;; is disabled. ;; Watch for problems with infinite loops with recursive variables. (if (dynamic *do-inline-optimizations*) (simple-function-body? (flic-lambda-body exp) (flic-lambda-vars exp) recursive-vars toplevel?) '#f)) (flic-ref ;; We get into infinite loops trying to inline recursive variables. (not (memq (flic-ref-var exp) recursive-vars))) ((or flic-pack flic-const) '#t) (else '#f))) ;;; Determining whether to inline a function is difficult. This is ;;; very conservative to avoid code bloat. What we need to do is ;;; compare the cost (in program size mainly) of the inline call with ;;; an out of line call. For an out of line call, we pay for one function ;;; call and a setup for each arg. When inlining, we pay for function ;;; calls in the body and for args referenced more than once. In terms of ;;; execution time, we win big when a functional parameter is called ;;; since this `firstifies' the program. ;;; Here's the criteria: ;;; An inline function gets to reference no more that 2 non-parameter ;;; values (including constants and repeated parameter references). ;;; For non-toplevel functions, be slightly more generous since the ;;; fixed overhead of binding the local function would go away. (define (simple-function-body? exp lambda-vars recursive-vars toplevel?) (let ((c (if toplevel? 2 4))) (>= (the fixnum (simple-function-body-1 exp lambda-vars recursive-vars c)) 0))) ;;; I've made some attempt here to order the cases by how frequently ;;; they appear. (define (simple-function-body-1 exp lambda-vars recursive-vars c) (declare (type fixnum c)) (if (< c 0) (values c '()) (typecase exp (flic-ref (let ((var (flic-ref-var exp))) (cond ((memq var lambda-vars) (values c (list-remove-1 var lambda-vars))) ((memq var recursive-vars) (values -1 '())) (else (values (the fixnum (1- c)) lambda-vars))))) (flic-app (simple-function-body-1/l (cons (flic-app-fn exp) (flic-app-args exp)) lambda-vars recursive-vars c)) (flic-sel (simple-function-body-1 (flic-sel-exp exp) lambda-vars recursive-vars (the fixnum (1- c)))) (flic-is-constructor (simple-function-body-1 (flic-is-constructor-exp exp) lambda-vars recursive-vars (the fixnum (1- c)))) ((or flic-const flic-pack) (values (the fixnum (1- c)) lambda-vars)) (else ;; case & let & lambda not allowed. (values -1 '()))))) (define (list-remove-1 item list) (cond ((null? list) '()) ((eq? item (car list)) (cdr list)) (else (cons (car list) (list-remove-1 item (cdr list)))) )) (define (simple-function-body-1/l exps lambda-vars recursive-vars c) (declare (type fixnum c)) (if (or (null? exps) (< c 0)) (values c lambda-vars) (multiple-value-bind (c-1 lambda-vars-1) (simple-function-body-1 (car exps) lambda-vars recursive-vars c) (simple-function-body-1/l (cdr exps) lambda-vars-1 recursive-vars c-1)))) ;;;=================================================================== ;;; Constant structured data detection ;;;=================================================================== ;;; Look to determine whether an object is a structured constant, ;;; recursively examining its components if it's an app. This is ;;; necessary in order to detect constants with arbitrary circular ;;; reference to the vars in recursive-vars. (define (structured-constant-recursive? object recursive-vars stack) (typecase object (flic-const '#t) (flic-ref (let ((var (flic-ref-var object))) (or (memq var stack) (var-toplevel? var) (and (memq var recursive-vars) (structured-constant-recursive? (var-value var) recursive-vars (cons var stack)))))) (flic-pack '#t) (flic-app (structured-constant-app-recursive? (flic-app-fn object) (flic-app-args object) recursive-vars stack)) (flic-lambda (lambda-hoistable? object)) (else '#f))) (define (structured-constant-app-recursive? fn args recursive-vars stack) (and (is-type? 'flic-pack fn) (eqv? (length args) (con-arity (flic-pack-con fn))) (every-1 (lambda (a) (structured-constant-recursive? a recursive-vars stack)) args))) ;;; Here's a non-recursive (and more efficient) version of the above. ;;; Instead of looking at the whole structure, it only looks one level ;;; deep. This can't detect circular constants, but is useful in ;;; contexts where circularities cannot appear. (define (structured-constant? object) (typecase object (flic-ref (var-toplevel? (flic-ref-var object))) (flic-const '#t) (flic-pack '#t) (flic-lambda (lambda-hoistable? object)) (else '#f))) (define (structured-constant-app? fn args) (and (is-type? 'flic-pack fn) (eqv? (length args) (con-arity (flic-pack-con fn))) (every-1 (function structured-constant?) args))) ;;; Determine whether a lambda can be hoisted to top-level. ;;; The main purpose of this code is to mark structured constants ;;; containing simple lambdas to permit later folding of sel expressions ;;; on those constants. Since the latter expression is permissible ;;; only on inlinable functions, stop if we hit an expression that ;;; would make the function not inlinable. (define (lambda-hoistable? object) (and (can-inline? object '() '#t) (lambda-hoistable-aux (flic-lambda-body object) (flic-lambda-vars object)))) (define (lambda-hoistable-aux object local-vars) (typecase object (flic-ref (or (var-toplevel? (flic-ref-var object)) (memq (flic-ref-var object) local-vars))) ((or flic-const flic-pack) '#t) (flic-sel (lambda-hoistable-aux (flic-sel-exp object) local-vars)) (flic-is-constructor (lambda-hoistable-aux (flic-is-constructor-exp object) local-vars)) (flic-app (and (lambda-hoistable-aux (flic-app-fn object) local-vars) (every-1 (lambda (x) (lambda-hoistable-aux x local-vars)) (flic-app-args object)))) (else '#f))) ;;; Having determined that something is a structured constant, ;;; enter it (and possibly its subcomponents) in the hash table ;;; and return a var-ref. (define (enter-structured-constant value recursive?) (multiple-value-bind (con args var) (enter-structured-constant-aux value recursive?) (when (not var) (setf var (create-temp-var 'constant)) (add-new-structured-constant var con args)) (make-flic-ref var))) (define (enter-structured-constant-aux value recursive?) (let* ((fn (flic-app-fn value)) (con (flic-pack-con fn)) (args (if recursive? (map (function enter-structured-constant-arg) (flic-app-args value)) (flic-app-args value)))) (values con args (lookup-structured-constant con args)))) (define (enter-structured-constant-arg a) (if (is-type? 'flic-app a) (enter-structured-constant a '#t) a)) (define (lookup-structured-constant con args) (lookup-structured-constant-aux (table-entry *structured-constants-table* con) args)) (define (lookup-structured-constant-aux alist args) (cond ((null? alist) '#f) ((every (function flic-exp-eq?) (car (car alist)) args) (cdr (car alist))) (else (lookup-structured-constant-aux (cdr alist) args)))) (define (add-new-structured-constant var con args) (push (cons args var) (table-entry *structured-constants-table* con)) (setf (var-toplevel? var) '#t) (setf (var-value var) (make-flic-app (make-flic-pack con) args '#t)) (push var *structured-constants*) var) ;;;=================================================================== ;;; Invariant argument stuff ;;;=================================================================== ;;; When processing a saturated call to a locally defined function, ;;; note whether any of the arguments are always passed the same value. (define (note-invariant-args args vars) (when (and (not (null? args)) (not (null? vars))) (let* ((arg (car args)) (var (car vars)) (val (var-arg-invariant-value var))) (cond ((not (var-arg-invariant? var)) ;; This argument already marked as having more than one ;; value. ) ((and (is-type? 'flic-ref arg) (eq? (flic-ref-var arg) var)) ;; This is a recursive call with the same argument. ;; Don't update the arg-invariant-value slot. ) ((or (not val) (flic-exp-eq? arg val)) ;; Either this is the first call, or a second call with ;; the same argument. (setf (var-arg-invariant-value var) arg)) (else ;; Different values for this argument are passed in ;; different places, so we can't mess with it. (setf (var-arg-invariant? var) '#f))) (note-invariant-args (cdr args) (cdr vars))))) ;;; After processing a let form, check to see if any of the bindings ;;; are for local functions with invariant arguments. ;;; Suppose we have something like ;;; let foo = \ x y z -> ;;; in ;;; and y is known to be invariant; then we rewrite this as ;;; let foo1 = \ x z -> let y = in ;;; foo = \ x1 y1 z1 -> foo1 x1 z1 ;;; in ;;; The original foo binding is inlined on subsequent passes and ;;; should go away. Likewise, the binding of y should be inlined also. ;;; *** This is kind of bogus because of the way it depends on the ;;; *** magic force-inline bit. It would be better to do a code walk ;;; *** now on the entire let expression to rewrite all the calls to foo. (define (add-stuff-for-invariants bindings) (if (null? bindings) '() (let* ((var (car bindings)) (val (var-value var))) (setf (cdr bindings) (add-stuff-for-invariants (cdr bindings))) (if (and (is-type? 'flic-lambda val) ;; Don't mess with single-reference variable bindings, ;; or things we are going to inline anyway. (not (var-single-ref var)) (not (var-simple? var)) ;; All references must be in saturated calls to do this. (eqv? (var-referenced var) (var-fn-referenced var)) ;; There is at least one argument marked invariant. (some (function var-arg-invariant?) (flic-lambda-vars val)) ;; Every argument marked invariant must also be hoistable. (every-1 (function arg-hoistable?) (flic-lambda-vars val))) (hoist-invariant-args var val bindings) bindings)))) (define (arg-hoistable? var) (if (var-arg-invariant? var) (or (not (var-arg-invariant-value var)) (flic-invariant? (var-arg-invariant-value var) (dynamic *local-bindings*))) '#t)) (define (hoist-invariant-args var val bindings) (let ((foo1-var (copy-temp-var (def-name var))) (foo1-def-vars '()) (foo1-app-args '()) (foo1-let-vars '()) (foo-def-vars '())) (push foo1-var bindings) (dolist (v (flic-lambda-vars val)) (let ((new-v (copy-temp-var (def-name v)))) (push (init-flic-var new-v '#f '#f) foo-def-vars) (if (var-arg-invariant? v) (when (var-arg-invariant-value v) (push (init-flic-var v (copy-flic-top (var-arg-invariant-value v)) '#f) foo1-let-vars)) (begin (push v foo1-def-vars) (push (make-flic-ref new-v) foo1-app-args)) ))) (setf foo1-def-vars (nreverse foo1-def-vars)) (setf foo1-app-args (nreverse foo1-app-args)) (setf foo1-let-vars (nreverse foo1-let-vars)) (setf foo-def-vars (nreverse foo-def-vars)) (record-hack 'let-hoist-invariant-args var foo1-let-vars) ;; Fix up the value of foo1 (init-flic-var foo1-var (let ((body (make-flic-let foo1-let-vars (flic-lambda-body val) '#f))) (if (null? foo1-def-vars) ;; *All* of the arguments were invariant. body ;; Otherwise, make a new lambda (make-flic-lambda foo1-def-vars body))) '#f) ;; Fix up the value of foo and arrange for it to be inlined. (setf (flic-lambda-vars val) foo-def-vars) (setf (flic-lambda-body val) (if (null? foo1-app-args) (make-flic-ref foo1-var) (make-flic-app (make-flic-ref foo1-var) foo1-app-args '#t))) (setf (var-simple? var) '#t) (setf (var-force-inline? var) '#t) ;; Return modified list of bindings bindings)) ;;;=================================================================== ;;; Install globals ;;;=================================================================== ;;; The optimizer, CFN, etc. can introduce new top-level variables that ;;; are not installed in the symbol table. This causes problems if ;;; those variables are referenced in the .hci file (as in the inline ;;; expansion of some other variables). So we need to fix up the ;;; symbol table before continuing. (define (install-uninterned-globals vars) (dolist (v vars) (let* ((module (locate-module (def-module v))) (name (def-name v)) (table (module-symbol-table module)) (def (table-entry table name))) (cond ((not def) ;; This def was not installed. Rename it if it's a gensym ;; and install it. (when (gensym? name) (setf name (rename-gensym-var v name table))) (setf (table-entry table name) v)) ((eq? def v) ;; Already installed. '#t) (else ;; Ooops! The symbol installed in the symbol table isn't ;; this one! (error "Duplicate defs ~s and ~s in symbol table for ~s!" v def module)) )))) (define (rename-gensym-var var name table) (setf name (string->symbol (symbol->string name))) (if (table-entry table name) ;; This name already in use; gensym a new one! (rename-gensym-var var (gensym (symbol->string name)) table) ;; OK, no problem (setf (def-name var) name))) ;;;=================================================================== ;;; Postoptimizer ;;;=================================================================== ;;; This is another quick traversal of the structure to determine ;;; whether references to functions are fully saturated or not. ;;; Also makes sure that reference counts on variables are correct; ;;; this is needed so the code generator can generate ignore declarations ;;; for unused lambda variables. (define-flic-walker postoptimize (object)) (define-postoptimize flic-lambda (object) (dolist (var (flic-lambda-vars object)) (setf (var-referenced var) 0)) (postoptimize (flic-lambda-body object))) (define-postoptimize flic-let (object) (dolist (var (flic-let-bindings object)) (setf (var-referenced var) 0) (let ((val (var-value var))) (setf (var-arity var) (if (is-type? 'flic-lambda val) (length (flic-lambda-vars val)) 0)))) (dolist (var (flic-let-bindings object)) (postoptimize (var-value var))) (postoptimize (flic-let-body object))) (define-postoptimize flic-app (object) (let ((fn (flic-app-fn object))) (typecase fn (flic-ref (let* ((var (flic-ref-var fn)) (arity (var-arity var))) (if (not (var-toplevel? var)) (incf (var-referenced var))) (when (not (eqv? arity 0)) (postoptimize-app-aux object var arity (flic-app-args object))))) (flic-pack (let* ((con (flic-pack-con fn)) (arity (con-arity con))) (postoptimize-app-aux object '#f arity (flic-app-args object)))) (else (postoptimize fn))) (dolist (a (flic-app-args object)) (postoptimize a)))) (define (postoptimize-app-aux object var arity args) (declare (type fixnum arity)) (let ((nargs (length args))) (declare (type fixnum nargs)) (cond ((< nargs arity) ;; not enough arguments (when var (setf (var-standard-refs? var) '#t))) ((eqv? nargs arity) ;; exactly the right number of arguments (when var (setf (var-optimized-refs? var) '#t)) (setf (flic-app-saturated? object) '#t)) (else ;; make the fn a nested flic-app (multiple-value-bind (arghead argtail) (split-list args arity) (setf (flic-app-fn object) (make-flic-app (flic-app-fn object) arghead '#t)) (setf (flic-app-args object) argtail) (when var (setf (var-optimized-refs? var) '#t)) (dolist (a arghead) (postoptimize a)))) ))) (define-postoptimize flic-ref (object) (let ((var (flic-ref-var object))) (if (not (var-toplevel? var)) (incf (var-referenced var))) (setf (var-standard-refs? var) '#t))) (define-postoptimize flic-const (object) object) (define-postoptimize flic-pack (object) object) (define-postoptimize flic-and (object) (for-each (function postoptimize) (flic-and-exps object))) (define-postoptimize flic-case-block (object) (for-each (function postoptimize) (flic-case-block-exps object))) (define-postoptimize flic-if (object) (postoptimize (flic-if-test-exp object)) (postoptimize (flic-if-then-exp object)) (postoptimize (flic-if-else-exp object))) (define-postoptimize flic-return-from (object) (postoptimize (flic-return-from-exp object))) (define-postoptimize flic-sel (object) (postoptimize (flic-sel-exp object))) (define-postoptimize flic-is-constructor (object) (postoptimize (flic-is-constructor-exp object))) (define-postoptimize flic-con-number (object) (postoptimize (flic-con-number-exp object))) (define-postoptimize flic-void (object) object)