Reputation: 384
I'm trying to perform the equivalent of the eval
method from Python in nim.
I was under the impression that parseStmt
from the macros
package should help me with this, but I'm facing a compilation issue that I don't understand.
import macros
echo parseStmt("1 + 2")
I would have expected this to print 3
when executed, but instead the compilation complains that
Error: request to generate code for .compileTime proc: $
I found this thread, and the examples there work, and following this, I was able to make the following program that works as I would expect:
import macros
import strformat
macro eval(value: string): untyped =
result = parseStmt fmt"{value}"
echo eval("1+2")
But I don't undertand why it needs to be written in this way at all. If I inline the statement, let value = "1 + 2"; echo parseStmt fmt"{value}"
, I get the same compile error as above.
Also, why is parseStmt value
different from parseStmt fmt"{value}"
, in the context of the eval
macro above?
What am I missing here?
Thank you in advance for any clarifications!
Upvotes: 5
Views: 1636
Reputation: 1719
Unlike Python which is an interpreted language, Nim is compiled. This means that all code is parsed and turned into machine code on compile-time and the program that you end up with doesn't really know anything about Nim at all (at least as long as you don't import the Nim compiler as a module, which is possible). So parseStmt
and all macro/template expansion stuff in Nim is done completely during compilation. The error, although maybe a bit hard to read, is trying to tell you that what was passed to $
(which is the convert-to-string operator in Nim, and called by echo
on all its arguments) is a compile-time thing that can't be used on runtime. In this case it's because parseStmt
doesn't return "3"
, it returns something like NimNode(kind: nnkIntLit, intVal: 3)
, and the NimNode
type is only available during compile-time. Nim however allows you to run code on compile-time to return other code, this is what a macro does. The eval
macro you wrote there takes value
which is any statement that resolves to a string during runtime, passed as a NimNode
. This is also why result = parseStmt value
won't work in your case, because value
is not yet a string, but could be something like reading a string from standard input which would result in a string during runtime. However the use of strformat
here is a bit confusing and overkill. If you change your code to:
import macros
macro eval(value: static[string]): untyped =
result = parseStmt value
echo eval("1+2")
It will work just fine. This is because we have now told Nim that value
needs to be a static
i.e. known during compile-time. In this case the string literal "1+2"
is obviously known at compile-time, but this could also be a call to a compile-time procedure, or even the output of staticRead
which reads a file during compilation.
As you can see Nim is very powerful, but the barrier between compile-time and run-time can sometimes be a bit confusing. Note also that your eval
procedure doesn't actually evaluate anything at compile-time, it simply returns the Nim code 1 + 2
so your code ends up being echo 1 + 2
. If you want to actually run the code at compile-time you might want to look into compile-time procedures.
Hope this helps shed some light on your issue.
Upvotes: 12
Reputation: 403
Note: while this answer outlines why this happens, keep in mind that what you're trying to do probably won't result in what you want (which I assumed to be runtime evaluation of expressions).
You're trying to pass a NimNode
to parseStmt
which expects a string
. The fmt
macro automatically stringifies anything in the {}
, you can omit the fmt
by doing $value
to turn the node into a string.
As I already noted, this will not work as it does in Python: Nim does not have runtime evaluation. The expression in the string is going to be evaluated at compile time, so a simple example like this will not do what you want:
import std/rdstdin
let x = readLineFromStdin(">")
echo eval(x)
First of all, because you're stringifying the AST you pass to the eval
, it's not the string
behind the x
variable that's going to get passed to the macro - it's going to be the symbol that denotes the x
variable. If you stringify a symbol, you get the underlying identifier, which means that parseStmt
will receive "x"
as its parameter. This will effect in the string stored in x
being printed out, which is wrong.
What you want instead is the following:
import std/rdstdin
import std/macros
macro eval(value: static string): untyped =
result = parseStmt(value)
echo eval("1 + 2")
This prevents runtime-known values from being passed to the macro. You can only pass const
s and literals to it now, which is the correct behavior.
Upvotes: 2