[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[Guile-commits] 02/03: peval: Enable inlining for functions with kwargs
From: |
Andy Wingo |
Subject: |
[Guile-commits] 02/03: peval: Enable inlining for functions with kwargs |
Date: |
Wed, 13 Mar 2024 15:29:13 -0400 (EDT) |
wingo pushed a commit to branch main
in repository guile.
commit f95bf6921e13799abca6a0a13087609c42baba6b
Author: Andy Wingo <wingo@pobox.com>
AuthorDate: Wed Mar 13 20:19:59 2024 +0100
peval: Enable inlining for functions with kwargs
* module/language/tree-il/peval.scm (peval): Handle all lambda inlining
the same, and extend with support for multiple clauses and keyword
arguments.
* test-suite/tests/peval.test ("case-lambda"): Enable kwarg inlining.
---
module/language/tree-il/peval.scm | 428 +++++++++++++++++++++++---------------
test-suite/tests/peval.test | 14 +-
2 files changed, 274 insertions(+), 168 deletions(-)
diff --git a/module/language/tree-il/peval.scm
b/module/language/tree-il/peval.scm
index 1eb928f07..dd777d863 100644
--- a/module/language/tree-il/peval.scm
+++ b/module/language/tree-il/peval.scm
@@ -1,6 +1,6 @@
;;; Tree-IL partial evaluator
-;; Copyright (C) 2011-2014,2017,2019-2023 Free Software Foundation, Inc.
+;; Copyright (C) 2011-2014,2017,2019-2024 Free Software Foundation, Inc.
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
@@ -1554,7 +1554,83 @@ top-level bindings from ENV and return the resulting
expression."
(make-primcall src name args)))))
(($ <call> src orig-proc orig-args)
- ;; todo: augment the global env with specialized functions
+ (define (residualize-call)
+ (make-call src (for-call orig-proc) (map for-value orig-args)))
+
+ (define (singly-referenced-lambda? proc)
+ (match proc
+ (($ <lambda>) #t)
+ (($ <lexical-ref> _ _ sym)
+ (and (not (assigned-lexical? sym))
+ (= (lexical-refcount sym) 1)
+ (singly-referenced-lambda?
+ (operand-source (lookup sym)))))
+ (_ #f)))
+
+ (define (attempt-inlining proc names syms vals body)
+ (define inline-key (source-expression proc))
+ (define existing-counter (find-counter inline-key counter))
+ (define inlined-exp (make-let src names syms vals body))
+
+ (cond
+ ((and=> existing-counter counter-recursive?)
+ ;; A recursive call. Process again in tail context.
+
+ ;; Mark intervening counters as recursive, so we can
+ ;; handle a toplevel counter that recurses mutually with
+ ;; some other procedure. Otherwise, the next time we see
+ ;; the other procedure, the effort limit would be clamped
+ ;; to 100.
+ (let lp ((counter counter))
+ (unless (eq? counter existing-counter)
+ (set-counter-recursive?! counter #t)
+ (lp (counter-prev counter))))
+
+ (log 'inline-recurse inline-key)
+ (loop inlined-exp env counter ctx))
+ ((singly-referenced-lambda? orig-proc)
+ ;; A lambda in the operator position of the source
+ ;; expression. Process again in tail context.
+ (log 'inline-beta inline-key)
+ (loop inlined-exp env counter ctx))
+ (else
+ ;; An integration at the top-level, the first
+ ;; recursion of a recursive procedure, or a nested
+ ;; integration of a procedure that hasn't been seen
+ ;; yet.
+ (log 'inline-begin exp)
+ (let/ec k
+ (define (abort)
+ (log 'inline-abort exp)
+ (k (residualize-call)))
+ (define new-counter
+ (cond
+ ;; These first two cases will transfer effort from
+ ;; the current counter into the new counter.
+ (existing-counter
+ (make-recursive-counter recursive-effort-limit
+ operand-size-limit
+ existing-counter counter))
+ (counter
+ (make-nested-counter abort inline-key counter))
+ ;; This case opens a new account, effectively
+ ;; printing money. It should only do so once for
+ ;; each call site in the source program.
+ (else
+ (make-top-counter effort-limit operand-size-limit
+ abort inline-key))))
+ (define result
+ (loop inlined-exp env new-counter ctx))
+
+ (when counter
+ ;; The nested inlining attempt succeeded. Deposit the
+ ;; unspent effort and size back into the current
+ ;; counter.
+ (transfer! new-counter counter))
+
+ (log 'inline-end result exp)
+ result))))
+
(let revisit-proc ((proc (visit orig-proc 'operator)))
(match proc
(($ <primitive-ref> _ name)
@@ -1563,167 +1639,193 @@ top-level bindings from ENV and return the resulting
expression."
(augment-var-table-with-externally-introduced-lexicals
exp store))
(for-tail exp)))
- (($ <lambda> _ _
- ($ <lambda-case> _ req opt rest #f inits gensyms body #f))
- ;; Simple case: no keyword arguments.
- ;; todo: handle the more complex cases
- (let* ((nargs (length orig-args))
- (nreq (length req))
- (opt (or opt '()))
- (rest (if rest (list rest) '()))
- (nopt (length opt))
- (key (source-expression proc)))
- (define (singly-referenced-lambda? orig-proc)
- (match orig-proc
- (($ <lambda>) #t)
- (($ <lexical-ref> _ _ sym)
- (and (not (assigned-lexical? sym))
- (= (lexical-refcount sym) 1)
- (singly-referenced-lambda?
- (operand-source (lookup sym)))))
+
+ (($ <lambda> _ _ clause)
+ ;; A lambda. Attempt to find the matching clause, if
+ ;; possible.
+ (define (inline-clause req opt rest kw inits gensyms body
+ arity-mismatch)
+ (define (bind name sym val binds)
+ (cons (vector name sym val) binds))
+ (define (has-binding? binds sym)
+ (match binds
+ (() #f)
+ ((#(n s v) . binds)
+ (or (eq? s sym) (has-binding? binds sym)))))
+
+ ;; The basic idea is that we are going to transform an
+ ;; expression like ((lambda (param ...) body) arg ...)
+ ;; into (let ((param arg) ...) body). However, we have to
+ ;; consider order of effects and scope: the args are
+ ;; logically parallel, whereas initializer expressions for
+ ;; params that don't have arguments are evaluated in
+ ;; order, after the arguments. Therefore we have a set of
+ ;; parallel bindings, abbreviated pbinds, which proceed
+ ;; from the call site, and a set of serial bindings, the
+ ;; sbinds, which result from callee initializers. We
+ ;; collect these in reverse order as we parse arguments.
+ ;; The result is an outer let for the parallel bindings
+ ;; containing a let* of the serial bindings and then the
+ ;; body.
+
+ (define (process-req req syms args pbinds sbinds)
+ (match req
+ (() (process-opt (or opt '()) syms inits args pbinds sbinds))
+ ((name . req)
+ (match syms
+ ((sym . syms)
+ (match args
+ (() (arity-mismatch))
+ ((arg . args)
+ (process-req req syms args
+ (bind name sym arg pbinds)
+ sbinds))))))))
+
+ (define (keyword-arg? exp)
+ (match exp
+ (($ <const> _ (? keyword?)) #t)
+ (_ #f)))
+ (define (not-keyword-arg? exp)
+ (match exp
+ ((or ($ <const> _ (not (? keyword?)))
+ ($ <void>)
+ ($ <primitive-ref>)
+ ($ <lambda>))
+ #t)
(_ #f)))
- (define (inlined-call)
- (let ((req-vals (list-head orig-args nreq))
- (opt-vals (let lp ((args (drop orig-args nreq))
- (inits inits)
- (out '()))
- (match inits
- (() (reverse out))
- ((init . inits)
- (match args
- (()
- (lp '() inits (cons init out)))
- ((arg . args)
- (lp args inits (cons arg out))))))))
- (rest-vals (cond
- ((> nargs (+ nreq nopt))
- (list (make-primcall
- #f 'list
- (drop orig-args (+ nreq nopt)))))
- ((null? rest) '())
- (else (list (make-const #f '()))))))
- (if (>= nargs (+ nreq nopt))
- (make-let src
- (append req opt rest)
- gensyms
- (append req-vals opt-vals rest-vals)
- body)
- ;; The default initializers of optional arguments
- ;; may refer to earlier arguments, so in the general
- ;; case we must expand into a series of nested let
- ;; expressions.
- ;;
- ;; In the generated code, the outermost let
- ;; expression will bind all required arguments, as
- ;; well as the empty rest argument, if any. Each
- ;; optional argument will be bound within an inner
- ;; let.
- (make-let src
- (append req rest)
- (append (list-head gensyms nreq)
- (last-pair gensyms))
- (append req-vals rest-vals)
- (fold-right (lambda (var gensym val body)
- (make-let src
- (list var)
- (list gensym)
- (list val)
- body))
- body
- opt
- (list-head (drop gensyms nreq)
nopt)
- opt-vals)))))
+ (define (process-opt opt syms inits args pbinds sbinds)
+ (match opt
+ (() (process-rest syms inits args pbinds sbinds))
+ ((name . opt)
+ (match inits
+ ((init . inits)
+ (match syms
+ ((sym . syms)
+ (cond
+ (kw
+ (match args
+ ((or () ((? keyword-arg?) . _))
+ ;; Optargs and kwargs; stop optarg dispatch at
+ ;; first keyword.
+ (process-opt opt syms inits args pbinds
+ (bind name sym init sbinds)))
+ (((? not-keyword-arg? arg) . args)
+ ;; Arg is definitely not a keyword; it is an
+ ;; optarg.
+ (process-opt opt syms inits args
+ (bind name sym arg pbinds)
+ sbinds))
+ (_
+ ;; We can't tell whether the arg is a keyword
+ ;; or not! Annoying semantics, this.
+ (residualize-call))))
+ (else
+ ;; No kwargs.
+ (match args
+ (()
+ (process-opt opt syms inits args pbinds
+ (bind name sym init sbinds)))
+ ((arg . args)
+ (process-opt opt syms inits args
+ (bind name sym arg pbinds)
+ sbinds))))))))))))
+
+ (define (process-rest syms inits args pbinds sbinds)
+ (match rest
+ (#f
+ (match kw
+ ((#f . kw)
+ (process-kw kw syms inits args pbinds sbinds))
+ (#f
+ (unless (and (null? syms) (null? inits))
+ (error "internal error"))
+ (match args
+ (() (finish pbinds sbinds body))
+ (_ (arity-mismatch))))))
+ (rest
+ (match syms
+ ((sym . syms)
+ (let ((rest-val (make-primcall src 'list args)))
+ (unless (and (null? syms) (null? inits))
+ (error "internal error"))
+ (finish pbinds (bind rest sym rest-val sbinds)
+ body)))))))
+
+ (define (process-kw kw syms inits args pbinds sbinds)
+ ;; Require that the ordered list of the keywords'
+ ;; syms is the same as the remaining gensyms to bind.
+ ;; Psyntax emits tree-il with this property, and it
+ ;; is required by (and checked by) other parts of the
+ ;; compiler, e.g. tree-il-to-cps lowering.
+ (unless (equal? syms (match kw (((k name sym) ...) sym)))
+ (error "internal error: unexpected kwarg syms"))
+
+ (define (process-kw-args positional? args pbinds)
+ (match args
+ (()
+ (process-kw-inits kw inits pbinds sbinds))
+ ((($ <const> _ (? keyword? keyword)) arg . args)
+ (match (assq keyword kw)
+ ((keyword name sym)
+ ;; Because of side effects, we don't
+ ;; optimize passing the same keyword arg
+ ;; multiple times.
+ (if (has-binding? pbinds sym)
+ (residualize-call)
+ (process-kw-args #f args
+ (bind name sym arg pbinds))))
+ (#f (residualize-call))))
+ (((? not-keyword-arg?) . args)
+ (if positional?
+ (arity-mismatch)
+ (residualize-call)))
+ (_ (residualize-call))))
+
+ (define (process-kw-inits kw inits pbinds sbinds)
+ (match kw
+ (()
+ (unless (null? inits) (error "internal error"))
+ (finish pbinds sbinds body))
+ (((keyword name sym) . kw)
+ (match inits
+ ((init . inits)
+ (process-kw-inits kw inits pbinds
+ (if (has-binding? pbinds sym)
+ sbinds
+ (bind name sym init
sbinds))))))))
+
+ (process-kw-args #t args pbinds))
+
+ (define (finish pbinds sbinds body)
+ (match sbinds
+ (()
+ (match (reverse pbinds)
+ ((#(name sym val) ...)
+ (attempt-inlining proc name sym val body))))
+ ((#(name sym val) . sbinds)
+ (finish pbinds sbinds
+ (make-let src (list name) (list sym) (list val)
+ body)))))
+
+ ;; Limitations:
+ ;;
+ ;; - #:key or #:rest, but not both.
+ ;; - #:allow-other-keys unsupported.
(cond
- ((or (< nargs nreq) (and (null? rest) (> nargs (+ nreq nopt))))
- ;; An error, or effecting arguments.
- (make-call src (for-call orig-proc) (map for-value orig-args)))
- ((or (and=> (find-counter key counter) counter-recursive?)
- (singly-referenced-lambda? orig-proc))
- ;; A recursive call, or a lambda in the operator
- ;; position of the source expression. Process again in
- ;; tail context.
- ;;
- ;; In the recursive case, mark intervening counters as
- ;; recursive, so we can handle a toplevel counter that
- ;; recurses mutually with some other procedure.
- ;; Otherwise, the next time we see the other procedure,
- ;; the effort limit would be clamped to 100.
- ;;
- (let ((found (find-counter key counter)))
- (if (and found (counter-recursive? found))
- (let lp ((counter counter))
- (if (not (eq? counter found))
- (begin
- (set-counter-recursive?! counter #t)
- (lp (counter-prev counter)))))))
-
- (log 'inline-recurse key)
- (loop (inlined-call) env counter ctx))
+ ((and kw (or rest (match kw ((aok? . _) aok?))))
+ (residualize-call))
(else
- ;; An integration at the top-level, the first
- ;; recursion of a recursive procedure, or a nested
- ;; integration of a procedure that hasn't been seen
- ;; yet.
- (log 'inline-begin exp)
- (let/ec k
- (define (abort)
- (log 'inline-abort exp)
- (k (make-call src (for-call orig-proc)
- (map for-value orig-args))))
- (define new-counter
- (cond
- ;; These first two cases will transfer effort
- ;; from the current counter into the new
- ;; counter.
- ((find-counter key counter)
- => (lambda (prev)
- (make-recursive-counter recursive-effort-limit
- operand-size-limit
- prev counter)))
- (counter
- (make-nested-counter abort key counter))
- ;; This case opens a new account, effectively
- ;; printing money. It should only do so once
- ;; for each call site in the source program.
- (else
- (make-top-counter effort-limit operand-size-limit
- abort key))))
- (define result
- (loop (inlined-call) env new-counter ctx))
-
- (if counter
- ;; The nested inlining attempt succeeded.
- ;; Deposit the unspent effort and size back
- ;; into the current counter.
- (transfer! new-counter counter))
-
- (log 'inline-end result exp)
- result)))))
- (($ <lambda> src-proc meta orig-body)
- ;; If there are multiple cases and one matches nargs, omit all the
others.
- (or (and
- orig-body
- (lambda-case-alternate orig-body)
- (let ((nargs (length orig-args)))
- (let loop ((body orig-body))
- (match body
- (#f #f) ;; No matching case; an error.
- (($ <lambda-case> src-case req opt rest kw inits
gensyms case-body alt)
- (cond (kw
- ;; FIXME: Not handling keyword cases.
- #f)
- ((let ((nreq (length req)))
- (if rest
- (<= nreq nargs)
- (<= nreq nargs (+ nreq (if opt (length opt)
0)))))
- ;; Keep only this case.
- (revisit-proc
- (make-lambda
- src-proc meta
- (make-lambda-case src-case req opt rest kw
inits gensyms case-body #f))))
- (else (loop alt))))))))
- (make-call src (for-call orig-proc) (map for-value
orig-args))))
+ (process-req req gensyms orig-args '() '()))))
+
+ (let lp ((clause clause))
+ (match clause
+ ;; No clause matches.
+ (#f (residualize-call))
+ (($ <lambda-case> src req opt rest kw inits gensyms body alt)
+ (inline-clause req opt rest kw inits gensyms body
+ (lambda () (lp alt)))))))
+
(($ <let> _ _ _ vals _)
;; Attempt to inline `let' in the operator position.
;;
@@ -1747,10 +1849,10 @@ top-level bindings from ENV and return the resulting
expression."
;; traverse through lambdas. In that case re-visit
;; the procedure.
(proc (revisit-proc proc)))
- (make-call src (for-call orig-proc)
- (map for-value orig-args))))
- (_
- (make-call src (for-call orig-proc) (map for-value orig-args))))))
+ (residualize-call)))
+
+ (_ (residualize-call)))))
+
(($ <lambda> src meta body)
(case ctx
((effect) (make-void #f))
diff --git a/test-suite/tests/peval.test b/test-suite/tests/peval.test
index c96cfac21..756cccdf3 100644
--- a/test-suite/tests/peval.test
+++ b/test-suite/tests/peval.test
@@ -1,7 +1,7 @@
;;;; tree-il.test --- test suite for compiling tree-il -*- scheme -*-
;;;; Andy Wingo <wingo@pobox.com> --- May 2009
;;;;
-;;;; Copyright (C) 2009-2014, 2017, 2020, 2022-2023 Free Software
Foundation, Inc.
+;;;; Copyright (C) 2009-2014, 2017, 2020, 2022-2024 Free Software
Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
@@ -1523,10 +1523,14 @@
(const 0))
;; keyword cases survive
- (pass-if (= 1 ((case-lambda* ((a b) 0) ((a #:key x) 1)) 0 #:x 1)))
- (pass-if (= 0 ((case-lambda* ((a b c) 0) ((a #:key x) 1)) 0 #:x 1)))
- (pass-if (= 0 ((case-lambda* ((a #:key x) 0) ((a b) 0)) 0 #:x 1)))
- (pass-if (= 1 ((case-lambda* ((a #:key x) 0) ((a b c) 1)) 0 1 2))))
+ (pass-if-peval ((case-lambda* ((a b) 0) ((a #:key x) 1)) 0 #:x 1)
+ (const 1))
+ (pass-if-peval ((case-lambda* ((a b c) 0) ((a #:key x) 1)) 0 #:x 1)
+ (const 0))
+ (pass-if-peval ((case-lambda* ((a #:key x) 0) ((a b) 0)) 0 #:x 1)
+ (const 0))
+ (pass-if-peval ((case-lambda* ((a #:key x) 0) ((a b c) 1)) 0 1 2)
+ (const 1)))
(with-test-prefix "eqv?"
(pass-if-peval (eqv? x #f)