qhwa

qhwa

Formular - A tiny DSL engine / code evaluator (configuration as code)

Hi, all,

I just published Formular package. It is a tiny library that evaluates a piece of Elixir code.

Online documentation

On the shoulder of Elixir’s Code module

Given a piece of Elixir code (as a string, or AST), Formular runs it with Elixir’s Code module under some security limitations.

So far, the limitations are:

  • No calling module functions;
  • No calling exit;
  • No sending messages.

Indeed, the whole library consists of only one thin module, thanks to the power Elixir has already shipped out of the box.

Motivation

Formular was developed to support some dynamic configuration scenarios. For example, in a scene of an online book store, the discount of a book can be dynamically configured as a piece of code, then evaluated by Formular:

iex> discount_formula = ~s"
...>   case order do
...>     # old books get a big promotion
...>     %{book: %{year: year}} when year < 2000 ->
...>       0.5
...>   
...>     %{book: %{tags: tags}} ->
...>       # Elixir books!
...>       if ~s{elixir} in tags do
...>         0.9
...>       else
...>         1.0
...>       end
...>
...>     _ ->
...>       1.0
...>   end
...> "
...>
...> book_order = %{
...>   book: %{
...>     title: "Elixir in Action", year: 2019, tags: ["elixir"]
...>   }
...> }
...>
...> Formular.eval(discount_formula, [order: book_order])
{:ok, 0.9}

In such a way, the discount calculation code, which changes frequently, is separated away from the stable business flow, and the primary code is probably more generic and flexible.

I’ve been using it in production for a while so I publish it today in case others may find it useful too.

Cheers!

Most Liked

hauleth

hauleth

DoS (atom exhaustion):

Formular.eval(~S|for a <- %Range{first: 0, last: 100_000, step: 1}, do: :"#{a}"|, [])

I needed to create range manually, as you do not export ../2 operator.

qhwa

qhwa

Two new versions have been released:

  • v0.2.2

    • brings performance improvements to 0.2.x
  • v0.3.0

    • allows limiting the execution time and heap size
    • allows compiling a code string to an Elixir module which brings more performance improvements,
    • adds new API Formula.used_vars/2 to extract the used variable names in the formula.

Highlights:

Performance improvements:

A simple benchmark on a simple formula results in this:

Name                      ips        average  deviation         median         99th %
compiled_module      947.84 K        1.06 μs  ±5536.01%        0.83 μs        1.45 μs
eval                  14.53 K       68.82 μs     ±8.93%       68.69 μs       87.93 μs
eval_ast               4.86 K      205.58 μs    ±18.43%      193.89 μs      333.89 μs

Comparison: 
compiled_module      947.84 K
eval                  14.53 K - 65.23x slower +67.76 μs
eval_ast               4.86 K - 194.85x slower +204.52 μs

The compilation approach is about 80x faster than the original eval approach in v0.2.2, and 300x faster than versions prior to v0.2.2.

Restricted evaluation:

  • limited execution time:

    Formular.eval(code, timeout: :timer.seconds(5))
    
  • limited max heap size by word:

    Formular.eval(code, max_heap_size: 15_000)
    

If limited, the evaluation will be run in a separate process.

Extract used vars API:

This can be helpful if you need to build some UI arround the formula.

iex> Formular.used_vars("a + b - 1")
[:a, :b]
mat-hek

mat-hek

Membrane Core Team

Nice, though I’m curious if you tried using Sand?

qhwa

qhwa

I played with Sand as @mat-hek shared and it does what I wanted. Not implying by the name, under the scene, Sand runs the code with Code.eval_quoted/3 too. Only in a separated process which can be limited in reductions & memory usage. I think that is the right way to go.

Also, I did some benchmarks, and the result was surprising at first glance.

Code:

code = """
  squares = %{3 => 9, 4 => 16, 5 => 25}
  squares[3]
"""

ast = Code.string_to_quoted!(code)

Benchee.run(%{
  eval: fn -> {:ok, 9} = Formular.eval(code, []) end,
  eval_ast: fn -> {:ok, 9} = Formular.eval(ast, []) end,
  sand_run: fn -> {:ok, 9, _} = Sand.run(code) end,
  sand_run_without_cpu_memory_monitoring: fn ->
    {9, _} = Sand.run_without_cpu_memory_monitoring(code)
  end
})

(Sand doesn’t accept AST at this moment)

Result:

Name                                             ips        average  deviation         median         99th %
sand_run_without_cpu_memory_monitoring       23.91 K       41.82 μs    ±20.35%       39.04 μs       70.77 μs
sand_run                                      3.50 K      285.99 μs    ±28.04%      269.01 μs      509.55 μs
eval_ast                                      3.35 K      298.88 μs     ±9.51%      293.91 μs      437.12 μs
eval                                          3.09 K      323.70 μs     ±9.67%      317.00 μs      467.09 μs

Comparison: 
sand_run_without_cpu_memory_monitoring       23.91 K
sand_run                                      3.50 K - 6.84x slower +244.18 μs
eval_ast                                      3.35 K - 7.15x slower +257.06 μs
eval                                          3.09 K - 7.74x slower +281.88 μs

I figured out the reason after some research and it is very interesting. I’m excited about the work ahead.

Thank you @hauleth , really appreciate it. I know the next direction now.

qhwa

qhwa

I’m glad to see it is helpful to you too! And I really appreciate that you give it a try.

Please be aware that, as discussed here, Formular is not a safe sandbox right now. The design purpose is more about compiling your configuration into runnable code inside the application. So if the code comes from some untrusted user inputs, it could potentially damage the system. Improving is on the way though.

Where Next?

Popular in Libraries Top

Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 13801 100
New
nikokozak
Hello all, I’ve been working on Svonix - a library for quickly integrating Svelte components into Phoenix views. It’s a much-needed succ...
New
archan937
It is a well-know topic within the Elixir community: “To mock or not to mock? :)” Every alchemist probably has his / her own opinion con...
New
arkgil
Hi all! I’m happy to announce that Telemetry v0.3.0 is out! This release marks the conversion from Elixir to Erlang so that all the libr...
New
martinthenth
Hello everybody :wave: Recently, some of my colleagues talked about database ids and uuids and their problems, and I remembered the pain...
New
mbuhot
EctoJob A transactional job queue built with Ecto, PostgreSQL and GenStage Available on Hex.pm: ecto_job | Hex Docs: API Reference — ec...
New
Antrater
Hi everyone! I’m thrilled to announce a huge thing. We have been developing Elixir Moon Design System for quite a while. We are finally ...
New
gabrielpoca
Hello everyone! I want to share with you something that I’m really proud of: https://stillstatic.io/ Still is a static site builder for...
New
tmbb
I’ve decided to create this topic to discuss optimization possibilities for something like Phoenix LiveView. I’ve created this topic unde...
New
gjaldon
As the title states, EctoEnum has just been updated after some time of hardly any activity in the repo. Here’s the latest release: https:...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Sub Categories:

We're in Beta

About us Mission Statement