How to execute code from a string variable in Crystal?

1.2k views Asked by At

I like eval in Ruby because it works pretty straightforward:

eval("puts 7 * 8") # => 56

What is an eval's equivalent in Crystal ? I know that we can do something similar with macro:

macro eval(code)
 {{code.id}}
end

eval("puts 7 * 8") # => 56

But this won't work with runtime values:

a = "yo"
eval("puts #{a}") # => prints nothing
2

There are 2 answers

0
waj On BEST ANSWER

Crystal is a compiled language, while Ruby is interpreted. That makes evaluating code at runtime much more complicated.

In your example, the macro is expanded at compile time, so actually your program is just puts 7 * 8. In other words, it works because the code is known at compile time.

But if you wanted to execute the code contained in an arbitrary string, it would have to invoke the Crystal compiler and then execute the resulting executable. This is actually something we do in the Crystal unit tests. But there is no "eval" function included in the standard library because that would imply that your compiled program includes the Crystal compiler built in and it actually doesn't make much sense.

Another problem is how to pass arguments and take return values. Since the program you're running and the evaluated code are the result of different compilations, they might have different binary representations of the same types.

On the other hand, using eval in Ruby is usually known as a bad practice and must be avoided if possible.

0
kojix2 On

waj, one of the developers of the Crystal language, wrote in 2015

because that would imply that your compiled program includes the Crystal compiler built in and it actually doesn't make much sense.

But, including the interpreter in the executable may not be such a crazy idea in 2022. Anyolite can include mruby or CRuby in the executable.

https://github.com/Anyolite/anyolite

kitty.cr

require "anyolite"

code = ARGV[0]

class Kitty
  def initialize(@n : Int32)
  end

  def mew
    puts "mew " * @n
  end
end

Anyolite::RbInterpreter.create do |rb|
  Anyolite.wrap(rb, Kitty)
  Anyolite.eval(code)
end

build:

crystal build kitty.cr

run:

./kitty "Kitty.new(3).mew"

output:

mew mew mew

The generated executable is about 7MB larger (probably because it contains mruby). However, that is not a problem considering the advantage of being able to eval with Crystal. Yes, eval may be a bad practice. But I think there are cases where it can be very useful.