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]" }