Get the structure of an arithmetic expression


Given an arithmetic expression, for example x + y/z, I want to decompose it to something like +(x, /(y, z). Is it possible with Julia? I thought this was the result of :(x + y/z) but no.

The reason is that I have long arithmetic expression and I want to get math.add(x, math.divide(y, z)) for a JavaScript code. It’s painful to do that manually.

If you know how to do with another language this is ok too.

1 Like

You were probably misled by the source code syntax printing, expressions are indeed structured like you’re thinking:

julia> :(x + y/z)
:(x + y / z)

julia> :( +(x, /(y, z)) )
:(x + y / z)

julia> :( +(x, /(y, z)) ) == :(x + y/z)

julia> dump( :(x + y/z) )
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Symbol x
    3: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol /
        2: Symbol y
        3: Symbol z

Transpiling may not be this easy, there isn’t always a one-to-one correspondence of functions, and the arity of the most equivalent functions may differ, which can affect operator parsing.

Thanks. I only use the four arithmetic operators +, -, * and /, each binary.

I found a way in R, but I’m still curious concerning a Julia way.

I’m curious, why metaprogram in another language and transpile to Javascript instead of just writing the expressions with binary +,-,*,/ in Javascript to begin with? Is math.add and math.divide different? Does Javascript not have ways to modify and evaluate AST expressions?

Because I deal with complex numbers in JavaScript. math.add is the complex addition, etc.

1 Like

Ah that makes sense. And having looked it up it doesn’t seem like Javascript exposes ASTs as much as Julia and R does, or at least people don’t talk about it. math.js has its own parser and AST tooling, do you think that can serve your purposes? It may be a bit awkward because the scoping and parsing isn’t tied to the language, but it’s a lot better than doing the metaprogramming in an entirely different language and crossing fingers for a one-to-one translation.

That’s ok, I’ve finished to do what I wanted with the help of R.

In JavaScript there’s the acorn library which is a complete AST constructor, not only for arithmetic expressions. It constructs an AST for any piece of JavaScript code.

Here is a thread about AST parsers in several languages.

1 Like

Those are fun examples but not very general-purpose. acorn looks more general-purpose but at first browse I couldn’t see how metaprogramming could be done, so math.js’s parsing still seems like the more official way.

How are you transferring ASTs between separate languages anyway, strings? If so, that would explain the post; it’s not that the Expression is not structured right, it’s that printing and string deparses the same way to infix notation. I don’t know of a way to reduce infix notation as much as possible; it’s not possible entirely because some operators are infix-only, like _?_:_.

If you replace the operators with alphabetical symbols or expressions, it will be deparsed to function calls like you need; in the general case, it’s just a matter of recursively visiting nested Expr and replacing operator symbols mapped to alternate names. I imagine you did something similar in R.

julia> ex = :( +(x, /(y, z)) )
:(x + y / z)

julia> ex.args[1] = :(math.add); ex.args[3].args[1] = :(math.divide); ex
:(math.add(x, math.divide(y, z)))

julia> string(ex)
"math.add(x, math.divide(y, z))"

julia> function replacesymbols!(ex::Expr, symbolmap::AbstractDict{Symbol})
         ex.head = get(symbolmap, ex.head, ex.head)
         for i in eachindex(ex.args)
           arg = ex.args[i]
           if arg isa Symbol
             ex.args[i] = get(symbolmap, arg, arg)
           elseif arg isa Expr
             replacesymbols!(arg, symbolmap)
         ex # input is already mutated so not a necessary return

julia> altops = Dict(:+ => :(math.add),
                     :- => :(math.subtract),
                     :* => :(math.multiply),
                     :/ => :(math.divide),

julia> ex = :(a+b/c*e-f^g) # note that ^ is not in mapping
:((a + (b / c) * e) - f ^ g)

julia> replacesymbols!(ex, altops)
:(math.subtract(math.add(a, math.multiply(math.divide(b, c), e)), f ^ g))

julia> string(ex)
"math.subtract(math.add(a, math.multiply(math.divide(b, c), e)), f ^ g)"

Bear in mind that there aren’t always built-in alphabetical aliases for infix operators in Julia, for example ÷ has div but + is by itself, so only eval the expression prior to replacement in Julia.

Probably easier to use the MacroTools.jl package for this sort of thing. For example:

julia> using MacroTools

julia> symbolmap = Dict(:+ => :(math.add), :- => :(math.sub), :* => :(math.mul), :/ => :(math.div), :^ => :(math.pow));

julia> MacroTools.postwalk(x -> get(symbolmap, x, x), :(x + y / (z - 2y)))
:(math.add(x, math.div(y, math.sub(z, math.mul(2, y)))))