-
Notifications
You must be signed in to change notification settings - Fork 173
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
Dynamic evaluator evaluates too much #555
Comments
The problem stems from Eval.hs, line 460. See comment there. |
So, straight up removing the hack and returning the evaluated form as is has the following effects:
-> (meta-set! print-doc "doc" "Print the documentation for a binding.")
-> (meta-set! sig "doc" "Annotate a binding with the desired signature.")
-> (meta-set! print-sig "doc" "Print the annotated signature for a binding.")
-> (meta-set! hide "doc" "Mark a binding as hidden, this will make it not print with the 'info' command.")
-> (meta-set! private "doc" "Mark a binding as private, this will make it inaccessible from other modules.")
-> (meta-set! todo "doc" "sets the todo property for a binding.")
-> (meta-set! private? "doc" "Is this binding private?")
-> (meta-set! private? "todo" "This is buggy, will report true when meta is set to 'false'!")
-> (meta-set! hidden? "doc" "Is this binding hidden?")
-> (meta-set! hidden? "todo" "This is buggy, will report true when meta is set to 'false'!")
-> (meta-set! annotate "doc" "Add an annotation to this binding.")
-> (meta-set! defproject "doc" "Define a project configuration.")
-> (meta-set! const-assert "doc" "asserts that the expression `expr` is true at compile time.\nOtherwise it will fail with the message `msg`.\n\nThe expression must be evaluable at compile time.")
...many more meta-set! forms.
(list 'foo)
; => (foo)
;; contrasted with carp as it stands right now, executing
(list 'foo)
;; results in
; I couldn’t find the symbol 'foo' at line 1, column 8 in 'REPL'.
Maybe you wanted one of the following?
Bool
doc
fmt
for
not
or
todo
;; the extra eval we're trying to fix here.
鲮 (list 'defn 'foo (array) '2)
-> (defn foo [] 2)
鲮 foo
Can't find symbol 'foo' at REPL:1:1. So! The way forward seems to be to specialize the workaround to the specific forms we need to account for, namely, a returned def, defn, defndynamic, defmacro, etc. Ideally, we could simply remove the workaround and treat this as intended behavior. Returning a form that constitutes a definition and actually defining something are, after all, different things. We could argue that this is how macros/dynamic functions should work--they return forms--it's up to the user to determine when and how these forms are evaluated. However, there seems to be no way for the user to "force evaluation" of a form. Perhaps adding this functionality is the more appropriate solution? I will open a PR that pursues 1. If I can figure it out, I'll also open a change that pursues the alternative of adding a function that forces evaluation of a form, as I think this gives the programer more control and keeps the semantics cleaner (if we use solution 1. carp users have to recall that some forms, when returned, are evaluated immediately, while others aren't--hardly convenient.) |
Thanks for your investigations! I have worked on removing the hack, and yes, the problem is to make the returned code from macros "execute". I haven't gotten to any conclusions though, I can mess around more with it today and write up any of my own findings here. |
How about these terms do not get evaluated unless they’re passed to an |
That would work, but then you'd have to wrap calls to macros in eval to get them to do anything, like:
I have a feeling this can be solved in a nicer way... |
Not necessarily, you could do the |
Right, didn't think of that! Hmm, need to reconsider..! |
After looking through the code and poking at it a bit without much success, I think it would be beneficial if I do some kind of rewrite of the Eval module. Ideally we could decide on the desired semantics first though, so I have something more concrete to implement. |
The module as it is now is pretty beefy which makes it a little tricky to grok at times, so I think a rewrite could be good! I actually like the Is this issue only problematic in the REPL? Are there two passes for compilation--macro-expansion and then compile? Or are the REPL and compiler basically equivalent? My sense is that we can skirt around the issue if we have two passes for the compiler, but not for the REPL. Macro calls in the source file should be expanded, and then we should compile the resulting code (this is, I think, roughly how its handled in common lisp). However, in the REPL, it might be a nicer experience to return only the result of expansion on evaluation of a macro call, providing an explicit method like So, given this file: (defmacro id-function []
(list 'defn 'id (array 'x) 'x))
(id-function) The following happens on compile/load:
(defmacro id-function []
(list 'defn 'id (array 'x) 'x))
(defn id [x] x)
When you
I think evaluating (id-function)
;; => (defn id [x] x) If one wants to evaluate the result of a macro call in the REPL: (eval (id-function))
;; => id redefined in environment... This approach, I think, should make things like ;; foo.carp
(doc foo "foo function")
(defn foo [x] x)
Macro Expansion:
load:
In other words, the evaluator just does one pass on whatever forms it encounters, the compiler is designed to do two evaluator passes (macro-expansion phase and compilation phase) and the REPL is equivalent to the evaluator (one pass). Not sure if this fairytale approach actually meshes with Carp's design, though! |
Interesting thoughts! I'm not sold on the idea of having the REPL and file loading be different things – right now they are exactly the same which means you can work interactively (with a file in your editor) sending forms to the REPL process mixed with the I really like the analogy with a Haskell "macro monad" though! I guess that's what's used in template Haskell? And it also reminds me of the REPL in Idris where nothing is ever executed unless you tell it to be. Having that kind of more principled approach to the whole compile time programming model in Carp seems extremely worthwhile. Inspired by @hellerve's comment I started thinking though... Since macros can call side-effecting code, perhaps the way to solve this with top-level "things" like |
... and that actually works. For some reason, adding this plus turning off the "double step evaluation" in
The take-away from this macro is that it's not so much a code generator as a compile time function that does not evaluate it's arguments before being applied. Any thoughts? :) |
I think this makes sense. As I see it, there are two distinct ways in which we use the evaluator:
I thus am now tempted to think the default workflow should be evaluation, and generating code should be a special case. |
Ah interesting! So the "type" of macros is basically: If the distinction doesn't matter, I think removing the workaround from the evaluator and providing a function like |
@scolsen Sorry for never responding! I think they are quite different, but do not need to have different expressions within the language, i.e. when I say So, should we settle on providing something like |
As far as my understanding goes, to evaluate macros in other lisps, the evaluator treats macro expansion like a separate compiler pass: the macro expander walks the input list and expands all of the forms that are macros, then hands off the produced list to the next step in the compilation phase. If there are no macros to expand, then the expander simply returns its input. The REPL is essentially implemented with the following function:
What is stopping a similar mechanism from being implemented in Carp? |
The main problem is that we don’t have We do have macroexpansion as a pass between parsing and type inference, but the problem that is described above is that it is buggy, basically. |
Here’s another neat something I realized over the holidays: Once we have (defmacro defndynamic-evald [arg]
(let [e (eval arg)]
(list e (list ˋeval e))))
(defmacro defndyamic [name args body]
(list 'defmacro name args
(list 'let (flatten (map defndynamic-evald args))
body))) This depends on |
That's pretty wild! |
I think we can close this now, finally! |
There is something wrong with the implementation of the dynamic evaluation of Carp code. Expressions returning things that can be evaluated (i.e. lists) are evaluated an "extra" time, which is often wrong. This extra step needs to be removed, and the reason it was added there investigated.
One simple example of how this bug manifests is that if you enter a quoted expression in the repl it will evaluate:
The text was updated successfully, but these errors were encountered: