Language Reference

This section describes in details the Fython language.

In code examples

  • shell code is indicated with a dollar $
  • Python code is indicated with triple quote >>>
  • Fortran code is indicated with triple bangs !!!

When no mention of the language is made, assume it is Fython.

The term shared library is often simply referenced by so.

Syntax

In Fython a statement is formed by a keyword, modifiers and target

keyword modifier* target

The keyword is the action performed by the statement. The modifiers mutate the default behavior of the actions, The action dictated by the keyword and modifiers is then applied to all the target.

real pointer x

real pointer: x y z
real pointer: x, y, z

real pointer:
  x
  y
  x

real pointer:
  x, y, z
  a, b, c

The comma is necessary for target that are non atomic

real cons: x=1, y=2, z=3

The only exception to the statement construction are in-place assignment operation

real: x y

x = y + 1
x += 10

Modifiers are also called attributes.

Any modifier that is allowed in Fortran can also be used in Fython

real pointer x
int allocatable contiguous y

Arrays are defined by indicating their dimensions

real:
  x(10)
  y(1:10, 0:5)

Array elements are accessed with the slice notation

x[1:6] => y[2, :]

You can initialize an array or use an array in place with the bracket notation

real x(3) = [1, 2, 3]

f([1, 2, 3])

Imports

Three kinds of imports are possible in Fython. Aliased namespace import, star import and slice import.

import pca
import stat.mean = m

import stat.variance(*)
import stat.newspaper(x, y=z)

When the module url is composed of only one name, such as the pca imports. The statement is equivalent to

import pca = pca

With an aliased namespace import, the object in the module are access with a dot .

import stat.mean = m
m.cube_mean(1, 2, 3)

With a star import all the object of the imported module are avalaible

import stat.mean(*)
cube_mean(1, 2, 3)

With a slice import only the stated names are imported, optionally aliased

import stat.mean(cube_mean, char_mean= cm)
cube_mean(1, 2, 3)
char_mean('abc', 'def', 'ijk')

For all imports it is necessary that you have write permission to the directoty that contains the imported module. This is because Fython needs to maintain build products in the same directory. The only exception to this rule are shared library import, as no build product needs to be maintained in these case.

The url of a module is its qualified Python name

import numpy.random.uniform

This implies that for a file to be importable, it must be on your Python path.

The first way to put a file on your Python is to create a host Python package and registering it to Python with a setup script

$ python3 setup.py develop

See the Getting Started section for the details.

The other method is to modify your path directly

#>>>
import sys
import fython
sys.path.append('/opt/nag')
m = fython.load('random.sobolev')

In a Python url, the file extension cannot be part of the url. You should then take into account the following resolution order. In a compile time import, Fython first search for

  • a Fython file (.fy, __init__.fy)
  • a Fortran file (.f90, .f03, ... and many other)
  • a So File (.so)

In a pycessor time import, Fython search for

  • a Python file (.py, __init__.py).

For print and read statement that uses an url, the assumed extension is .out.

For Fortran, only star and slice imports are allowed. For So only star imports are allowed.

Usefull modifiers of the import statements are

import asis payoff_defs_provider(*)

import noforce mkl.include.mkl_vsl(*)

The asis modifier prevents any package interpolation to happen during the import. This is usefull when designing a packaged meant to be a template. See the Template section.

The noforce modifier prevents a forcefull recompilation. If the module alreasy exists, it is not recompile, even if it was loaded with load(url, force=1. This is usefull to avoid recompilation of heavy module, that anyway never changes.

Declaration

The declarations order follows Fortran convention. Variables, Classes and interface declaration shoud appear first within a scoping unit, then functions.

A scoping unit is the entire module, the body of a class, or the body of a function.

For this release, nested class or nested function are not supported.

Operator

Fython has the augmented assignment operators, the logical operator, the bitwise operators, and the pointer operator.

x += 1
x -= 1
x *= 2
x /= 2

x <<= 1
x &= 1
x ^= 1
x |= 1
x >>= 1


x < <= == != => > y # this is an invalid syntax

x and y or b not c

x >> 1 + y << 4

x => y # pointer

The min and max operator are often convenient.

x ++= y # x = max(x, y)
x --= y # x = min(x, y)

Variable Declaration

In Fython the elementary types have a Python flavor

real x
int y
char z
bool a
complex b

Constant are declared with the attribute cons.

real cons x

Classes are instantiated by using there name

class A:
  pass

A a

String variable can be assign a value with a multiline string

char(100) x

x = """
  extra leading space
  at the beggining remove
"""

x = '''
  triple quote
'''

x = 'single line'
x = "double quote"

Any newline or tab character in the string will be honored.

For procedure argument, use the proc modifier

Coarray

A coarray is defined by specifying its codimension in bracket

real x[*]
real y(10)[*]

A coarray is accessed with the slice notation

x = 1 # this_image() x
x[2] = 2 # x on image 2

y[1] = 1 # this_image() y
y[:][2] = 1 # y on image 2

To use coarray in Fython, you need to set the compiler to use with the set_compiler function

#>>>
from fython import *

set_compiler(
  cmd = 'ifort',
  prefix = '',
  infix = '_mp_',
  suffix = '_',
  debug = '-coarray -coarray-num-images=5',
  release = '-coarray -coarray-num-images=5',
  error_regex = 'error:',
)

m = load('.coarray_test')

Function

A function is declared with the keyword def

def f:
  real in x
  real res r

For a variable to be recognized as an argument, it must have one of the intent modifier

in
out
inout

The return value of a function must be indicated with the modifier

res

When no argument has the modifier res, the function has no return value.

You can separate the implementation and the specification of you function with spec interpolation

def pure f:
  real in x
  real res r


def f:
  r = x + 3

The spec for f can be in the same module or originate from an import. You can also explicitlye specify the spec to use with the spec.

def elemental f:
  real in x
  real res r

def spec(f) g:
  r = x + 10

  def spec(f, g) h:
    pass # multiple spec parent

You can use the inline instruction to include verbatim the definition of one function into another

def f:
  x += 1

def g:
  inline f

The modifier debug or release can be use to specify in which mode to include the code. This is usefull for conditional inclusion of logging code for example.

inline debug f
inline release f

When no modifier is specified, the code is inlined in all compilation mode.

Automatic argument completion dispense for the need to write all the arguments of a function provided the name of the argument is the same than a name in the current scope.

real x = 1
real y = 10

def f:
  real in x
  real in y

f(y=1.) # x added automatically
f() # both x and y added

Automatic arguments completion works for keyword arguments call only. It cannot be mixed with positional argument code.

# with f as above

f(y) # not supported
f(y=1.) # supported

If a function should not be compiled, used the noprod modifier. This is usefull when the function is only used as a spec provider, and that the function should not be compiled.

def noprod f:
  real x(n) # n is not defined, this would give an error if compiled

def spec(f) g:
  int in n

  x += 1 # definition of x is provided by the spec of f

The noprod modifier is not inherited during a spec interpolation. So, only f is not compiled. To not compile g, explicitly use the modifier noprod.

to help distinguish between pure and non-pure function used the modifiers pure and sidef

def pure f:
  pass

def sidef g:
  pass

The modifier sidef has no effect during compilation. The modifier clearly states the intent of the coder: that the function g has side-effects, and cannot be marked as pure.

Interface

An interface is declared with the interface keyword

interface:
  def f:
    real in x
    real res r

To facilitate the definition of C procedure the modifier iso(c) can be used

interface:
  def iso(c) f:
    real in x

The iso(c) modifier can be used on any function declaration and is not restricted to interface declaration. The effect of the modifier is to produce

!!!
subroutine f(x) bind(c)
  use iso_c_binding
  real, intent(in) :: x
end subroutine

Class

A class is defined with the class keyword.

class A:
  real x

  def f:
    self in
    real res r
    r = self.x

  def pget y:
    s in
    real res r
    r = s.x

  def pset y:
    s inout
    real in value
    s.x = value

The first argument of any class method must be the self argument. The name used for the self argument can be anything. Above we used s instead of self for the getter and setter.

Getter and Setter are defined with the pget and pset modifiers.

Inheritance is indicated with parenthesis after the class name

class C(B, A):
  pass

You can separate the specification and the implementation of a class with the spec interpolation

# spec.fy

class A:
  real x
  def pure f:
    self in
    real res r

# code.fy
import spec(*)

class A:
  def f:
    r = self.x + 10

You can explicitly state the spec to use with the spec modifier

class A:
  real x

class spec(A) B:
  pass

You can use the inline statement to include verbatim the definition of a class or a function into your class

class A:
  real x

class B:
  inline A

Allocation

Memory is allocated and deallocated with the alloc and dealloc keyword

alloc: x y(n) z

alloc(n):
  x
  y
  z(m)

dealloc: x y z

When the keyword alloc has an argument, it is used as the default size for any variable where no size is specified.

Control Flow

Fython has if, for, fop, while and where statement

if x < 1:
  y += 1

elif x < 1:
  y += 2

else:
  y = 0

The third argument in the bracket of a for statement is the step size

for i in [1, 2]:
  r += x[i]

for i in [0, 10, 2]:
  r += x[i] # 0, 2, 4, ...

The fop loop is a parallel for loop. The Fortran equivalent is a do concurrent loop.

fop i in [1, 2]:
  r += x[i]

The while loop is

while x < 10:
  x += 1

The where statement is

where x > 1:
  x = y

elwhere x < 1:
  x -= 1

else:
  x = 0

Print

Printing to the console needs no modifier

print 'x {:x}'

When an url is used the file extension is assumed to be .out

print .simulation 'x {:x}'

A file system path can also be used

print './simulation.out' 'x {:x}'

You can then choose any extension you want.

You can print to a character variable when its name does not contain a dot

char(100) r

print r 'x {:x}'

If the name contains a dot use the unit modifier

print unit(atom.name) 'proton'

The unit modifier can also be used if you opened a file by yourself

int u = 10
open(unit=u, file='./simulation.out')
print unit(u) 'x {:x}'

If you use a number, the unit modifier is not necessary

print 10 'x {:x}'

You can control the mode in which the file is written to during a print statment with the mode modifier

print mode(a) 'x {:x}'

print mode(w) 'overwrite any previous content'

The default mode is a.

To continue printing on the same line, use the c modifier

print c 'start '
print c ' on same line'
print ' ending the line'
print 'this one on a new line'

The format mini-language is that of Fortran plus several additions

print """
  {:x} : general format used
  {f5.2:x} : float format
  {i5:x} : int format

  {v:y} : vector format: [1, 2, 3, ]

  {vc:y} : vector content format: 1, 2, 3,

  {va:y} : vector format: array([1, 2, 3, ]) ; usefull for python post-processing
"""

The additions are the v, vc and va formats that facilitates the printing of vectors.

Format that helps printing to the JSON format are also avalaible. The JSON formats avoid typing the name of a variable twice, and helps to deal with comma.

print """
  {jn:x} : json no comma before: "x": x

  {j:x} : json with comma before: ,"x":x

  {jv:x} : json vector: "x":[1, 2, 3]

  {jvn:x} : json vector no comma before: ,"x":[1,2,3]

  {j_tag:x} : json with specified tag: ,"tag":x

  {jv_tag:x} : json vector with specified tag: ,"tag":[1,2,3]

  {jn_tag:x} : json no comma before with specified tag: "tag":x

  {jvn_tag:x} : json no comma before vector with specified tag: "tag":[1,2,3]

"""

In a Typical printing with JSON format, the first element is explicitly specified without leading comma, then the remaining elements are added, prepended by a comma.

print """
  [
    { } # first element no comma

    ,{ } # any addition prepended by a comma

    ,{
      {jn:x} # no comma
      {j:y} # prepended by a comma

    }

  ]
"""

If a print statement is used only in debug mode, use the xip instruction

xip 'printed only in debug mode'
print 'printed in both debug and release mode'

The xip takes the same modifiers than the print instruction. The xip instruction is usefull for debugging.

Read

You can read a file by specifying its url. The extension is then assumed to be .out

read .data: x y z

You can specify a path in the file system with a string

read './data.out': x y z

You are then free to use any extension you want.

The read statement in Fython supports csv-like formats automatically. In Fortran, this is a called a list-directed read. For this release, the other kind of read statement are not supported.

You can use the name of variable that does not contains a dot for the read source

char(100) data

read data: x y z

If the name of the variable contains a dot, use the unit modifier

read unit(mendeleiv.table): x y z

You can read into a vector or any other variable

read .data:
  x[:, i]
  atom.name

FyTypes

Only three kinds of data type can travel back and forth between Python and Fython

#>>>
from fython import *
x = Real()
y = Int()
z = Char(size=100)

m = load('.mean')

m.f(x, y, z)

Only function that have no return value can be called from Python. In Fortran term, f must be a subroutine.

Fython can modify in-place the element send by Python. The change will be seen in Python. The same is true in Python. In change made to a fytype in Python, will be seen in Fython.

The value of a fytype is always accesses with a slice, wheter the fytype is a scalar or a vector

#>>>
x = Real()
y = Real(value=[1, 2, 3])

x[:] = 9
y[:1] = 10 + x[:]

The three Fytypes all have the optional arguments size, shape and ``value. They are shown below with their default value

#>>>
x = Real(size=4, shape=[], value=None)
y = Int(size=4, shape=[], value=None)
z = Char(size=100, shape=[], value=None)

An empty list indicates a scalar. A vector is defined by specifying the number of element in each dimension.

#>>>
x = Real(shape=[10, 10])

The argument value is used to connect a fytype to a Python object. Any change made to the fytype will be reflected in the Python object

#>>>
x = [1, 2, 3]
y = Real(value=x)

To access a global variable, use its name

#>>>
 from fython import *
 m = load('.mean')
 tolerance = m.tolerance()

Fython will automatically detects the type of the variable and use the default fytype initializer above. You can specify the variable specification yourself

#>>>
x = m.x(size=8, shape=[10])

Once setted the shape of fytype cannot change. This limitation can be overcome by letting Python and Fython share informations

#>>>
m = load('.mean')
m.compute()
n = m.result_size()
result = m.result(size=n[:])

Callback

In Fython, it is possible to call a Python function. Such callable function are called callback. The trick is to pass the address of the callback. This address is a simple integer, so, in fython, we cast it to a function pointer.

The code below shows how to transfer integer, real and arrays of these two.

The Fython code goes as follows.

import:
  iso_c_binding(*)

interface:
  def iso(c) py_fct:
    int(c_int) value in:
      xint
    real(c_float) value in:
      xreal
    int(c_int) value in:
      nintv
      nrealv
    int(c_int) in:
      xintv(*)
    real(c_float) in:
      xrealv(*)

def f:
  int(8) in:
    py_fct_pointer_int
  c_funptr:
    py_fct_pointer
  proc(py_fct) pointer:
    pyf
  int:
    x
    xv(2)
  real:
    y
    yv(2)

  print 'fython start: {:py_fct_pointer_int}'
  py_fct_pointer = transfer(py_fct_pointer_int, py_fct_pointer)
  c_f_procpointer(py_fct_pointer, pyf)
  print 'pyf called'
  x = 1
  y = 0.5
  xv = 10
  yv = 5.5
  pyf(x, y, 2, 2, xv, yv)
  print 'fython exit'

The Python goes like this

#>>>
from mttc import *
from ctypes import *
import numpy as np

m = load('.pycall', force = 2)

def f(xint, xreal, nintv, nrealv, xintv, xrealv):
  array_type = c_int*nintv
  addr = addressof(xintv.contents)
  xintv = np.array(array_type.from_address(addr), copy=0)

  array_type = c_float*nrealv
  addr = addressof(xrealv.contents)
  xrealv = np.array(array_type.from_address(addr), copy=0)

  print('hello from python', xint, xreal, nintv, nrealv, xintv, xrealv)

c_fun_dec = CFUNCTYPE(None, c_int, c_float, c_int, c_int, POINTER(c_int), POINTER(c_float))

c_fun = c_fun_dec(f)

c_fun_int = cast(c_fun, c_void_p).value

m.f(
  py_fct_pointer_int = Int8(c_fun_int),
)

Load Function

The python api side load function has the following default arguments

#>>>
load(
  fython_module_url,
  force = 0,
  release = 0,
)

The force argument is used to force a refresh of the Fython dependency cache.

The release argument indicates wheter to run in debugging mode (0), or release mode (1). In debug mode, Fython tries to detect errors. In release mode, all the compiler optimization are enabled.

When a Fython module is loaded, you access its objects with there name.

#>>>
from fython import *
m = load('.mean')

x = m.global_variable()
y = m.global_variable_with_explicit_shape(shape=[10])

# function call
m.mean(x, y)

# keyword call
m.mean(
  offset = x,
  vec = y,
)

When a global variable is accessed, Fython will automatically determine its fytype. For function however, only minimal arguments consistency is made. You should make sure that you invoke your Fython function with the right argument order and the right fytype. The argument order consistency can be alleviated by using a keyword argument call. Fython will then call your Fython function with the argument in the right order.

Pycessor

Pycession instruction are specified within bars. The Python imports necessary for the pycession at the top of your Fython module

import numpy = np

pi = |np.py|

A pycession can be multiline

|
  for T in ['real', 'int']:
    write('def f_{T:s} = g(T = {T:s})'.format(T)
|

The signature of the write function is

#>>>
write(
  string,
  *args,
  end = '\n',
  **kwargs,
)

The positional and keyword arguments are used to format the string argument.

When a pycession is a Python expression, its value is directly inserted in your Fython code. The write function is necessary when the pycession is not an expression. Each string passed to the write function is inserted in your Fython code. The end argument is appended to the string.

Any valid Python code is possible in a Pycession.

The Pycessor is a preprocessor. Do not use it to pass arguments to Fython because the dependency system will not see any post-compilation change in your Python module. The Pycessor is meant to facilitate the generation of tedious code, or to trigger any kind of necessary preparation before compilation.

Template

A function template is defined with the def statement

def f:
  T in x
  T res r
  r += x

def g = f(T=real)

To templatize a whole package use the import statement

import quicksort(*)
||type_provide=stat.mean, target_class=KMean||

Package interpolation can also be multilines

import quicksort(*)
||
  type_provide = stat.mean
  target_class = KMean

||

To disable package interpolation during a package import, use the asis modifier.

import asis random

This is usefull to avoid further interpolation during an ongoing package interpolation.