Discussion:
Observations about load-time.
(too old to reply)
Kaz Kylheku
2020-05-02 20:44:26 UTC
Permalink
In improving the compilation of a load-time operator (very similar to
ANSI Lisp load-time-value) I discovered an interesting "law".

If a load-time is wrapped in another one without an intervening lambda,
the inner load-time can disappear

(load-time (f (load-time x))) -> (load-time (f x))

So that is to say, an expression in an inner load-time does not need to
be independently hoisted to load-time; it's already hoisted there by the
outer load-time which requires its value.

But if the inner load-time is in a lambda that doesn't enclose the outer
load-time, then not:

(load-time (lambda (&optional (arg (load-time x)))
(load-time y)))

---/---> ;; invalid transformation!

(load-time (lambda (&optional x)
y))

The reason is that the x and y expressions evaluate when the lambda is
called, and that may be outside of load-time. The transformation will
result in x and y being calculated each time the lambda is called,
against the wishes expressed by load-time.

Avoiding independently hoisting every single load-time expression to
load time can save on the amount of global state information needed to
track all those values. Moreover, hoisting every expression could cause
spurious garbage to be retained.

Example:

(load-time (+ (load-time (calculate-bignum-1))
(load-time (calculate-bignum-2))))


All we want is the resulting bignum, but if the inner load-times are
separately hoisted to load-time and record their own hidden load-time
value in a global space, then it may be the case that those intermediate
bignums won't be garbage-collected. (Because, say, the collector treats
all load-time value slots as GC roots, as long as functions exist to
which those values are attached, even if the code will never be executed
again.)

A carefully tuned load-time operator could be used as the compiler's
mechanism for hoisting values to the top level, such as
"lambda-lifting". If a lambda is identified as liftable, the compiler
can wrap it in load-time. E.g.

(defun add-1 (list)
(mapcar (lambda (x) (+ 1 x)) arg))

Here the lambda doesn't refer to anything in the lexical environment.
It refers to the function + which is standard; it can be relied upon to
exist at the time the defun is evaluated. Therefore, this transformation
is correct, and ensures that the lambda object is instantiated just
once:

(defun add-1 (list)
(mapcar (load-time (lambda (x) (+ 1 x))) arg))

This is basically manual lambda lifting; the compiler could insert
that as a way of implementing lambda lifting.

If a compiler uses the load-time operator for lambda (and other)
lifting, then considerations of treating the nesting become significant,
because it will occur all over the place.
--
TXR Programming Lanuage: http://nongnu.org/txr
Music DIY Mailing List: http://www.kylheku.com/diy
ADA MP-1 Mailing List: http://www.kylheku.com/mp1
Kaz Kylheku
2020-05-03 16:04:03 UTC
Permalink
Post by Kaz Kylheku
In improving the compilation of a load-time operator (very similar to
ANSI Lisp load-time-value) I discovered an interesting "law".
If a load-time is wrapped in another one without an intervening lambda,
the inner load-time can disappear
(load-time (f (load-time x))) -> (load-time (f x))
Forgotten discussion: imperative looping and selection constructs.

Given

(load-time (when condition (load-time expr)))

then if the inner load-time is performed earnestly, expr is hoisted
out and always evaluated. If load-time is elided, then expr is evaluated
only when condition is true.

We can hand-wave this away by documenting load-time as not being
required to hoist anything anywhere.

Or else, the special forms that have special evaluation semantics can be
treated like lambda. If they happen occur in load-time, their specially
evaluated arguments can be nonetheless treated as non-load-time.

Loading...