Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to evaluate an expression in nim?

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!

like image 467
Benjamin Audren Avatar asked Dec 18 '20 08:12

Benjamin Audren


2 Answers

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.

like image 171
PMunch Avatar answered Nov 18 '22 16:11

PMunch


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)

Why?

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 consts and literals to it now, which is the correct behavior.

like image 2
lqdev Avatar answered Nov 18 '22 17:11

lqdev