Maxima through a pipe

See also Calling Maxima from Tcl which concerns the Tk version of Maxima


Joachim Kock: I have been using Maxima from Alpha, communicating with (command line) Maxima through a pipe. One advantage of running Maxima from an editor is that you can get a worksheet like interface with all the editing power of your editor (and let me mention, in case you didn't know, that Alpha has Tcl as extension language, so that makes it a heck of a powerful editor). But the setup of the pipe and the commands to call Maxima are pure Tcl and make sense also outside Alpha, and in fact it can serve as a "math package" to be used programmatically in Tcl scripts.

The idea is simple: set up a pipe, send commands, receive the results, and strip away prompts and such. If you want to use this from within a script you want synchronous operations, whereas pipes provide assynchronous operation (correct me if I got these fancy words wrong). To fake synchronous operation the proc simply vwaits for a global variable to be written, and this happens when the eventhandler determines that the output from Maxima is complete. Sort of backwards... There is also a batch interface, but then you cannot keep Maxima's state between your commands...

Here is the code:

  # This is a batch-like interface to MAXIMA. The command [maxima] (which 
  # is just synonym for [maxima::maxima]) will send all its arguments to
  # evaluation by MAXIMA and return the result.  When called isolated, 
  # MAXIMA will be spawned and then quit immediately after returning the 
  # result.  Alternatively, a MAXIMA session can be started with the 
  # command [maxima::start].  In this case subsequent calls to [maxima] 
  # will be evaluated in this session (until the session is killed with 
  # [maxima::stop]).  The advantages of this approach is that it is more
  # economical to call an existing process than to spawn a new one for 
  # each call, and second, that the state of MAXIMA is preserved, so that 
  # succesive calls can refer to MAXIMA variables (either user-defined 
  # variables or labels like D5).  (There is also a variant proc [maximaTex]
  # which returns the result in TeX, which is useful in live math TeX 
  # documents (worksheets).)
  #
  # Examples:
  # 
  # % source maxima.tcl
  # % maxima factor( 23423412342131234 )
  # 2*131*173491*515313977
  # % set f [maxima expand( (x+y+z)^4 )]
  # z^4+4*y*z^3+4*x*z^3+6*y^2*z^2+12*x*y*z^2+6*x^2*z^2+4*y^3*z+12*x*y^2*z
  #         +12*x^2*y*z+4*x^3*z+y^4+4*x*y^3+6*x^2*y^2+4*x^3*y+x^4
  # % maxima factor( $f )
  # (z+y+x)^4




  namespace eval maxima {}

  # The array data contains:
  #     transcript -- a log of everything done (like a shell)
  #     res -- last result
  #     label -- label of the last result (e.g. (D5))
  #     size -- the offset (in the transcript) of the previous feedback,
  #             including the prompt (e.g. just after (C5))

  proc maxima::start { } {
      # Close any old pipe:
      stop
      # Initialise the array:
      variable data
      set data(size) 0
      # Open the pipe:
      set data(pipe) [open "|maxima" RDWR]
      fconfigure $data(pipe) -buffering line -blocking 0
      # Set up a handler for the output:
      fileevent $data(pipe) readable ::maxima::receive
      # Wait until the receiver has finished with the MAXIMA welcome header:
      vwait ::maxima::data(res)
      # Then send a configuration instruction:
      maximaRunningBatch {DISPLAY2D:FALSE$}
      return
  }

  proc maxima::stop { } {
      variable data
      # Close the pipe:
      catch { close $data(pipe) }
      # Reset the data array:
      unset -nocomplain data
  }

  proc maxima::isRunning { } {
      variable data
      if { [info exists data(pipe)] } {
          return [expr { [lsearch -exact [file channels] $data(pipe)] != -1 }]
      } else { 
          return 0 
      }
  }

      
      
  # Event handler for data(pipe).  Writes the result to the variable 
  # data(res), from where the proc [maxima] reads it immediately.
  proc maxima::receive { } {
      variable data
      if { [eof $data(pipe)] } {
          # There is nothing more to read --- just stop:
          stop
          return
      }
      # Just read as much as possible, and append it:
      append data(transcript) [read $data(pipe)]
      # When we read a promt, stop reading:
      if { [regexp -start $data(size) -- {\(C\d+\)\s*$} $data(transcript)] } {
          # We have found a new prompt.  Hence the result should be just before that:
          if { [regexp -start $data(size) -- {.*\((D\d+)\)(.*)\(C\d+\)\s*$} $data(transcript) "" data(label) tmpRes] } {
              set data(res) [string trim $tmpRes]
          } else {
              # We should only come in here at startup, then we need to set data(res)
              # since the caller is waiting for this variable to be set:
              set data(res) ""
          }
          set data(size) [string length $data(transcript)]
      }
  }

  # Requires trailing semicolon
  proc maxima::maximaRunningBatch { cmd } {
      variable data
      # First update the transcript:
      append data(transcript) $cmd
      # Set up a handler for the output:
      fileevent $data(pipe) readable ::maxima::receive

      # Then send the command:
      puts $data(pipe) $cmd
      # Timeout mechanisms: We are going to wait for the variable res.  
      # Make sure it is written at least ofter some time:
      set timeout [after 10000 {set ::maxima::data(res) "TIMEOUT"}]
      vwait maxima::data(res)
      # If we have come so far there is no more need for the time bomb:
      after cancel $timeout
      if { [string equal $data(res) "TIMEOUT"] } {
          stop
          error "TIMEOUT"
      }
      return $data(res)
  }


  # This proc doesn't use pipes or file events or anything.
  # 
  # Requires trailing semicolon
  proc maxima::maximaBatch { cmd } {
      set preCmd {DISPLAY2D:FALSE$ }
      set transcript [exec maxima --batch-string=${preCmd}${cmd}]
      if { [regexp -- {.*\n\(D\d+\) (.*)\nBye.$} $transcript "" res] } {
          return $res
      } else {
          regexp -- {.*\n\(C\d+\)[^\n]*\n(.*)\nBye.$} $transcript "" transcript
          error $transcript
      }
  }

  proc maxima::maxima { args } {
      variable data
      # Build the command:
      set cmd [join $args " "]
      set cmd [string trim $cmd]
      if { ![string equal [string index $cmd end] {;}] } {
          append cmd {;}
      }
      # See if we have maxima running already:
      if { [isRunning] } {
          return [maximaRunningBatch $cmd]
      } else {
          return [maximaBatch $cmd]
      }
  }


  namespace eval maxima {
      namespace export maxima maximaTex
  }

  namespace import -force maxima::maxima maxima::maximaTex



  # ====================================================================
  # Here comes the part with tex formated output


  # This proc doesn't use pipes or file events or anything.
  # 
  # Requires trailing semicolon
  proc maxima::maximaTexBatch { cmd } {
      set preCmd {DISPLAY2D:FALSE$ }
      set transcript [exec maxima --batch-string=${preCmd}${cmd}tex(%)\;]
      if { [regexp -- {.*\n\(D\d+\).*\$(\$.*\$)\$\n\(D\d+\).*\nBye.$} $transcript "" res] } {
          return $res
      } else {
          if { [regexp -- {.*DISPLAY2D : FALSE\n(.*)\n\(C\d+\)} $transcript "" err] } {
              error $err
          }
          error $transcript
      }
  }


  proc maxima::receiveTex { } {
      variable data
      if { [eof $data(pipe)] } {
          # There is nothing more to read --- just stop:
          stop
          return
      }
      # Just read as much as possible, and append it:
      append data(transcript) [read $data(pipe)]
      # When we read a promt, stop reading:
      if { [regexp -start $data(size) -- {\(C\d+\)\s*$} $data(transcript)] } {
          # We have found a new prompt.  Hence the result should be just before that:
          if { [regexp -start $data(size) -- {.*\$(\$.*\$)\$\n\(D\d+\) FALSE\n\(C\d+\)\s*$} $data(transcript) "" tmpRes] } {
              set data(res) [string trim $tmpRes]
          } else {
              # We should only come in here at startup, then we need to set data(res)
              # since the caller is waiting for this variable to be set:
              set data(res) ""
          }
          set data(size) [string length $data(transcript)]
      }
  }



  # Requires trailing semicolon
  proc maxima::maximaTexRunningBatch { cmd } {
      variable data
      # First we just send the command to usual evaluation:
      maximaRunningBatch $cmd
      
      # Now send the tex formating command

      # Set up a handler for the output:
      fileevent $data(pipe) readable ::maxima::receiveTex
      # Send the tex formating command
      puts $data(pipe) tex(%)\;

      # Timeout mechanisms: We are going to wait for the variable res.  
      # Make sure it is written at least ofter some time:
      set timeout [after 10000 {set ::maxima::data(res) "TIMEOUT"}]
      vwait maxima::data(res)
      # If we have come so far there is no more need for the time bomb:
      after cancel $timeout
      if { [string equal $data(res) "TIMEOUT"] } {
          stop
          error "TIMEOUT"
      }
      return $data(res)
  }

  proc maxima::maximaTex { args } {
      variable data
      # Build the command:
      set cmd [join $args " "]
      set cmd [string trim $cmd]
      if { ![string equal [string index $cmd end] {;}] } {
          append cmd {;}
      }
      # See if we have maxima running already:
      if { [isRunning] } {
          return [maximaTexRunningBatch $cmd]
      } else {
          return [maximaTexBatch $cmd]
      }
  }

VK: I can't make it work for me, could you please help:

 % source maxima.tcl
 % maxima
 can't read "err": no such variable
 % maxima X^4;
 can't read "err": no such variable
 % maxima::start
 can't read "data(pipe)": no such element in array
 %

<Joachim Kock>: I have fixed the problem reported, namely that the script tries to access a variable that has not been assigned. However, the fact that you come into this case seems to indicate that you have an error in any case. But now at least you should be able to see what the error is... The 'cant read data(pipe)' error I cannot explain. (If you really want help, you should provide some miminal info: operating system, version, Tcl version, maxima version, etc.)