elpaca icon indicating copy to clipboard operation
elpaca copied to clipboard

Add `elpaca-thunk` macro

Open usaoc opened this issue 2 years ago • 7 comments

This patch provides a new elpaca-thunk macro that wraps the body directly in a thunk as opposed to an evaled form. The main expansion logic is abstracted into a separate function, which in turn is used by both the old elpaca and the new elpaca-thunk macros. The new macro is potentially useful to advanced users who want a more reliable compiled semantics.

usaoc avatar Aug 06 '23 11:08 usaoc

Thanks for taking the time to write a patch. Could you please include an example of a minimal init file which would make use of elpaca-thunk? That way I can byte compile it and test to see if everything works.

progfolio avatar Aug 06 '23 21:08 progfolio

The last commit adds an example file that uses elpaca-thunk in a way that will fail for elpaca once compiled. The interpreted and compiled semantics should stay identical, except that loading the compiled file does not bring in the compile-time dependency.

usaoc avatar Aug 07 '23 11:08 usaoc

Thanks for adding that. I'm wondering if we could make use of declaring a compiler-macro so that we don't need the elpaca-thunk variation. That would be beneficial because it would:

  • keep the API simpler
  • Potentially solve #158
  • Solve the compile-time semantics you're seeking with elpaca-thunk

I haven't experimented with compiler-macros much, but I was curious how eval-after-load worked and saw how it was implemented to allow the compiler to peek at the BODY. Thoughts?

progfolio avatar Aug 09 '23 01:08 progfolio

Compiler macro cannot solve the problem, because they are for implementing optimizing semantics for functions. Here we want an alternative semantics altogether, not simply optimizing (i.e. observationally equivalent). Moreover, even with our current macro approach, there’s no reliable means for a macro to decide the exact bootstrap-time dependency of the body.

On this topic, what eval-after-load does is unfortunately both shallow in scope and, most importanly, unsound: It simply checks whether the form is a directly quoted form and, if so, wraps the form in a thunk instead. Expectedly, this is unsound as it can change the semantics.

In conclusion, I’m afraid there’s nothing that solves the dependency problem better than a conscious Elisp programmer does.

usaoc avatar Aug 09 '23 08:08 usaoc

On this topic, what eval-after-load does is unfortunately both shallow in scope and, most importanly, unsound: It simply checks whether the form is a directly quoted form and, if so, wraps the form in a thunk instead. Expectedly, this is unsound as it can change the semantics.

Can you give an example of how the "unsoundness" of eval-after-load would cause a practical problem. It would help me understand the limitation of such an approach.

progfolio avatar Aug 10 '23 01:08 progfolio

Phase mismatch will expose the difference in behavior, as is the case with elpaca{,-thunk}. Consider #161 again:

(eval-after-load 'foo
  '(foo-macro ...))

This works for interpreted, but not compiled code! Compiler macros are not supposed to change the behavior of the code. From a user’s perspective, this is as confusing as it can get, as the code is explicitly quoted to avoid the dependency problem. In general, my opinion is that the user should be in full control of what happens at what time. I assume with-eval-after-load is encouraged for exactly this reason.

usaoc avatar Aug 10 '23 09:08 usaoc

Phase mismatch will expose the difference in behavior, as is the case with elpaca{,-thunk}. Consider #161 again:

(eval-after-load 'foo
  '(foo-macro ...))

This works for interpreted, but not compiled code! Compiler macros are not supposed to change the behavior of the code. From a user’s perspective, this is as confusing as it can get, as the code is explicitly quoted to avoid the dependency problem. In general, my opinion is that the user should be in full control of what happens at what time. I assume with-eval-after-load is encouraged for exactly this reason.

The same issue exists with with-eval-after-load apparently:

(with-eval-after-load 'test
  (test (message "PASS")))

(defmacro test (&rest body)
  "Test BODY."
  `(progn ,@body))

(provide 'test)

Will still warn about the macro test being defined too late.

This emacs-devel thread discusses the issues:

https://lists.gnu.org/archive/html/emacs-devel/2018-02/msg00516.html

And it looks like it boils down to (as far as Stefan is concerned) to "don't use a macro" or "quote what you want to hide from the macro-exapnsion and (therefore) the byte-compiler". I can see why the Emacs developers recommend against byte-compiling one's init file.

I'll have to think on this more, but my goal is to keep things pragmatic and simple.

progfolio avatar Aug 10 '23 12:08 progfolio