nsmith5/Maxima.jl

Support block quotes

Closed this issue · 17 comments

Supporting more general julia expressions would be really powerful. For example if,

julia> f = quote
           function sin_and_cos(x)
               return sin(x)^2 + cos(x)^2
           end
       end
quote  # REPL[1], line 2:
    function sin_and_cos(x) # REPL[1], line 3:
        return sin(x) ^ 2 + cos(x) ^ 2
    end
end

julia> trigsimp(f)
quote  # REPL[1], line 2:
    function sin_and_cos(x) # REPL[1], line 3:
        return 1
    end
end

worked, that would be amazing. Implications: writing really impressive macros.

Hi, I was actually planning on implementing quote blocks for you this week because I have it working for Reduce.jl. Nested quote blocks are not set up yet, but first-order quote blocks are definitely do-able. However, to make it work the MExpr type will have to be slightly modified to support an array of strings instead of single string. In order to support it in the rest of your program, all your other functions that depend on MExpr will need to be modified to work with the slight change.

Oh nice, though a general solution would be better. Why does MExpr have to be an array of strings? I know MacroTools has some good tools for expression walking that might help here...

Well, it could be possible to store all the statements into a single string that is delimited by ; or $. In fact, this is how I implemented the rcall function for Reduce. However, I decided to still go with an array of strings in the RExpr data type because I liked the idea of being able to split up the commands in that way. However, the array doesn't restrict you from putting multiple statements into a single string, that is still possible. Havin the array might not be necessary, but I thought it doesn't hurt to have it, also for displaying and readability purposes. Does Maxima have support for something like nested quote blocks? Perhaps, what could be done is to flatten it into a single quote block. However, the idea of having function blocks within quote blocks sounds like a good idea, I will ponder that.

For example, what if you were only interested in one particular statement that you got by executing a sequence of statements. Then it would be convenient to just look up the statement in the array, just as being able to look up a particular element of the Julia quote block. Julia quote blocks are stored using arrays, so why not store sequences of maxima/reduce statements using an array also.

Ah I see what you mean. I was going to walk the Julia Expr and look for sub-expressions that can be evaluated with a Maxima function. I think you're considering something like translating the whole Julia block expression to an equivalent Maxima expression?

Yes, a quote block is equivalent to a sequence of maxima expressions.

implemented quote block support for io.jl and mexpr.jl in this new branch: here

the other functions would have to be updated as well also

it should be possible to extend this to support function blocks as well, I will think about it some more...

Made progress on the quoteblock branch, now you can do this

julia> fun = Expr(:function,:(f(x)),:(y = x+1; y^2)) |> MExpr
 
                                                     2
                       f(x) := block([], y : x + 1, y )

julia> fun |> parse
:(function f(x)
        y = x + 1
        y ^ 2
    end)

Alright, one more commit. Now it's complete, now you can do what you wanted:

julia> f = Expr(:function,:(sin_and_cos(x)),:(sin(x)^2+cos(x)^2))
:(function sin_and_cos(x)
        sin(x) ^ 2 + cos(x) ^ 2
    end)

julia> trigsimp(f)
:(function sin_and_cos(x)
        1
    end)

This literally works now. I was skeptical at first if something like this could be done, but it's possible!

Ah awesome! That is amazing! I'm mid thesis writting right now so excuse my tardiness looking over the pull request. I'll have a look as soon as possible. Good work though!

You were right, some expression walking was required. However, it is the maxima expression that needs to be walked, so an existing package wouldn't have you covered (although it might lead to some good inspiration). You need to be able to parse the syntax of the maxima expressions in order to be able to walk them and perform substitutions on subexpressions within one statement or string. The ability to do that comes with the parse function that takes MExpr to Expr. The same expression walking logic is needed within the trigsimp and similar functions of simplify.jl, only with different return actions.

Good luck on the Thesis ~

Right so you translated the entire block expression into maxima and simplify the whole thing on mass in maxima? That pretty powerful. I'm exciting to take a look at this as soon as possible.

Yes, translate into maxima language, then walk the maxima expression to determine what substring or subexpression to apply the desired-action function to. Took me a few weeks to get there..

Amazing!

This is the part that defines trigsimp and similar single argument functions:

simfun = [:trigsimp, etc...]

:(export $(simfun...)) |> eval

ty = [:(Compat.String),:Expr]

for fun in simfun
  for T in ty
    quote
      function $fun(expr::$T)
        convert($T, $fun(MExpr(expr)))
      end
    end |> eval
  end
  quote
    function $fun(m::MExpr)
      nsr = Array{Compat.String,1}(0)
      sexpr = split(m).str
      for h in 1:length(sexpr)
        if contains(sexpr[h],":=")
          sp = split(sexpr[h],":=")
          push!(nsr,Compat.String(sp[1])*":="*string(sp[2]|>Compat.String|>MExpr|> $fun))
        elseif contains(sexpr[h],"block([],")
          rp = replace(sexpr[h],"block([],","") |> chop
          sp = split(rp,",")
          ns = "block([],"
          for u in 1:length(sp)
            ns = ns*string(sp[u] |> Compat.String |> MExpr |> $fun)
          end
          ns = ns*")"
          push!(nsr,ns)
        elseif contains(sexpr[h],":")
          sp = split(sexpr[h],":")
          push!(nsr,Compat.String(sp[1])*":"*string(sp[2]|>Compat.String|>MExpr|> $fun))
        else
          push!(nsr,$(string(fun))*"($(sexpr[h]))" |> MExpr |> mcall)
        end
      end
      return MExpr(nsr)
    end
  end |> eval
end

One of the key things is that it has to recursively call itself in order to walk subexpressions deeper.

That's why there is the call to $fun within the loop within the function defining $fun.

When it finally halts, that is the final else statement, where the actual mcall takes place.

The parsing in Reduce actually turned out much more difficult.

The logic required is much more convoluted, but the results are also satisfactory. In maxima, the block([],...) structure is easier to parse. In reduce, they are delimited by begin and end, which can also have ; statement delimiters between them. In order to walk the expression I had to precompute out to what program statement to fetch the expressions for and then recursively "loop-shift" my way forward until I reach the halting point. What insanity, haha

function parse(r::RExpr,be=0)
  pexpr = Array{Any,1}(0); sexpr = split(r).str
  iter = 1:length(sexpr)
  for h  iter
    for key in keys(reprjl)
      sexpr[h] = replace(sexpr[h],key,reprjl[key]); end; end
  state = start(iter); #show(sexpr)
  while !done(iter,state); (h,state) = next(iter, state)
    sh = split(sexpr[h],r"[ ]+")
    en = 1; isempty(replace(sh[en]," ","")) && (en = 2); #show(sh[en])
    if contains(sh[en],"procedure")
      js = join(split(sexpr[h],"procedure")[2:end],"procedure")
      (h,state) = next(iter, state)
      y = h; (h,state) = bematch(sexpr[h],sexpr,h,iter,state)
      push!(pexpr,Expr(:function,parse(js),rparse(sexpr[y:h],be)))
    elseif contains(sh[en],"begin")
      js = join(split(sexpr[h],"begin")[2:end],"begin")
      ep = Array{Any,1}(length(0)); sh1 = split(js,r"[ ]+")
      c = sum(sh1.=="begin")-sum(sh1.=="end"); flag = c  0
      c  -1 && (js = join(split(js,"end")[1:end+c],"end"))
      y = h; (h,state) = bematch(js,sexpr,h,iter,state)
      ep[1] = rparse(vcat(js,sexpr[y+1:h]...),be+1)
      ep[1] == nothing && shift!(ep)
      while !done(iter,state) & flag
        (h,state) = next(iter, state); cQ = c
        js = sexpr[h]; sh2 = split(js,r"[ ]+")
        c += sum(sh2.=="begin")-sum(sh2.=="end")
        c  -1 && (js = join(split(js,"end")[1:end+c],"end"))
        y = h; (h,state) = bematch(js,sexpr,h,iter,state)
        epr = rparse(vcat(js,sexpr[y+1:h]...),cQ)
        epr  nothing && push!(ep,epr); end
      push!(pexpr,Expr(:block,ep...))
    elseif contains(sh[en],"return")
      js = join(split(sexpr[h],"return")[2:end],"return")
      y = h; (h,state) = bematch(js,sexpr,h,iter,state)
      rp = rparse(vcat(js,sexpr[y+1:h]...),be)
      push!(pexpr,Expr(:return,rp))
    elseif contains(sh[en],"end"); nothing
    elseif isempty(sh[en]); nothing
    elseif contains(sexpr[h],":=")
      sp = split(sexpr[h],":=")
      push!(pexpr,Expr(:(=),parse(sp[1]),rparse(sp[2],be)))
    elseif contains(sexpr[h],":")
      sp = split(sexpr[h],":")
      push!(pexpr,Expr(:(:),rparse(sp[1],be),rparse(sp[2],be)))
    else
      js=sexpr[h]; se=sum(sh.=="end"); 0<sebe ? (js=replace(js,"end","")) :
        (se>be && (js=join(split(js,"end")[1:end-be],"end")))
      push!(pexpr,parse(js)); end; end; u = length(pexpr)
  return u==1 ? pexpr[1] : (u==0 ? nothing : Expr(:block,pexpr...)); end

rparse(r,be=0) = parse(r |> Compat.String |> RExpr, be)
rparse(r::Array{Compat.String,1},be=0) = parse(RExpr(r),be)

function bematch(js,sexpr,h,iter,state)
  sh = split(js,r"[ ]+"); y = h
  c = sum(sh.=="begin")-sum(sh.=="end"); flag = c > 0
  while !done(iter,state) & flag
    (y,state) = next(iter, state)
    sh2 = split(sexpr[y],r"[ ]+")
    c += sum(sh2.=="begin")-sum(sh2.=="end")
    flag = c > 0; end; return (y,state); end

example,

julia> R"procedure fun; begin; x; return begin; return x end; x; end" |> parse
:(function fun
        x
        return begin
                return x
            end
        x
    end)

julia> ans == parse(RExpr(ans))
true

Whew... this is absolutely insane.

Closed by #23