Fix script that translates Fortran to Julia

Running with Julia 1.6.1 the script

#=
This julia script converts fortran 90 code into julia.
It uses naive regex replacements to do as much as possible,
but the output WILL need further cleanup.
Known conversion problems such as GOTO are commented and marked with FIXME
Most variable declaration lines are entirely deleted, which may or
may not be useful. 
To run from a shell: 
julia fortran-julia.jl filename.f90
Output is written to filename.jl.
=#

using DataStructures

# Regex/substitution pairs for replace(). Order matters here.
replacements = OrderedDict(
  # Lowercase everything not commented
  r"^(?!.*!).*"m => lowercase,
  # Lowercase start of lines with comments
  r"^.*!"m => lowercase,
  # Remove '&' multiline continuations
  r"\s*&\s*" => "",
  # Comments use # not !
  "!" => "#",
  # Powers use ^ not **
  "**" => "^",
  # Only double quotes allowed for strings
  "'" => "\"",
  # DO loop to for loop
  r"do (.*),(.*)" => s"for \1:\2",
  # Spaces around math operators
  r"([\*\+\/=])(?=\S)" => s"\1 ",
  r"(?<=\S)([\*\+\/=])" => s" \1",
  # Spaces around - operators, except after e
  # r"([^e][\-])(\S)" => s"\1 \2",
  r"(?<!\W\de)(\h*\-\h*)" => s" - ",
  # Space after all commas
  r"(,)(\S)" => s"\1 \2",
  # Replace ELSEIF/ELSE IF with elseif 
  r"(\s+)else if" => s"\1elseif",
  # Replace IF followed by ( to if (
  r"(\s+)(elseif|if)\(" => s"\1\2 (",
  # Remove THEN
  r"([)\s])then(\s+)" => s"\1\2",
  # Relace END XXXX with end
  r"(\s+)end\h*.*" => s"\1end",
  # Replace expnent function
  r"(\W)exp\(" => s"\1exp(",
  # Reorganise functions and doc strings. This may be very project specific.
  r"#\^\^+\s*subroutine\s*(\w+)([^)]+\))\s*(.*?)#\^\^\^+"sm => 
      Base.SubstitutionString("\"\"\"\n\\3\"\"\"\nfunction \\1\\2::Void"),
  r"\#\^\^+\s*real function\s*(\w+)([^)]+\))\s*(.*?)\#\^\^\^+"sm => 
      Base.SubstitutionString("\"\"\"\n\\3\"\"\"\nfunction \\1\\2::Float64"),
  # Don't need CALL
  r"(\s*)call(\h+)" => s"\1",
  # Use real math symbols
  "gamma" => "Γ",
  "theta" => "Θ",
  "epsilon" => "ϵ",
  "lambda" => "λ",
  "alpha" => "α",
  # Swap logical symbols
  ".true." => "true",
  ".false." => "false",
  r"\s*\.or\.\s*" => " || ",
  r"\s*\.and\.\s*" => " && ",
  r"\s*\.not\.\s*" => " ! ",
  r"\s*\.eq\.\s*" => " == ",
  r"\s*\.ne\.\s*" => " != ",
  r"\s*\.le\.\s*" => " <= ",
  r"\s*\.ge\.\s*" => " >= ",
  r"\s*\.gt\.\s*" => " > ",
  r"\s*\.lt\.\s*" => " < ",
  # Remove (expression) brackets after if
  # r"if \((.*)\)(\s*\n)" => s"if \1\2",
  # Add end after single line if with an = assignment
  r"if\s*(.*?) = (.*?)(\n)" => s"if \1 = \2 end\3",
  # Format floats as "5.0" not "5."
  r"(\W\d+)\.(\D)" => s"\1.0\2",
  # Tab to 4 spaces
  r"\t" => "    ",
  # Relace suberror with error and mark for fixup
  r"(\W)suberror\((.*?),.*?\)" => s"\1 error(\2)",
  # Mark #FIXME the various things this script can't handle
  r"(write|goto|while\s)" => s"#FIXME \1",
)

# Patterns to remove
removal = [
  # Trailing whitespace
  r"\h*$"m,
  # Variable declarations
  r"\n\s*real\s.*",
  r"\n\s*real, external\s.*",
  r"\n\s*integer\s.*",
  r"\n\s*implicit none",
  r"\n\s*logical\s.*",
  # Import statements
  r"\n\s*use\s.*",
]

# Load the file from the first command line argument
filename = ARGS[1]
code = read(filename,String)

# Process replacements and removals.
for (f, r) in replacements
  global code = replace(code, f, r)
end
for r in removal
  global code = replace(code, r, "")
end
println(code)


# Write the output to a .jl file with the same filename stem.
stem = split(filename, ".")[1]
outfile = stem * ".jl"
write(outfile, code)

slightly modified from here
with julia fortran-julia.jl xhi.f90 where xhi.f90 is the Fortran program

program main
write (*,*) "hi"
end program main

I get an error

ERROR: LoadError: MethodError: no method matching replace(::String, ::Regex, ::typeof(lowercase))
Closest candidates are:
  replace(!Matched::Union{Function, Type}, ::Any; count) at set.jl:605
  replace(::String, !Matched::Pair{var"#s77", B} where {var"#s77"<:AbstractChar, B}; count) at strings/util.jl:513
  replace(::String, !Matched::Pair{var"#s74", B} where {var"#s74"<:Union{Tuple{Vararg{AbstractChar, N} where N}, Set{var"#s55"} where var"#s55"<:AbstractChar, AbstractVector{var"#s72"} where var"#s72"<:AbstractChar}, B}; count) at strings/util.jl:518
  ...
Stacktrace:
 [1] top-level scope
   @ c:\julia\fortran-julia.jl:108
in expression starting at c:\julia\fortran-julia.jl:107

Since modern Fortran Julia are somewhat similar languages, it would be nice if there were a working partial transpiler.

When converting pre-v1.0 Julia scripts to v1.0-compatible syntax, it’s helpful to run the script in v0.7, which will give you deprecation warnings:

julia> replace("asdf", r"", lowercase)
┌ Warning: `replace(s::AbstractString, pat, f)` is deprecated, use `replace(s, pat => f)` instead.
│   caller = top-level scope at none:0
└ @ Core none:0
"asdf"

…so in this case, you’ll change replace(code, f, r) to replace(code, f => r) and replace(code, r, "") to replace(code, r => "").

5 Likes

Thanks – with those changes the script works. For example, it transforms

Fortran code
module black_scholes_mod
implicit none
integer, parameter :: dp = kind(1.0d0)
private
public :: call_price,alnorm,cumnorm,print_call_price
contains
impure elemental subroutine print_call_price(s,k,r,t,vol)
! Black-Scholes price of a European call option
real(kind=dp), intent(in) :: s     ! stock price
real(kind=dp), intent(in) :: k     ! strike price
real(kind=dp), intent(in) :: r     ! annual interest rate -- 0.02 means 2%
real(kind=dp), intent(in) :: t     ! time to expiration in years
real(kind=dp), intent(in) :: vol   ! annualized volatility -- 0.30 means 30%
write (*,"(*(f8.4))") s,k,r,t,vol,call_price(s,k,r,t,vol)
end subroutine print_call_price

pure elemental function call_price(s,k,r,t,vol) result(price)
! Black-Scholes price of a European call option
real(kind=dp), intent(in) :: s     ! stock price
real(kind=dp), intent(in) :: k     ! strike price
real(kind=dp), intent(in) :: r     ! annual interest rate -- 0.02 means 2%
real(kind=dp), intent(in) :: t     ! time to expiration in years
real(kind=dp), intent(in) :: vol   ! annualized volatility -- 0.30 means 30%
real(kind=dp)             :: price ! call price
real(kind=dp)             :: d1,d2,vol_sqrt_t
vol_sqrt_t = vol*sqrt(t)
d1 = (log(s/k) + (r + 0.5_dp*vol**2)*t)/vol_sqrt_t
d2 = d1 - vol_sqrt_t
price = s*cumnorm(d1) - k*exp(-r*t)*cumnorm(d2) 
end function call_price
!
!  Algorithm AS66 Applied Statistics (1973) vol.22, no.3

!  Evaluates the tail area of the standardised normal curve
!  from x to infinity if upper is .true. or
!  from minus infinity to x if upper is .false.

! ELF90-compatible version by Alan Miller
! Latest revision - 29 November 2001
pure elemental function alnorm(x, upper) result(fn_val)
real(dp), intent(in)  ::  x
logical , intent(in)  ::  upper
real(dp)              ::  fn_val
!  local variables
real(dp), parameter   ::  zero=0.0_dp, one=1.0_dp, half=0.5_dp, con=1.28_dp
real(dp)              ::  z, y
logical               ::  up
!  machine dependent constants
real(dp), parameter  ::  ltone = 7.0_dp, utzero = 18.66_dp
real(dp), parameter  ::  p = 0.398942280444_dp, q = 0.39990348504_dp,   &
                         r = 0.398942280385_dp, a1 = 5.75885480458_dp,  &
                         a2 = 2.62433121679_dp, a3 = 5.92885724438_dp,  &
                         b1 = -29.8213557807_dp, b2 = 48.6959930692_dp, &
                         c1 = -3.8052e-8_dp, c2 = 3.98064794e-4_dp,     &
                         c3 = -0.151679116635_dp, c4 = 4.8385912808_dp, &
                         c5 = 0.742380924027_dp, c6 = 3.99019417011_dp, &
                         d1 = 1.00000615302_dp, d2 = 1.98615381364_dp,  &
                         d3 = 5.29330324926_dp, d4 = -15.1508972451_dp, &
                         d5 = 30.789933034_dp
up = upper
z = x
if (z < zero) then
   up = .not. up
   z = -z
end if
if (z <= ltone  .or.  (up  .and.  z <= utzero)) then
   y = half*z*z
   if (z > con) then
      fn_val = r*exp( -y )/(z+c1+d1/(z+c2+d2/(z+c3+d3/(z+c4+d4/(z+c5+d5/(z+c6))))))
   else
      fn_val = half - z*(p-q*y/(y+a1+b1/(y+a2+b2/(y+a3))))
   end if
else
   fn_val = zero
end if
if (.not. up) fn_val = one - fn_val
end function alnorm
!
pure elemental function cumnorm(x) result(fn_val)
! adapted from alnorm with upper = .false.
real(dp), intent(in) ::  x
real(dp)             ::  fn_val
!  local variables
real(dp), parameter  ::  zero=0.0_dp, one=1.0_dp, half=0.5_dp, con=1.28_dp
real(dp)             ::  z, y
logical              ::  up
!  machine dependent constants
real(dp), parameter  ::  ltone = 7.0_dp, utzero = 18.66_dp
real(dp), parameter  ::  p = 0.398942280444_dp, q = 0.39990348504_dp,   &
                         r = 0.398942280385_dp, a1 = 5.75885480458_dp,  &
                         a2 = 2.62433121679_dp, a3 = 5.92885724438_dp,  &
                         b1 = -29.8213557807_dp, b2 = 48.6959930692_dp, &
                         c1 = -3.8052e-8_dp, c2 = 3.98064794e-4_dp,     &
                         c3 = -0.151679116635_dp, c4 = 4.8385912808_dp, &
                         c5 = 0.742380924027_dp, c6 = 3.99019417011_dp, &
                         d1 = 1.00000615302_dp, d2 = 1.98615381364_dp,  &
                         d3 = 5.29330324926_dp, d4 = -15.1508972451_dp, &
                         d5 = 30.789933034_dp
z = x
if (z < zero) then
   up = .true.
   z = -z
else
   up = .false.
end if
if (z <= ltone  .or.  (up  .and.  z <= utzero)) then
   y = half*z*z
   if (z > con) then
      fn_val = r*exp( -y )/(z+c1+d1/(z+c2+d2/(z+c3+d3/(z+c4+d4/(z+c5+d5/(z+c6))))))
   else
      fn_val = half - z*(p-q*y/(y+a1+b1/(y+a2+b2/(y+a3))))
   end if
else
   fn_val = zero
end if
if (.not. up) fn_val = one - fn_val
end function cumnorm
!
end module black_scholes_mod

to

pseudo-Julia code
module black_scholes_mod
integer, parameter :: dp = kind(1.0d0)
private
public :: call_price, alnorm, cumnorm, print_call_price
contains
impure elemental subroutine print_call_price(s, k, r, t, vol)
# Black - Scholes price of a European option
real(kind = dp), intent(in) :: s     # stock price
real(kind = dp), intent(in) :: k     # strike price
real(kind = dp), intent(in) :: r     # annual interest rate -  - 0.02 means 2%
real(kind = dp), intent(in) :: t     # time to expiration in years
real(kind = dp), intent(in) :: vol   # annualized volatility -  - 0.30 means 30%
#FIXME write ( * , "( * (f8.4))") s, k, r, t, vol, call_price(s, k, r, t, vol)
end

pure elemental function call_price(s, k, r, t, vol) result(price)
# Black - Scholes price of a European option
real(kind = dp), intent(in) :: s     # stock price
real(kind = dp), intent(in) :: k     # strike price
real(kind = dp), intent(in) :: r     # annual interest rate -  - 0.02 means 2%
real(kind = dp), intent(in) :: t     # time to expiration in years
real(kind = dp), intent(in) :: vol   # annualized volatility -  - 0.30 means 30%
real(kind = dp)             :: price # price
real(kind = dp)             :: d1, d2, vol_sqrt_t
vol_sqrt_t = vol * sqrt(t)
d1 = (log(s / k) + (r + 0.5_dp * vol^2) * t) / vol_sqrt_t
d2 = d1 - vol_sqrt_t
price = s * cumnorm(d1) - k * exp( - r * t) * cumnorm(d2) 
end
#
#  Algorithm AS66 Applied Statistics (1973) vol.22, no.3

#  Evaluates the tail area of the standardised normal curve
#  from x to infinity if upper is true or
#  from minus infinity to x if upper is false

# ELF90 - compatible version by Alan Miller
# Latest revision - 29 November 2001
pure elemental function alnorm(x, upper) result(fn_val)
real(dp), intent(in)  ::  x
real(dp)              ::  fn_val
#  local variables
real(dp), parameter   ::  zero = 0.0_dp, one = 1.0_dp, half = 0.5_dp, con = 1.28_dp
real(dp)              ::  z, y
#  machine dependent constants
real(dp), parameter  ::  ltone = 7.0_dp, utzero = 18.66_dp
real(dp), parameter  ::  p = 0.398942280444_dp, q = 0.39990348504_dp, r = 0.398942280385_dp, a1 = 5.75885480458_dp, a2 = 2.62433121679_dp, a3 = 5.92885724438_dp, b1 = - 29.8213557807_dp, b2 = 48.6959930692_dp, c1 = - 3.8052e - 8_dp, c2 = 3.98064794e - 4_dp, c3 = - 0.151679116635_dp, c4 = 4.8385912808_dp, c5 = 0.742380924027_dp, c6 = 3.99019417011_dp, d1 = 1.00000615302_dp, d2 = 1.98615381364_dp, d3 = 5.29330324926_dp, d4 = - 15.1508972451_dp, d5 = 30.789933034_dp
up = upper
z = x
if (z < zero) 
   up = ! up
   z = - z
end
if (z < = ltone || (up && z < = utzero))  end
   y = half * z * z
   if (z > con) 
      fn_val = r * exp( - y ) / (z + c1 + d1 / (z + c2 + d2 / (z + c3 + d3 / (z + c4 + d4 / (z + c5 + d5 / (z + c6))))))
   else
      fn_val = half - z * (p - q * y / (y + a1 + b1 / (y + a2 + b2 / (y + a3))))
   end
else
   fn_val = zero
end
if ( ! up) fn_val = one - fn_val end
end
#
pure elemental function cumnorm(x) result(fn_val)
# adapted from alnorm with upper = false
real(dp), intent(in) ::  x
real(dp)             ::  fn_val
#  local variables
real(dp), parameter  ::  zero = 0.0_dp, one = 1.0_dp, half = 0.5_dp, con = 1.28_dp
real(dp)             ::  z, y
#  machine dependent constants
real(dp), parameter  ::  ltone = 7.0_dp, utzero = 18.66_dp
real(dp), parameter  ::  p = 0.398942280444_dp, q = 0.39990348504_dp, r = 0.398942280385_dp, a1 = 5.75885480458_dp, a2 = 2.62433121679_dp, a3 = 5.92885724438_dp, b1 = - 29.8213557807_dp, b2 = 48.6959930692_dp, c1 = - 3.8052e - 8_dp, c2 = 3.98064794e - 4_dp, c3 = - 0.151679116635_dp, c4 = 4.8385912808_dp, c5 = 0.742380924027_dp, c6 = 3.99019417011_dp, d1 = 1.00000615302_dp, d2 = 1.98615381364_dp, d3 = 5.29330324926_dp, d4 = - 15.1508972451_dp, d5 = 30.789933034_dp
z = x
if (z < zero) 
   up = true
   z = - z
else
   up = false
end
if (z < = ltone || (up && z < = utzero))  end
   y = half * z * z
   if (z > con) 
      fn_val = r * exp( - y ) / (z + c1 + d1 / (z + c2 + d2 / (z + c3 + d3 / (z + c4 + d4 / (z + c5 + d5 / (z + c6))))))
   else
      fn_val = half - z * (p - q * y / (y + a1 + b1 / (y + a2 + b2 / (y + a3))))
   end
else
   fn_val = zero
end
if ( ! up) fn_val = one - fn_val end
end
#
end

that requires further editing, especially the declarations. Here is the

Julia translation script
#=
This julia script converts fortran 90 code into julia.
It uses naive regex replacements to do as much as possible,
but the output WILL need further cleanup.
Known conversion problems such as GOTO are commented and marked with FIXME
Most variable declaration lines are entirely deleted, which may or
may not be useful. 
To run from a shell: 
julia fortran-julia.jl filename.f90
Output is written to filename.jl.
=#

using DataStructures

# Regex/substitution pairs for replace(). Order matters here.
replacements = OrderedDict(
  # Lowercase everything not commented
  r"^(?!.*!).*"m => lowercase,
  # Lowercase start of lines with comments
  r"^.*!"m => lowercase,
  # Remove '&' multiline continuations
  r"\s*&\s*" => "",
  # Comments use # not !
  "!" => "#",
  # Powers use ^ not **
  "**" => "^",
  # Only double quotes allowed for strings
  "'" => "\"",
  # DO loop to for loop
  r"do (.*),(.*)" => s"for \1:\2",
  # Spaces around math operators
  r"([\*\+\/=])(?=\S)" => s"\1 ",
  r"(?<=\S)([\*\+\/=])" => s" \1",
  # Spaces around - operators, except after e
  # r"([^e][\-])(\S)" => s"\1 \2",
  r"(?<!\W\de)(\h*\-\h*)" => s" - ",
  # Space after all commas
  r"(,)(\S)" => s"\1 \2",
  # Replace ELSEIF/ELSE IF with elseif 
  r"(\s+)else if" => s"\1elseif",
  # Replace IF followed by ( to if (
  r"(\s+)(elseif|if)\(" => s"\1\2 (",
  # Remove THEN
  r"([)\s])then(\s+)" => s"\1\2",
  # Relace END XXXX with end
  r"(\s+)end\h*.*" => s"\1end",
  # Replace expnent function
  r"(\W)exp\(" => s"\1exp(",
  # Reorganise functions and doc strings. This may be very project specific.
  r"#\^\^+\s*subroutine\s*(\w+)([^)]+\))\s*(.*?)#\^\^\^+"sm => 
      Base.SubstitutionString("\"\"\"\n\\3\"\"\"\nfunction \\1\\2::Void"),
  r"\#\^\^+\s*real function\s*(\w+)([^)]+\))\s*(.*?)\#\^\^\^+"sm => 
      Base.SubstitutionString("\"\"\"\n\\3\"\"\"\nfunction \\1\\2::Float64"),
  # Don't need CALL
  r"(\s*)call(\h+)" => s"\1",
  # Use real math symbols
  "gamma" => "Γ",
  "theta" => "Θ",
  "epsilon" => "ϵ",
  "lambda" => "λ",
  "alpha" => "α",
  # Swap logical symbols
  ".true." => "true",
  ".false." => "false",
  r"\s*\.or\.\s*" => " || ",
  r"\s*\.and\.\s*" => " && ",
  r"\s*\.not\.\s*" => " ! ",
  r"\s*\.eq\.\s*" => " == ",
  r"\s*\.ne\.\s*" => " != ",
  r"\s*\.le\.\s*" => " <= ",
  r"\s*\.ge\.\s*" => " >= ",
  r"\s*\.gt\.\s*" => " > ",
  r"\s*\.lt\.\s*" => " < ",
  # Remove (expression) brackets after if
  # r"if \((.*)\)(\s*\n)" => s"if \1\2",
  # Add end after single line if with an = assignment
  r"if\s*(.*?) = (.*?)(\n)" => s"if \1 = \2 end\3",
  # Format floats as "5.0" not "5."
  r"(\W\d+)\.(\D)" => s"\1.0\2",
  # Tab to 4 spaces
  r"\t" => "    ",
  # Relace suberror with error and mark for fixup
  r"(\W)suberror\((.*?),.*?\)" => s"\1 error(\2)",
  # Mark #FIXME the various things this script can't handle
  r"(write|goto|while\s)" => s"#FIXME \1",
)

# Patterns to remove
removal = [
  # Trailing whitespace
  r"\h*$"m,
  # Variable declarations
  r"\n\s*real\s.*",
  r"\n\s*real, external\s.*",
  r"\n\s*integer\s.*",
  r"\n\s*implicit none",
  r"\n\s*logical\s.*",
  # Import statements
  r"\n\s*use\s.*",
]

# Load the file from the first command line argument
filename = ARGS[1]
code = read(filename,String)

# Process replacements and removals.
for (f, r) in replacements
  global code = replace(code, f => r)
end
for r in removal
  global code = replace(code, r => "")
end
println(code)


# Write the output to a .jl file with the same filename stem.
stem = split(filename, ".")[1]
outfile = stem * ".jl"
write(outfile, code)
1 Like

I think the if statements are wrong in the pseudo-code.

See also the online version of their F77 → f90 converter.

Know other tools to restructure dusty GOTO into modern code?