Almost Fortran

Arjen Markus (9 november 2007) It may seem weird or even perverse to design a Fortran-like scripting language, but people for whom programming is not their daily job, who write small programs in Fortran once in a while and who rely on MATLAB or Excel or ... for other computational tasks, Tcl's syntax may present a significant hurdle.

So, when I was confronted with a problem regarding a (sort of) scripting facility in a Fortran program, I played with the idea of introducing Tcl in disguise.

The program below demonstrates this idea.

AM (13 november 2007) Added support for one-dimensional arrays. Not complete yet, I want a function like size(array) to work too. And probably something like sum(array), but let us not get too ambitious :). I am not going to cover the complete Fortran 90/95/2003 standard, just a small, useful language that is syntactically close enough to Fortran.

AM (15 november 2007) I realised that the L programming language is aiming for a very similar goal, though the approach is very different.

 # interpfort.tcl --
 #     First attempt to create a kind of Fortran interpreter
 #     Note:
 #     To keep things manageable, it is only a Fortran lookalike
 #     language that this interpreter can handle. But it should
 #     sufficient for scripting purposes: people used to Fortran
 #     can then use a language that looks pretty much like
 #     Fortran.
 #
 #     Limitations:
 #     - You can only define functions right now
 #     - You can not _call_ a function right now
 #     - Support for one-dimensional arrays only
 #     - Arrays will have a different syntax than in Fortran:
 #       array[i,j] instead of array(i,j).
 #     - No exponentiation operator (**)
 #     - No string manipulation, reading/writing files
 #     - Very limited error handling
 #     - Keywords are reserved!
 #

 namespace eval ::InterpFort {

     variable funcname

 }

 # convertFort --
 #     Convert the Fortran like code to valid Tcl
 #
 # Arguments:
 #     code        Code in question
 #
 # Result:
 #     None
 #
 # Side effects:
 #     The functions defined in the code are turned into regular
 #     Tcl procedures
 #
 proc ::InterpFort::convertFort {code} {
     variable funcname

     set newcode {}
     foreach line [split $code \n] {
         set comment [string first "!" $line]
         if { $comment >= 0 } {
             set line [string range $line 0 [expr {$comment-1}]]
         }

         set line [string tolower $line]
         switch -re -- $line {
             {^ *function *} {
                 set line [TreatFunction $line]
             }
             {^ *if *\(} {
                 set line [TreatIf $line]
             }
             {^ *do *} {
                 set line [TreatDo $line]
             }
             {^ *end *do} {
                 set line "\}"
             }
             {^ *end *if} {
                 set line "\}"
             }
             {^ *return} {
                 set line "return \$$funcname"
             }
             {^ *exit} {
                 set line "break"
             }
             {^ *else} {
                 set line "\} else \{"
             }
             {^ *cycle} {
                 set line "continue"
             }
             {^ *end *function} {
                 set line "return \$$funcname\n\}"
             }
             {[a-zA-Z0-9_] *= *[^=]} {
                 set line [TreatAssign $line]
             }
         }
         append newcode "$line\n"
      }

      puts "Code:\n$newcode"

      if { [catch {
               uplevel 1 $newcode
           } msg] } {
          puts "Error during conversion: $msg"
      }
      return
 }


 # TreatFunction --
 #     Convert the function header line to valid Tcl
 #
 # Arguments:
 #     line        Line containing the function keyword
 #
 # Result:
 #     Valid equivalent Tcl code
 #
 proc ::InterpFort::TreatFunction {line} {
     variable funcname

     if { [regexp {function +([a-zA-Z0-9_]+) *\((.+)\)} $line => name arguments] } {

         set funcname $name
         set line "proc $name \{[string map {, " "} $arguments]\} \{"
     } else {
         error "Error converting Fortran function header to Tcl: $line"
     }
     return $line
 }


 # TreatDo --
 #     Convert the do-loop line to valid Tcl
 #
 # Arguments:
 #     line        Line containing the do keyword
 #
 # Result:
 #     Valid equivalent Tcl code
 #
 proc ::InterpFort::TreatDo {line} {

     if { [regexp {do +([a-zA-Z0-9_]+) *= *([^,]+),([^,]+),?(.*)} \
              $line => var begin end step] } {
         if { $step == "" } {
             set step 1
         }
         set line "for {set $var \[expr {$begin}\]} {\$$var <= $end} \
                   {incr $var \[expr {$step}\]} \{"
     } else {
         error "Error converting Fortran do-loop to Tcl: $line"
     }
     return $line
 }


 # TreatIf --
 #     Convert an if statement to valid Tcl
 #
 # Arguments:
 #     line        Line containing an if statement
 #
 # Result:
 #     Valid equivalent Tcl code
 #
 proc ::InterpFort::TreatIf {line} {

     regsub {if *\(} $line "" line
     regsub {\) *then} $line "" line

     set line [TreatExpression $line]

     set line "if \{ $line \} \{"

     return $line
 }


 # TreatAssign --
 #     Convert an assignment to valid Tcl
 #
 # Arguments:
 #     line        Line containing an assignment
 #
 # Result:
 #     Valid equivalent Tcl code
 #
 proc ::InterpFort::TreatAssign {line} {

     set part [string first = $line]

     set var        [string trimleft [string range $line 0 [expr {$part-1}]]]
     set expression [string trimleft [string range $line [expr {$part+1}] end]]

     #
     # No function calls or arrays yet ...
     #
     set expression [TreatExpression $expression]

     set line "set $var \[expr \{$expression\}\]"

     return $line
 }


 # TreatExpression --
 #     Convert an expression to valid Tcl
 #
 # Arguments:
 #     line        Line containing an expression
 #
 # Result:
 #     Valid equivalent Tcl code
 #
 proc ::InterpFort::TreatExpression {line} {

     #
     # Insert the $ for each substring that _might_ be a variable name
     #
     regsub -all {([a-zA-Z_][a-zA-Z0-9_]*)} $line {$\1} line

     #
     # Correct the cases where it is not
     #
     regsub -all {([0-9.])\$e} $line {\1e} line
     regsub -all {\$([a-zA-Z_][a-zA-Z0-9_]*)\(} $line {\1(} line

     return $line
 }

 # main --
 #     Test and sample usage
 #
 ::InterpFort::convertFort {
     function cowboy(x,y)
         r2 = x*x + y*y
         cowboy = (1.0 - r2)*(1.0 - r2)
     end function

     !
     ! This is a slightly more complicated function:
     ! It determines the root of:
     !
     ! exp(y)+y = x
     !
     ! using a Picard iteration.
     !
     ! Note: 
     ! This does not work for x>0 - no conversion.
     ! I should instead use Newton-Raphson
     !
     function invexp(x)
         y = x
         if ( x > 0.0 ) then
             y = 0.5 * x
         endif
         yold = -1.0e20
         do i = 1,20
             if ( abs(yold-y) < 1.0e-6 ) then
                 exit
             endif

             yold = y
             if ( y <= 0 ) then
                 y = x-exp(y)
             else
                 y = log(x-y)
             endif
             #puts $y  ! Yes, you can use Tcl commands ;)
         enddo
         invexp = y
     end function
 }

 for {set i 0} {$i < 20 } {incr i} {
    set x [expr {$i*0.1}]
    set y [expr {$i*0.2-0.5}]
    puts "Cowboy: [cowboy $x $y]"
 }
 for {set i 0} {$i < 20 } {incr i} {
    set x [expr {-$i*0.1}]
    puts "$x [invexp $x]"
 }