#+SETUPFILE: ../packages/org-themes/src/bigblow_inline/bigblow_inline.theme #+STARTUP: org-pretty-entities entitiespretty #+PROPERTY: header-args :comments no #+OPTIONS: timestamp:nil #+OPTIONS: h:3 num:t toc:3 #+TITLE: The pugs user manual #+OPTIONS: author:nil date:nil #+OPTIONS: tex:t #+LANGUAGE: en #+ATTR_LATEX: :width 4cm #+HTML_HEAD_EXTRA: <style> pre.src-pugs:before { content: 'pugs'; } </style> #+HTML_HEAD_EXTRA: <style> pre.src-pugs-error:before { content: 'invalid pugs'; } </style> #+HTML_HEAD_EXTRA: <style> .remark{ @extend .todo !optional;} </style> #+LATEX_CLASS_OPTIONS: [10pt] #+LATEX_HEADER: \usepackage[hmargin=2.5cm,vmargin=1.5cm]{geometry} #+LATEX_COMPILER: pdflatex --shell-escape #+LATEX_HEADER_EXTRA: \usepackage{amsmath} #+LATEX_HEADER_EXTRA: \usepackage{amsthm} #+LATEX_HEADER_EXTRA: \usepackage{amssymb} #+LATEX_HEADER_EXTRA: \usepackage{xcolor} #+LATEX_HEADER_EXTRA: \usepackage{mdframed} #+LATEX_HEADER_EXTRA: \BeforeBeginEnvironment{minted}{\begin{mdframed}[linecolor=blue,backgroundcolor=blue!5]} #+LATEX_HEADER_EXTRA: \AfterEndEnvironment{minted}{\end{mdframed}} #+LATEX_HEADER_EXTRA: \BeforeBeginEnvironment{verbatim}{\begin{mdframed}[linecolor=gray,backgroundcolor=green!5]} #+LATEX_HEADER_EXTRA: \AfterEndEnvironment{verbatim}{\end{mdframed}} #+LATEX_HEADER_EXTRA: \newtheorem{note}{Note} #+LATEX_HEADER_EXTRA: \BeforeBeginEnvironment{note}{\begin{mdframed}[linecolor=orange,backgroundcolor=orange!5]} #+LATEX_HEADER_EXTRA: \AfterEndEnvironment{note}{\end{mdframed}} #+LATEX_HEADER_EXTRA: \newtheorem{warning}{Warning} #+LATEX_HEADER_EXTRA: \BeforeBeginEnvironment{warning}{\begin{mdframed}[linecolor=red,backgroundcolor=red!5]} #+LATEX_HEADER_EXTRA: \AfterEndEnvironment{warning}{\end{mdframed}} * Introduction ~pugs~[fn:pugs-def] is a general purpose solver collection built to approximate solutions of partial differential equations. It is mainly (but not only) designed to deal with hyperbolic problems using finite-volume methods. ~pugs~ is a parallel software that uses ~MPI~[fn:MPI-def] and multi-threading for parallelism. Multi-threading is achieved through an encapsulation of some [[https:github.com/kokkos/kokkos][Kokkos]] mechanisms. The philosophy of ~pugs~ is to provide "simple" numerical tools that are assembled together through a high-level language (a DSL[fn:DSL-def] close to the mathematics) to build more complex solvers. This approach is inspired by the success of [[http://freefem.org][FreeFEM]], which use a similar approach. Before detailing the leading concepts and choices that we have made to develop ~pugs~, we give a simple example that should illustrate the capabilities of the code. ** An example For instance, the following code builds a uniform Cartesian grid of $]-1,1[^2$ made of $20\times20$ cells and transforms it by displacing the mesh nodes according to a user defined vector field $T: \mathbb{R}^2 \to \mathbb{R}^2$. #+NAME: introduction-example #+BEGIN_SRC pugs :exports both :results output import mesh; import math; import writer; let pi:R, pi = acos(-1); let theta:R^2 -> R, x -> 0.5*pi*(x[0]*x[0]-1)*(x[1]*x[1]-1); let M:R^2 -> R^2x2, x -> (cos(theta(x)), -sin(theta(x)), sin(theta(x)), cos(theta(x))); let T: R^2 -> R^2, x -> x + M(x)*x; let m:mesh, m = cartesian2dMesh((-1,-1), (1,1), (20,20)); m = transform(m, T); write_mesh(gnuplot_writer("transformed", 0), m); #+END_SRC The example is quite easy to read. - First, some *modules* are loaded: the ~mesh~ module, which contains some mesh manipulation functions. The ~math~ module provides a set of classical mathematical functions ($\sin$, $\cos$, ...). The ~writer~ module is used to save meshes or discrete functions to files using various formats. - The second block of data defines variables of different kind - ~pi~ is a real value that is initialized by an approximation of $\pi$. - ~theta~ is the real value function $\theta$ defined by \begin{equation*} \theta: \mathbb{R}^2 \to \mathbb{R},\quad\mathbf{x} \mapsto \frac{\pi}{2} (x_0^2-1)(x_1^2-1) \end{equation*} - ~M~ is the $\mathbb{R}^{2\times2}$ matrix field $M$ defined by \begin{equation*} M: \mathbb{R}^2 \to \mathbb{R}^{2\times2},\quad\mathbf{x} \mapsto \begin{pmatrix} \cos(\theta(\mathbf{x})) & -\sin(\theta(\mathbf{x})) \\ \sin(\theta(\mathbf{x})) & \cos(\theta(\mathbf{x})) \end{pmatrix} \end{equation*} - ~T~ is the vector field $T$ defined by \begin{equation*} T: \mathbb{R}^2 \to \mathbb{R}^2, \quad\mathbf{x} \mapsto (I+M(\mathbf{x}))\mathbf{x} \end{equation*} - Finally ~m~ is defined as the uniform Cartesian mesh grid of $]-1,1[^2$. The last argument: ~(20,20)~ sets the number of cells in each direction: $20$. - The third block is the application of the transformation $T$ to the mesh. Observe that if the resulting mesh is stored in the same variable ~m~, the old one was not modified in the process. It is important to already have in mind that the ~pugs~ language *does not allow* the modifications of values of variables of *non-basic* types. This is discussed in the section [[high-level-types]]. - Finally, the last block consists in saving the obtained mesh in a ~gnuplot~ file. The result is shown on Figure [[fig:intro-example]]. #+NAME: intro-transform-mesh-img #+BEGIN_SRC gnuplot :exports results :file (substitute-in-file-name "${PUGS_SOURCE_DIR}/doc/intro-transform-mesh.png") reset unset grid unset border unset key unset xtics unset ytics set terminal png truecolor enhanced set size square plot '<(sed "" $PUGS_SOURCE_DIR/doc/transformed.0000.gnu)' w l #+END_SRC #+CAPTION: Obtained transformed mesh #+NAME: fig:intro-example #+ATTR_LATEX: :width 0.38\textwidth #+ATTR_HTML: :width 300px; #+RESULTS: intro-transform-mesh-img Even if this first example is very simple, some key aspects can already be discussed. - There is no predefined constant in ~pugs~. Here a value is provided for ~pi~. - There are two kinds of variable in ~pugs~: variables of basic types and variable of high-level types. This two kinds of variable behave almost the same but one must know their differences to understand better the underlying mechanisms and choices that we made. See section [[basic-types]] and [[high-level-types]] for details. - Also, there are two types of functions: *user-defined* functions and *builtin* functions. In this example, ~theta~, ~M~ and ~T~ are user-defined functions. All other functions (~cos~, ~cartesian2dMesh~,...) are builtin functions and are generally defined when importing a module. These functions behave similarly, one should refer to [[functions]] for details. ** Concepts and design As it was already stated ~pugs~ can be viewed as a collection of numerical methods or utilities that can be assembled together, using a user friendly language, to build simulation scenarios. Utilities are tools that are often used in numerical simulations. Examples of such utilities are mesh manipulations, definition of initial conditions, post-processing, error calculations,... *** A C++ toolbox driven by a user friendly language Numerical simulation packages are software of a particular kind. Generally, in order to run a calculation, one has to define a set of data and parameters. This can simply be definition of a discretization parameter such as the mesh size. One can also specify boundary conditions, equations of state, source terms for a specific model. Choosing a numerical method or even more setting the model itself is common in large code. In ~pugs~, all these "parameters" are set through a DSL[fn:DSL-def]. Thus, when ~pugs~ is launched, it actually executes the provided script. A ~C++~ function is associated to each instruction of the script. The ~C++~ components of ~pugs~ are completely unaware of the other ones. ~pugs~ interpreter is responsible of data flow between the components: it manages the data transfer between those ~C++~ components and ensures that the workflow is properly defined. **** Why? In this section we motivate the choice of a language and not of a more standard approach. ***** Data files are evil There are lots of reasons not to use data files. By data file, we refer to a set of options that describe physical models, numerical methods or their settings. - Data files are not flexible. This implies in one hand that application scenarios must be known somehow precisely to reflect possible options combinations and on the other hand even defining a specific initial data may require the creation of a new option and the associated code (in ~C++~ for instance). \\ It is quite common to fix the last point by adding a local interpreter to evaluate user functions for instance. - Data files may contain irrelevant information. Actually, it is quite common to allow to define options that are valid but irrelevant to a given scenario. This policy can be changed but it is generally not an easy task and requires more work from the user (which can be a good thing). - It is quite common that data files become obsolete. An option was not the right one, or its type changed to allow other contexts... This puts pressure on the user. - Even worst, options meaning can depend on other options. Unfortunately, it happens quite easily. For instance, a global option can change implicitly the treatment associated to another one. This is quite dangerous since writing or reading the data file requires an important and up to date knowledge of the code's internals. - Another major difficulty when dealing with data files is to check the compatibility of provided options. ***** Embedded "data files" are not a solution Using directly the general purpose language (~C~, ~C++~, ~Fortran~,...) used to write the code can be tempting. It has the advantage that no particular treatment is necessary to build a parser (to read data files or a script), but it presents several drawbacks. - The first one is probably that it allows to much freedom. While defining the model and numerical options, the user has generally access to the whole code and can change almost everything, even things that should not be changed. - Again, one can easily have access to irrelevant options and it requires a great knowledge of the code to find important ones. - With that regard, defining a simulation properly can be a difficult task. For instance, in the early developments of ~pugs~ (when it was just a raw ~C++~ code) it was tricky to change boundary conditions for coupled physics. - Another difficulty is related to the fact that code's internal API is likely to change permanently in a research code. Thus valid constructions or setting may become rapidly obsolete. In other words keeping up to date embedded "data file" might be difficult. - Finally it requires recompilation of pieces of code (which can be large in some cases) even if one is just changing a simple parameter. ***** Benefits of a DSL Actually, an honest analysis cannot conclude that a DSL is the solution to all problems. However, it offers some advantages. - First, it allows a fine control on what the user can or cannot perform. In some sense, it offers a chosen level of flexibility. - It allows to structure the code in the sense that new developments have to be designed not only focusing on the result but also on the way it should be used (its interactions with the scripting language). - In the same idea, it provides a framework that should ease the desired principle of "do simple things and do them well". - There are no hidden dependencies between numerical options: the DSL code is easier to read (than data files) and it is more difficult to get it obsolete (this is not that true in early developments since the language itself and some concepts are still likely to change). - The simulation scenario is *defined* by the script, it is to the responsibility of the user and not to the charge of the code to check its meaning. ***** ~pugs~ language purpose ~pugs~ language is used to assemble ~C++~ tools which are well tested. These tools should ideally be small pieces of ~C++~ code that do one single thing and do it well. Another purpose of the language is to allow perform high-level calculations. In other words, the language defines a data flow and checks that each ~C++~ piece of code is used correctly. Since each piece of code acts as a pure function (arguments are unchanged by calls), the calling context is quite easy to check. Finally it aims at simplifying the definition of new methods since common utilities are available directly in scripts. ***** The framework: divide and conquer ~pugs~ is a research oriented software, thus generally the user is also a developer. If this paragraph is indeed more dedicated to the developer, by reading it, the user will have a better understanding of the development choices and the underlying policy that that the code follows. Actually the development framework imposed by the DSL tends to guide writing of new methods. - A natural consequence is that the developer is encourage to write small independent ~C++~ methods (even if it does not forbid to write big monolithic pieces of code). However as already said, one should try to respect the following precept: write small piece of code to ease their testing and validation, try to do simple things the right way. - Also, in the process of writing a *new numerical methods*, one must create *new functions in the language*. Moreover, if a new method is close to an existing one, it is generally *better* to use completely new underlying ~C++~ code than to patch existing methods. Starting from a *copy* of the existing code is *encouraged* for developments. This may sound weird since classical development guidelines encourage inheritance or early redesign. Actually, this policy is the result of the following discussion. - Using this approach, one *separates* clearly the *design* of a numerical method and the *design* of the code! - From the computer science point of view, early design for new numerical methods is generally wrong: usually one cannot anticipate precisely enough eventual problems or method corrections. - It is much more difficult to introduce bugs in existing methods, since previously validated methods are unchanged! - For the same reason, existing methods performances are naturally unchanged by new developments. - Also, when comparing methods, it is better to compare to the original existing code. - If the new method is abandoned or simply not kept, the existing code is not polluted. - Finally when a method is validated and ready to integrate the mainline sources of the code, it is easier to see differences with existing ones and *at this time* one can redesign the ~C++~ code checking that results are unchanged and performances not deteriorated. At this time, it is likely that the numerical method design is finished, thus (re)designing the source code makes more sense. - Another consequence is that utilities are not be developed again and again. - This implies an /a priori/ safer code: utilities are well tested and validated. - It saves development time obviously. - Numerical methods code is not polluted by environment instructions (data initialization, error calculation, post-processing,...) - The counterpart is somehow classical. In the one hand, the knowledge of existing utilities is required, this document tries to address a part of it. In the other hand, if the developer requires a new utility, a good practice is to discuss with the others to check if it could benefit to them. Then one can determine if it should integrate rapidly or not the main development branch. ***** Why not python or any other scripting language? As it was already pointed out above, general purpose languages offer to much freedom: it is not easy to protect data. For instance in the ~pugs~ DSL, non basic variables are constant (see paragraph [[high-level-types]]). It is important since it prevents the user to modify data in inconsistent ways. Also, one must keep in mind that constraining the expressiveness is actually a strength. As said before, one can warranty coherence of the data, perform calculations without paying attention to the parallelism aspects,... Observe that it is not a limitation: if the DSL's field of application needs to be extended, it is always possible. But these extensions should never break the rule that a DSL must not become a general purpose language. Providing a language close to the application (or here in particular close to the Mathematics language) reduces the gap between the application and its expression (code): Domain Specific Languages are made for that. Finally, python is ugly. *** A high-level language Following the previous discussion, the reader should now understand the motivations that drive the design choices which made of ~pugs~ some kind of a ~C++~ toolbox driven by a user friendly language. #+begin_verse Keep it simple and relevant! #+end_verse In the development process of an application, the easy step is generally the implementation itself. It is even more true when the developer feels that the changes to code are natural and that the modifications themselves look easy. Obviously, any experienced programmer knows that writing the code is only the first step of a much longer process which requires rigorous tests and validations, the enrichment of a non-regression database. Generally one also desires to ensure that the development is used within the correct bounds which requires to implement checking and data verification. In a perfect world, an up-to-date documentation of the functionality and its domain of validity. This is even more true when defining a language (or a DSL). Enriching a language syntax (or grammar) is not something that must be done to answer a specific need. It must not be done /because it is possible to do it/! #+begin_verse When designing a language, the difficulty is not to offer new functionalities,\\ it is generally to decide not to offer them.\\ --- Bjarne Stroustrup, C++ conference 2021. #+end_verse If the grammar of ~pugs~ is still likely to be extended, it should *never* integrate low-level instructions. Low-level instructions give too much freedom and thus are a source of errors. Several things are already done to forbid this kind of evolution. The constness of high-level data is a good illustration. For instance, meshes or discrete functions *cannot* be modified. This is not only a security to protects the user from doing "dangerous" manipulations, but it also permits to define high-level optimizations. - Since meshes are constant objects, one can for instance compute geometric data on demand. These data are kept into memory as long as the mesh lives. - Forbidding the modification of values of a discrete function ensures that parallel communication instructions should never appear in a ~pugs~ script. Another benefit of not providing low-level instructions is that the scripts are more easy to write and read, and it is more difficult to write errors. * Language ** Variables In order to simplify the presentation, before going further, we introduce the construction that allows to print data to the terminal. It follows ~C++~ streams construction for convenience. For instance #+NAME: cout-preamble-example #+BEGIN_SRC pugs :exports both :results output cout << "2+3 = " << 2+3 << "\n"; #+END_SRC produces the following output #+results: cout-preamble-example The code is quite obvious for ~C++~ users, note that ~"\n"~ is the linefeed string (there is no character type in ~pugs~, just strings). Actually, ~cout~ is itself a variable, we will come to this later. ~pugs~ is a strongly typed language. It means that a variable *cannot* change of type in its lifetime. *** Declaration and affectation syntax **** Declaration of simple variables To declare a variable ~v~ of a given type ~V~, one writes #+BEGIN_SRC pugs :exports source let v:V; #+END_SRC This instruction is read as #+begin_verse Let $v\in V$. #+end_verse Actually, - ~let~ is the declaration keyword, - ~v~ is the variable name, - ~:~ is a separation token (can be read as "/in/" in this context), - ~V~ is the identifier of the type, and - ~;~ marks the end of the instruction. For instance to declare a real variable ~x~, one writes #+NAME: declare-R #+BEGIN_SRC pugs :exports both :results none let x:R; #+END_SRC The instructions that follow the declaration of a variable can use it while defined in the same scope, but it cannot be used before. Also, after its declaration, one cannot declare another variable with the same name in the same scope (see [[blocks-and-life-time]]). #+NAME: redeclare-variable #+BEGIN_SRC pugs-error :exports both :results output let x:R; // first declaration let x:R; // second declaration #+END_SRC produces the following error #+results: redeclare-variable **** Affectation of simple variables To affect the value of an expression ~expression~ to variable ~v~ one simply uses the ~=~ operator. Thus assuming that a variable ~v~ has already been /declared/, one writes simply #+BEGIN_SRC pugs :exports source v = expression; #+END_SRC There is not to much to comment, reading is quite natural - ~v~ is the variable name, - ~=~ is the affectation operator, - ~expression~ is some code that provide a value (it can be another variable, an arithmetic expression, the result of a function,...), and - ~;~ marks the end of the instruction. One for instance can write #+NAME: simple-affectations-example #+BEGIN_SRC pugs :exports both :results output import math; // to load sin function let a:N; let b:N; let x:R; a=3; b=2+a; x=sin(b)+a; cout << "a = " << a << " b = " << b << " x = " << x << "\n"; #+END_SRC In this example, we imported the `math` module which provides the `sin` function. and we used the ~N~ data type of natural integers ($\mathbb{N}\equiv\mathbb{Z}_{\ge0}$). Running the example gives the following result. #+results: simple-affectations-example #+BEGIN_warning Actually, *separating* the *declaration* from the *initialization* of a variable is quite *dangerous*. This is prone to errors and can lead to the use of undefined values. During the compilation of scripts, ~pugs~ detects uninitialized values. For instance, #+NAME: uninitialized-variable #+BEGIN_SRC pugs-error :exports both :results output let x:R; let y:R; y=x+1; #+END_SRC produces the following compilation error #+results: uninitialized-variable For more complex constructions, it can be very difficult to detect at compile time, this is why it is *encourage* to use variable definition (see [[definition-simple-variables]]). Observe nonetheless, ~pugs~ checks at runtime that used variables are correctly defined. If not an error is produced. #+END_warning **** Definition of simple variables<<definition-simple-variables>> The best way to define variables is the following. To define a variable ~v~ of a given type ~V~, from an expression one writes #+BEGIN_SRC pugs :exports source let v:V, v = expression; #+END_SRC - ~let~ is the declaration keyword, - ~v~ is the variable name, - ~:~ is a separation token (can be read as "/in/" in this context), - ~V~ is the identifier of the type, - ~,~ is the separator that indicates the beginning of the affectation, - ~expression~ is some code that provide a value (it can be another variable, an expression, the result of a function,...), and - ~;~ marks the end of the instruction. A practical example is #+NAME: simple-definition-example #+BEGIN_SRC pugs :exports both :results output import math; // to load sin function let a:N, a=3; let b:N, b=2+a; let x:R, x=sin(b)+a; cout << "a = " << a << " b = " << b << " x = " << x << "\n"; #+END_SRC which produces the result #+results: simple-definition-example **** Compound declarations and affectations We shall now see some weird constructions that are not useful by themselves but which are important when dealing with builtin functions. We just give an example which should be enough to explain the syntax. #+NAME: compound-definition-example #+BEGIN_SRC pugs :exports both :results output let (a, b, x):N*N*R; (x,b) = (2.3, 12); a = 3; cout << "a = " << a << " b = " << b << " x = " << x << "\n"; let (c,y,d) : N*R*N, (c, y, d) = (8, 1e-3, 2); cout << "c = " << c << " d = " << d << " y = " << y << "\n"; #+END_SRC which produces the result #+results: compound-definition-example The only potential mistake with this construction, is that variables must appear in the same order in the declaration part and in the affectation part of a definition. For instance, the following code is invalid. #+NAME: invalid-compound-definition #+BEGIN_SRC pugs-error :exports both :results output let (x,y):R*R, (y,x) = (0,1); #+END_SRC It produces the following error #+results: invalid-compound-definition which is easy to fix. *** Blocks and lifetime of variables<<blocks-and-life-time>> In pugs scripts, variables have a precise lifetime. They are defined within scopes. The main scope is implicitly defined and sub-scopes are enclosed between bracket pairs: ~{~ and ~}~. Following ~C++~, a variable exits (can be used) as soon as it has been declare until the end of the scope where it has been declared. At this point we give a few examples. **** A variable cannot be used before its declaration #+NAME: undeclare-variable #+BEGIN_SRC pugs-error :exports both :results output n = 3; let n:N; #+END_SRC #+results: undeclare-variable **** A variable cannot be used after its definition scope #+NAME: out-of-scope-variable-use #+BEGIN_SRC pugs-error :exports both :results output { let n:N, n = 3; n = n+2; } cout << n << "\n"; #+END_SRC #+results: out-of-scope-variable-use **** Variable name can be reused in an enclosed scope #+NAME: nested-scope-variable-example #+BEGIN_SRC pugs :exports both :results output let n:N, n = 0; // global variable { cout << "global scope n = " << n << "\n"; let n:N, n = 1; // scope level 1 variable { cout << "scope level 1 n = " << n << "\n"; let n:N, n = 2; // scope level 2 variable cout << "scope level 2 n = " << n << "\n"; } { cout << "scope level 1 n = " << n << "\n"; let n:N, n = 4; // scope level 2.2 variable cout << "scope level 2.2 n = " << n << "\n"; } cout << "scope level 1 n = " << n << "\n"; } cout << "global scope n = " << n << "\n"; #+END_SRC #+results: nested-scope-variable-example This example is self explanatory. Obviously such constructions are generally a wrong idea. This kind of constructions may appear in loops where the variables defined in blocks follow the same lifetime rules. *** Basic types<<basic-types>> Basic types in ~pugs~ are boolean ~B~, natural integers ~N~, integers ~Z~, real ~R~, small vectors ~R^1~, ~R^2~ and ~R^3~, small matrices ~R^1x1~, ~R^2x2~ and ~R^3x3~ and strings ~string~. #+BEGIN_note Observe that while mathematically, obviously $\mathbb{R} = \mathbb{R}^1 = \mathbb{R}^{1\times1}$, the data types ~R~, ~R^1~ and ~R^1x1~ are different in ~pugs~ and are *not implicitly* convertible from one to the other! This may sound strange but there are few reasons for that. - First, these are the reflect of internal ~pugs~ ~C++~-data types that are used to write algorithms. In its core design pugs aim at writing numerical methods generically with regard to the dimension. One of the ingredients to achieve this purpose is to use dimension $1$ vectors and matrices when some algorithms reduce to dimension $1$ instead of ~double~ values. To avoid ambiguity that may arise in some situations (this can lead to very tricky code), we decided to forbid automatic conversions of these types with ~double~. When designing the language we adopt the same rule to avoid ambiguity. - A second reason is connected to the first one. Since ~pugs~ aims at providing numerical methods for problems in dimension $1$, $2$ or $3$, this allow to distinguish the nature of the underlying objects. - It is natural to consider that the coordinates of the vertices defining a mesh in dimension $d$ are elements of $\mathbb{R}^d$, - or that a velocity or a displacement are also defined as $\mathbb{R}^d$ values. Thus using ~R^1~ in dimension $1$ for this kind data precise their nature in some sense . #+END_note **** Expression types The interpreter gives type to some special expressions. - special boolean (type ~B~) expressions. Two *keywords* allow to define boolean values: ~true~ and ~false~. - Integers (type ~Z~) are defined as a contiguous list of digits. - For instance, the code ~0123~ is interpreted as the integer $123$. - However the sequence ~123 456~ is *not interpreted* as $123456$ but as the two integers $123$ and $456$. - Real values (type ~R~) use the same syntax as in ~C++~. For instance, the following expressions are accepted to define the number $1.23$. #+BEGIN_SRC pugs :exports both 1.23; 0.123E1; .123e+1; 123e-2; 12.3E-1; #+END_SRC - ~string~ values are defined as the set of characters enclosed between two double quotes ( ~"~ ). The string /Hello world!/ would be simply written as ~"Hello world!"~. Strings support the following escape sequences (similarly to ~C++~): | escape | meaning | |--------+-----------------| | ~\'~ | single quote | | ~\"~ | double quote | | ~\?~ | question mark | | ~\\~ | backslash | | ~\a~ | audible bell | | ~\b~ | backspace | | ~\f~ | new page | | ~\n~ | new line | | ~\r~ | carriage return | | ~\t~ | horizontal tab | | ~\v~ | vertical tab | These special characters are not interpreted by ~pugs~ itself but interpreted by the system when an output is created. They are just allowed in the definition of a ~string~. **** Variables of basic types are stored by value This means that each variable of basic type allocates its own memory to store its data. This is the natural behavior for variables thus one understands easily that in the following example: #+NAME: basic-type-value-storage #+BEGIN_SRC pugs :exports both :results output let a:N, a = 3; let b:N, b = a; a = 1; cout << "a = " << a << " b = " << b << "\n"; #+END_SRC which produces the expected result #+results: basic-type-value-storage When defining ~b~, the *value* contained is ~a~ is copied to set the value of ~b~. Thus changing ~a~'s value does not impact the variable ~b~. **** Variables of basic types are mutable In ~pugs~ the only variables that are mutable (their value can be *modified*) are of basic types. Executing the following code #+NAME: basic-type-mutable-value #+BEGIN_SRC pugs :exports both :results output let a:N, a = 2; a += 3; cout << "a = " << a << "\n"; #+END_SRC gives #+results: basic-type-mutable-value which is not a surprise. However, the use of the ~+=~ operator results in the modification of the stored value. There is no copy. Actually, this is not really important from the user point of view. One just have to keep in mind that, as it will be depicted after, high-level variables *are not mutable*: their values can be *replaced* by a new ones but *cannot be modified*. *** Implicit type conversions In order to avoid ambiguities, in ~pugs~, there is *no implicit* conversion in general. #+BEGIN_note Actually, there are only two situations for which implicit type conversion can occur - when the value is given as a parameter of a function, or - when the value is used as the returned value of a function. This will be detailed in section [[functions]]. #+END_note This means that all affectation, unary and binary operators are defined explicitly for supported types. *** Operators **** Affectation operators In the ~pugs~ language, the affectation operators are the following. | operator | description | |----------+----------------------| | ~=~ | affectation operator | |----------+----------------------| | ~+=~ | increment operator | | ~-=~ | decrement operator | | ~*=~ | multiply operator | | ~/=~ | divide operator | #+BEGIN_note It is important to note that in ~pugs~ language, affectation operators have *no* return value. This is a notable difference with ~C~ or ~C++~. Again, this is done to avoid common mistakes that can be difficult to address. For instance, the following ~C++~ code is valid but does not produce the expected result. #+BEGIN_SRC C++ :exports source bool b = true; // do things ... if (b=false) { // do other things ... } #+END_SRC Obviously the mistake is that the test should have been ~(b==false)~, since otherwise, the conditional block is never executed (in ~C++~, the result of an affectation is the value affected to the variable, which is always false in that case. This cannot happen with ~pugs~. A similar example #+NAME: no-affectation-result #+BEGIN_SRC pugs-error :exports both :results output let b:B, b=true; // do things if (b=false) { // do other things } #+END_SRC produces the following error #+results: no-affectation-result Actually, affectations are /expressions/ in ~C++~, in ~pugs~ language, affectations are /instructions/. #+END_note ***** List of defined operator ~=~ for basic types. As already mentioned, operator ~=~ is defined for *all* types in ~pugs~ if the right hand side expression has the *same* type as the left hand side variable. This is true for basic types as well as for high-level types. We now give the complete list of supported ~=~ affectations. The lists are sorted by type of left hand side variable. - ~B~: boolean left hand side variable. One is only allowed to affect boolean values. | ~=~ allowed expression type | |---------------------------| | ~B~ | - ~N~: natural integer ($\mathbb{N}$ or $\mathbb{Z}_{\ge0}$) left hand side variable. | ~=~ allowed expression type | |---------------------------| | ~B~ | | ~N~ | | ~Z~ (for convenience) | - ~Z~: integer ($\mathbb{Z}$) left hand side variable. | ~=~ allowed expression type | |---------------------------| | ~B~ | | ~N~ | | ~Z~ | - ~R~: real ($\mathbb{R}$) left hand side variable. | ~=~ allowed expression type | |---------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1~: vector of dimension 1 ($\mathbb{R}^1$) left hand side variable. | ~=~ allowed expression type | |---------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | | ~R^1~ | - ~R^2~: vector of dimension 2 ($\mathbb{R}^2$) left hand side variable. | ~=~ allowed expression type | |---------------------------------------------| | ~R^2~ | | ~0~ (special value) | | list of 2 scalar (~B~, ~N~, ~Z~ or ~R~) expressions | An example of initialization using a list or the special value ~0~ is #+NAME: affectation-to-R2-from-list #+BEGIN_SRC pugs :exports both :results output let u:R^2, u = (-3, 2.5); let z:R^2, z = 0; cout << "u = " << u << "\n"; cout << "z = " << z << "\n"; #+END_SRC which produces #+RESULTS: affectation-to-R2-from-list Observe that ~0~ is a special value and *treated as a keyword*. There is no conversion from integer values. For instance: #+NAME: R2-invalid-integer-affectation #+BEGIN_SRC pugs-error :exports both :results output let z:R^2, z = 1-1; #+END_SRC produces the compilation error #+results: R2-invalid-integer-affectation - ~R^3~: vector of dimension 3 ($\mathbb{R}^3$) left hand side variable. | ~=~ allowed expression type | |---------------------------------------------| | ~R^3~ | | ~0~ (special value) | | list of 3 scalar (~B~, ~N~, ~Z~ or ~R~) expressions | An example of initialization using a list is #+NAME: affectation-to-R3-from-list #+BEGIN_SRC pugs :exports both :results output let u:R^3, u = (-3, 2.5, 1E-2); let z:R^3, z = 0; cout << "u = " << u << "\n"; cout << "z = " << z << "\n"; #+END_SRC the output is #+RESULTS: affectation-to-R3-from-list - ~R^1x1~: matrix of dimensions $1\times1$ ($\mathbb{R}^{1\times1}$) left hand side variable. | ~=~ allowed expression type | |---------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | | ~R^1x1~ | - ~R^2x2~: matrix of dimension $2\times2$ ($\mathbb{R}^{2\times2}$) left hand side variable. | ~=~ allowed expression type | |---------------------------------------------| | ~R^2x2~ | | ~0~ (special value) | | list of 4 scalar (~B~, ~N~, ~Z~ or ~R~) expressions | An example of initialization using a list or the special value ~0~ is #+NAME: affectation-to-R2x2-from-list #+BEGIN_SRC pugs :exports both :results output let u:R^2x2, u = (-3, 2.5, 4, 1.2); let z:R^2x2, z = 0; cout << "u = " << u << "\n"; cout << "z = " << z << "\n"; #+END_SRC which produces #+RESULTS: affectation-to-R2x2-from-list - ~R^3x3~: matrix of dimension $3\times3$ ($\mathbb{R}^{3\times3}$) left hand side variable. | ~=~ allowed expression type | |---------------------------------------------| | ~R^3x3~ | | ~0~ (special value) | | list of 9 scalar (~B~, ~N~, ~Z~ or ~R~) expressions | An example of initialization using a list is #+NAME: affectation-to-R3x3-from-list #+BEGIN_SRC pugs :exports both :results output let u:R^3x3, u = (-3, 2.5, 1E-2, 2, 1.7, -2, 1.2, 4, 2.3); let z:R^3x3, z = 0; cout << "u = " << u << "\n"; cout << "z = " << z << "\n"; #+END_SRC the output is #+RESULTS: affectation-to-R3x3-from-list - ~string~ left hand side variable. Expressions of any basic types can be used as the right hand side. | ~=~ allowed expression type | |---------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | | ~R^1~ | | ~R^2~ | | ~R^3~ | | ~R^1x1~ | | ~R^2x2~ | | ~R^3x3~ | | ~string~ | ***** List of defined operator ~+=~ for basic types. - ~B~: the ~+=~ operator is not defined for left hand side boolean variables. - ~N~: natural integer ($\mathbb{N}$ or $\mathbb{Z}_{\ge0}$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ (for convenience) | - ~Z~: integer ($\mathbb{Z}$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | - ~R~: real ($\mathbb{R}$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1~: vector of dimension 1 ($\mathbb{R}^1$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~R^1~ | - ~R^2~: vector of dimension 2 ($\mathbb{R}^2$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~R^2~ | - ~R^3~: vector of dimension 3 ($\mathbb{R}^3$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~R^3~ | - ~R^1x1~: matrix of dimensions $1\times1$ ($\mathbb{R}^{1\times1}$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~R^1x1~ | - ~R^2x2~: matrix of dimension $2\times2$ ($\mathbb{R}^{2\times2}$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~R^2x2~ | - ~R^3x3~: matrix of dimension $3\times3$ ($\mathbb{R}^{3\times3}$) left hand side variable. | ~+=~ allowed expression type | |----------------------------| | ~R^3x3~ | - ~string~ left hand side variable. Expressions of any basic types can be used as the right hand side. | ~+=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | | ~R^1~ | | ~R^2~ | | ~R^3~ | | ~R^1x1~ | | ~R^2x2~ | | ~R^3x3~ | | ~string~ | ***** List of defined operator ~-=~ for basic types. - ~B~: the ~-=~ operator is not defined for left hand side boolean variables. - ~N~: natural integer ($\mathbb{N}$ or $\mathbb{Z}_{\ge0}$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ (for convenience) | - ~Z~: integer ($\mathbb{Z}$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | - ~R~: real ($\mathbb{R}$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1~: vector of dimension 1 ($\mathbb{R}^1$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~R^1~ | - ~R^2~: vector of dimension 2 ($\mathbb{R}^2$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~R^2~ | - ~R^3~: vector of dimension 3 ($\mathbb{R}^3$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~R^3~ | - ~R^1x1~: matrix of dimensions $1\times1$ ($\mathbb{R}^{1\times1}$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~R^1x1~ | - ~R^2x2~: matrix of dimension $2\times2$ ($\mathbb{R}^{2\times2}$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~R^2x2~ | - ~R^3x3~: matrix of dimension $3\times3$ ($\mathbb{R}^{3\times3}$) left hand side variable. | ~-=~ allowed expression type | |----------------------------| | ~R^3x3~ | - ~string~: the ~-=~ operator is not defined for left hand side string variables. ***** List of defined operator ~*=~ for basic types. - ~B~: the ~*=~ operator is not defined for left hand side boolean variables. - ~N~: natural integer ($\mathbb{N}$ or $\mathbb{Z}_{\ge0}$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ (for convenience) | - ~Z~: integer ($\mathbb{Z}$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | - ~R~: real ($\mathbb{R}$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1~: vector of dimension 1 ($\mathbb{R}^1$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^2~: vector of dimension 2 ($\mathbb{R}^2$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^3~: vector of dimension 3 ($\mathbb{R}^3$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1x1~: matrix of dimensions $1\times1$ ($\mathbb{R}^{1\times1}$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^2x2~: matrix of dimension $2\times2$ ($\mathbb{R}^{2\times2}$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^3x3~: matrix of dimension $3\times3$ ($\mathbb{R}^{3\times3}$) left hand side variable. | ~*=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | #+BEGIN_note Observe that for these small matrix types ($\mathbb{R}^{d\times d}$) the construction ~A *= B;~ is not allowed. The main reason for that is that for $d>1$ this operation has no interests since it requires a temporary. One will see bellow that one can write ~A = A*B;~ if needed. #+END_note - ~string~: the ~*=~ operator is not defined for left hand side string variables. ***** List of defined operator ~/=~ for basic types. - ~B~: the ~/=~ operator is not defined for left hand side boolean variables. - ~N~: natural integer ($\mathbb{N}$ or $\mathbb{Z}_{\ge0}$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ (for convenience) | - ~Z~: integer ($\mathbb{Z}$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | - ~R~: real ($\mathbb{R}$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1~: vector of dimension 1 ($\mathbb{R}^1$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^2~: vector of dimension 2 ($\mathbb{R}^2$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^3~: vector of dimension 3 ($\mathbb{R}^3$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^1x1~: matrix of dimensions $1\times1$ ($\mathbb{R}^{1\times1}$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^2x2~: matrix of dimension $2\times2$ ($\mathbb{R}^{2\times2}$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | - ~R^3x3~: matrix of dimension $3\times3$ ($\mathbb{R}^{3\times3}$) left hand side variable. | ~/=~ allowed expression type | |----------------------------| | ~B~ | | ~N~ | | ~Z~ | | ~R~ | **** Unary operators The ~pugs~ language allows the following tokens as unary operators | operator | description | |----------+----------------------| | ~not~ | not operator | | ~+~ | plus unary operator | | ~-~ | minus unary operator | |----------+----------------------| | ~++~ | increment operator | | ~--~ | decrement operator | |----------+----------------------| | ~[]~ | access operator | The ~not~, ~+~ and ~-~ operators apply to the *expression* on their right. ~++~ and ~--~ operators apply only to a *variable* that can be positioned before (post increment/decrement) or after the token (pre increment/decrement). These operators are also inspired from their ~C++~ counterparts for commodity. The ~+~ unary operator is a convenient operator that is *elided* when parsing the script. For basic types, when these operators are defined, they return a value of the same type as the argument (except if the argument is a ~N~, then the result is a ~Z~). These operators can be defined for high-level types. - The ~not~ operator is only defined for boolean values (~B~). - The ~-~ unary operator is defined for numeric basic types: ~B~, ~N~, ~Z~, ~R~, ~R^1~, ~R^2~, ~R^3~, ~R^1x1~, ~R^2x2~ and ~R^3x3~. It is not defined for ~string~ variables. - Pre and post increment operators, ~--~ and ~++~, are defined for all scalar basic types: ~N~, ~Z~ and ~R~. They are not defined for ~B~, ~R^1~, ~R^2~, ~R^3~, ~R^1x1~, ~R^2x2~, ~R^3x3~ and ~string~ variables. Note that the pre increment/decrement operators behave slightly differently than their ~C++~ counterparts since they are not allowed to be chained. In ~C++~, the following code is allowed #+BEGIN_SRC C++ :exports source int i = 0; int j = ++ ++i; #+END_SRC In ~pugs~, it is forbidden: #+NAME: double-pre-incr-result #+BEGIN_SRC pugs-error :exports both :results output let i:N, i=0; let j:N, j = ++ ++i; #+END_SRC produces the compilation error #+results: double-pre-incr-result Again, this is done to simplify the syntax and to avoid weird constructions. - Access operators are only defined for small vectors ~R^d~ and small matrices ~R^dxd~. To avoid use of uninitialized variables (or partially uninitialized variables), these are ~read-only~ access operators. Their syntax is the following. #+NAME: Rd-Rdxd-access-operator #+BEGIN_SRC pugs :exports both :results output let x:R^2, x = (1,2); let A:R^3x3, A = (1,2,3,4,5,6,7,8,9); cout << "x[0] = " << x[0] << "\nx[1] = " << x[1] << "\n"; cout << "A[0,0] = " << A[0,0] << "\nA[2,1] = " << A[2,1] << "\n"; #+END_SRC This code produces #+results: Rd-Rdxd-access-operator **** Binary operators Syntax for binary operators follows again a classical structure if ~exp1~ and ~exp2~ denotes two expressions and if ~op~ denotes a binary operator, one writes simply ~exp1 op exp2~. Here is the list of binary operators | keyword | operator | |---------+-----------------------| | ~and~ | logic and | | ~or~ | logic and | | ~xor~ | logic exclusive or | |---------+-----------------------| | ~==~ | equality | | ~!=~ | non-equality | | ~<~ | lower than | | ~<=~ | lower than or equal | | ~>~ | greater than | | ~>=~ | greater than or equal | |---------+-----------------------| | ~<<~ | shift left | | ~>>~ | shift right | |---------+-----------------------| | ~+~ | sum | | ~-~ | difference | | ~*~ | product | | ~/~ | division | Binary operators can be defined for high-level types. For basic types, they follow a few rules. - Logical operators ~and~, ~or~ and ~xor~ are defined for boolean operands (type is ~B~) only. The result of the expression is a boolean. #+begin_src latex :results drawer :exports results \begin{equation*} \left| \begin{array}{rl} \mathtt{and}:&\quad \mathbb{B} \times \mathbb{B} \to \mathbb{B}\\ \mathtt{or}:& \quad\mathbb{B} \times \mathbb{B} \to \mathbb{B}\\ \mathtt{xor}:& \quad \mathbb{B} \times \mathbb{B} \to \mathbb{B} \end{array} \right. \end{equation*} #+end_src - Comparison operators ~==~, ~!=~, ~<~, ~<=~, ~>~ and ~>=~ are defined for all basic scalar type and return a boolean value. #+begin_src latex :results drawer :exports results \begin{equation*} \forall \mathbb{S}_1, \mathbb{S}_2 \in \{\mathbb{B},\mathbb{N},\mathbb{Z},\mathbb{R}\}, \quad \left| \begin{array}{rl} \mathtt{==}:& \mathbb{S}_1 \times \mathbb{S}_2 \to \mathbb{B}\\ \mathtt{!=}:& \mathbb{S}_1 \times \mathbb{S}_2 \to \mathbb{B}\\ \mathtt{<}: & \mathbb{S}_1 \times \mathbb{S}_2 \to \mathbb{B}\\ \mathtt{<=}:& \mathbb{S}_1 \times \mathbb{S}_2 \to \mathbb{B}\\ \mathtt{>}: & \mathbb{S}_1 \times \mathbb{S}_2 \to \mathbb{B}\\ \mathtt{>=}:& \mathbb{S}_1 \times \mathbb{S}_2 \to \mathbb{B} \end{array} \right. \end{equation*} #+end_src When comparing a boolean value (type ~B~) with another scalar value type (~N~, ~Z~ or ~R~), the value ~true~ is interpreted as $1$ and the value ~false~ as $0$. \\ For vector and matrix basic types, the only allowed operators are ~==~ and ~!=~. #+begin_src latex :results drawer :exports results \begin{equation*} \forall d \in \{1,2,3\},\quad \left| \begin{array}{rl} \mathtt{==}:& \mathbb{R}^d \times \mathbb{R}^d \to \mathbb{B}\\ \mathtt{==}:& \mathbb{R}^{d \times d} \times \mathbb{R}^{d \times d} \to \mathbb{B}\\ \mathtt{!=}:& \mathbb{R}^d \times \mathbb{R}^d \to \mathbb{B}\\ \mathtt{!=}:& \mathbb{R}^{d \times d} \times \mathbb{R}^{d \times d} \to \mathbb{B} \end{array} \right. \end{equation*} #+end_src \\ This is also the case for ~string~ values: only allowed operators are ~==~ and ~!=~. #+begin_src latex :results drawer :exports results \begin{equation*} \left| \begin{array}{rl} \mathtt{==}:& \mathtt{string} \times \mathtt{string} \to \mathbb{B}\\ \mathtt{!=}:& \mathtt{string} \times \mathtt{string} \to \mathbb{B} \end{array} \right. \end{equation*} #+end_src - Shift operators ~<<~ and ~>>~ are not used to define binary operators between two basic types - Arithmetic operators are defined *** TODO High-level types<<high-level-types>> *** TODO Lists *** TODO Tuples types ** TODO modules ** TODO Statements *** if/else *** for loops *** do while loops *** while loops *** break/continue ** TODO Functions<<functions>> *** Pure functions *** Implicit type conversion for parameters and returned values *** User-defined functions *** Builtin functions [fn:pugs-def] ~pugs~: Parallel Unstructured Grid Solvers [fn:MPI-def] ~MPI~: Message Passing Interface [fn:DSL-def] ~DSL~: Domain Specific Language