Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add elpaca-thunk macro #173

Open
wants to merge 2 commits into
base: fix/thunk
Choose a base branch
from
Open

Add elpaca-thunk macro #173

wants to merge 2 commits into from

Conversation

usaoc
Copy link

@usaoc usaoc commented Aug 6, 2023

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.

@progfolio
Copy link
Owner

progfolio commented Aug 6, 2023

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.

@usaoc
Copy link
Author

usaoc commented Aug 7, 2023

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.

@progfolio
Copy link
Owner

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:

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?

@usaoc
Copy link
Author

usaoc commented Aug 9, 2023

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.

@progfolio
Copy link
Owner

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.

@usaoc
Copy link
Author

usaoc commented Aug 10, 2023

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.

@progfolio
Copy link
Owner

progfolio commented Aug 10, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants