Tutorial:Beginner's tutorial

From Bennu Wiki
(Redirected from Beginner's tutorial)
Jump to: navigation, search


This Beginner's tutorial aims to help beginning programmers to get to know Bennu. In its current state, the tutorial is reasonably advanced already, so if you don't understand something, please leave a note. Any comments or suggestions are also welcome.

Contents

The Beginning

Bennu, like every compiled language, has a compiler and an interpreter. The first one is used to convert the source file, the code that you write, to a file that the interpreter can understand. In this tutorial we are going to concentrate on the source file and move on from there.

Source File

A source file is simply a text file that respects certain syntax. This file can be created in any text editor, like the windows notepad. Bennu does not have an official IDE but there are patches and tutorials to make some free IDEs ready for Bennu (see Setting up Bennu).

For the sake of standardization there a few file extensions used primarily for Bennu sourcefiles:

  • .prg: program code
  • .inc: included code; code that is not meant to run stand-alone; header file (compare with C's .h).

Here's a basic source file:

import "mod_say" // import the module to output text to console, using say()

Process Main() // start the definition of the main process
Begin // start the code
    say("Hello World!");
End // end the definition of the main process

Used in example: mod_say, import, Process, Begin, End, say()

From Hello World

Introduction to processes

Like many other languages, Bennu has functions. Functions can be seen as parts of code you can run with only a single statement (the function call). The caller will wait for the function to finish. Let's clarify this with an example.

import "mod_say" // import the module to output text to console, using say()

Function int SayHelloWorld() // start the definition of a function called SayHelloWorld
Begin // start the code
    say("Hello World!");
End // end the definition of the function SayHelloWorld

Process Main() // start the definition of the main process
Begin // start the code
    SayHelloWorld();
    say("Hello again!"); // AFTER SayHelloWorld() is done
End // end the definition of the main process

Used in example: mod_say, import, Function, Process, Begin, End, say()

Here, we see the caller (Main and the called function SayHelloWorld).

Now you may be wondering why there are functions and processes. Processes are almost the same as functions, but with a slight difference we will discuss shortly. Try to run the same code, but making SayHelloWorld a process:

import "mod_say" // import the module to output text to console, using say()

Process int SayHelloWorld() // start the definition of a function called SayHelloWorld
Begin // start the code
    say("Hello World!");
End // end the definition of the function SayHelloWorld

Process Main() // start the definition of the main process
Begin // start the code
    SayHelloWorld();
    say("Hello again!"); // AFTER SayHelloWorld() is done
End // end the definition of the main process

Used in example: mod_say, import, Process, Begin, End, say()

You will notice it doesn't make a difference here.

More about processes

Bennu has a unique way of programming, because it makes use of processes and frames. Frames can be seen as a way of controlling the speed of your application. For example, you can tell Bennu to run at 60 frames per second, 100 frames per second or even 1000 frames per second. Of course, if the machine can't handle it, 1000 FPS won't be reached. In each frame, every process performs its code and advances a frame.

To have processes run in parallel, just call it like you would a function. When a process is called, it causes the caller to wait for the process, also like a function. However, when the process reaches a frame; statement, it tells the caller it can continue and this is where the parallelism begins. It is important to note that it is not defined in what order the processes will be run after this. In most cases it is the order in which the processes were started, but that is not guarenteerd. Let's see this in action.

import "mod_say"

Process int SayHelloWorld()
Begin
    say("SayHelloWorld: 1"); // this gets run in the first frame
    frame; // wait for the second frame to start
    say("SayHelloWorld: 2"); // this one in the second frame
    frame; // wait for the third frame to start
    say("SayHelloWorld: 3"); // this one in the third frame
End

Process Main()
Begin
    SayHelloWorld();
    say("Main: 1"); // this gets run in the first frame and AFTER the first one SayHelloWorld
    frame; // wait for the second frame to start
    say("Main: 2"); // this one in the second frame, BEFORE the one in SayHelloWorld
    frame; // wait for the third frame to start
    say("Main: 3"); // this one in the third frame, BEFORE the one in SayHelloWorld
End

The output is:

SayHelloWorld: 1
Main: 1
Main: 2
SayHelloWorld: 2
Main: 3
SayHelloWorld: 3

In the first frame, Main waits for SayHelloWorld, so the say() in SayHelloWorld is executed before the one in Main. After that, the processes run in turns, in this case Maingets his turn before SayHelloWorld. However, it is possible to influence the order of execution, but more on that later.

Frames

Bennu's interpretation works with frames. You can limit the execution rate by limiting the allowed amount of frames per second. To tell Bennu a process is done for the current frame, the statement frame; can be used. Multiple processes will take turns in getting executed and will not be executed simultaneously. For the more advanced people reading this, this is indeed a lot like coroutines, where instead of frame, the keyword yield is used; In Bennu you can also use yield as you would use frame.

import "mod_say"
import "mod_time" // import a time module, for get_timer(), which returns the number
                  // of milliseconds after the program was started
Process Main()
Begin
    while(get_timer()<1000)
        say("Hello World! @" + get_timer());
        frame;
    end
End

Used in example: mod_say, mod_time, while, frame, say(), get_timer()

This example will output Hello World! @... as many times as possible in one second.

But suppose we were to limit the FPS to 25, then it would only output it 25 times, spread evenly over the second it ran. This would be pretty useful in cases. Currently, this is done by importing mod_video and using set_fps(). This means that each Bennu-frame the mod_video module 'tells' Bennu to wait until the next frame should start. For example if the FPS is 25, the time between the start of frames is 40ms. If the execution of Bennu took 10ms, mod_video will tell Bennu to wait 30ms with execution, making sure the FPS is correct.

Processes and functions

One of the key features of Bennu is the use of processes. The code in each process runs independently from each other, although never at the same time, but the execution rate is influenced by the frame statement. A frame-statement in a process says something like "this process is done for this frame" and the process will wait (at the frame-statement) for the next frame to begin and the process acquires running privileges again.

A function is almost the same, but not entirely independent. The process or function calling a function will wait for the function to return even if that function contains a frame-statement. A good example is the textinput tutorial.

Processes and functions are given a unique identification number when they are created. You can use this to manipulate its execution (using signal), access local variables and its public variables (using the . operator). We will discuss more on this later.

Now let's create another process:

import "mod_say"
import "mod_time"
import "mod_proc" // import a process manipulator module, for let_me_alone()

Process Another() // start the definition of another process
Begin // start the code
    Loop // endlessly say something
        say(get_timer() + " - Another");
        frame;
    End
End // end the definition of another process

Process Main()
Begin

    Another(); // start another process

    while(get_timer()<1000)
        say(get_timer() + " - Main");
        frame;
    end

    let_me_alone(); // kill ALL processes EXCEPT this one

End

Used in example: mod_say, mod_time, mod_proc, say(), get_timer(), let_me_alone()

Notice that Main and Another are executed one after the other. If we were to limit the FPS to 25, they both would run at 25FPS, one after the other.

Multiple source files

If you want to use multiple sources files - which you want for larger projects, you are able to accomplish this by using the include statement.

Consider the last example. We'll cut it up in a .prg and a .inc.

example.prg:

import "mod_say"
import "mod_time"
import "mod_proc"

include "example.inc" // include example.inc: it will be as if the contents of
                      // example.inc were located at this point in example.prg
Process Main()
Begin

    Another();

    while(get_timer()<1000)
        say(get_timer() + " - Main");
        frame;
    end

    let_me_alone();

End

example.inc:

import "mod_say"  // we use these modules in this file, so it is good practise
import "mod_time" // to (also) include them here

Process Another()
Begin
    Loop
        say(get_timer() + " - Another");
        frame;
    End
End

Variables and datatypes

Variables

Basics

There are multiple scopes of variables in Bennu:

  • Global variables are variables available throughout the code (from the point of declaration)
  • Constants are like global variables, but their value cannot be changed.
  • Private variables are variables only available to the process or function in which they were declared.
  • Public variables are like private variables, but they can also be accessed outside of the owning process.
  • Local variables are like public variables, but they apply to all processes instead of a single one.

Bennu has various default types of variables:

type - can contain
int (32bits) - -2147483648..2147483647 (only whole numbers)
dword (unsigned int) - 0..4294967295 (only whole numbers)
short (16bits) - -32768..32767 (only whole numbers)
word (unsigned short) - 0..65535 (only whole numbers)
char (8bits) - -127..128 (only whole numbers)
byte (unsigned char) - 0..255 (only whole numbers)
float (32bits float) - almost any real number
string - text

To declare global variables, use Global:

Global
    int i;
    float f;
End

To declare constants, use Const. Notice that there is not a datatype declared:

Const
    i = 0;
    f = 0.0;
End

To declare private variables, use Private inside a process or function definition:

Process myprocess()
Private
    int i;
    float f;
End
Begin
End

To declare public variables, use Public inside a process or function definition:

Process myprocess()
Public
    int i;
    float f;
End
Begin
End

To declare local variables, use Local:

Local
    int i;
    float f;
End

Arrays, Structs and pointers

We can also use the basic types to form arrays, structs and pointers. Arrays are formed using brackets:

Global // or somewhere else it is allowed to declare variables
    int my_array_of_ten_integers[9]; // this makes an array of ten integers 0..9
End

We can access these integers like this:

my_int = my_array_of_ten_integers[0];
my_index = 9;
my_int = my_array_of_ten_integers[my_index];

Structs are formed using the keyword struct:

Global // or somewhere else it is allowed to declare variables
    Struct MyStruct // makes a struct with two members
        int i;
        float f;
    End
End

We can access members by using the [[.]] operator:

my_int = MyStruct.i;
my_float = MyStruct.f;

We can also make an array of structs:

Global // or somewhere else it is allowed to declare variables
    Struct MyStruct[9] // makes ten structs
        int i;
        float f;
    End
End

Pointers are variables which can 'point' to variables. They actually hold the address where that variable is located. If for some reason you move the variable (maybe because you used realloc()), the pointer no longer points to the variable. This is important to understand: the pointer points to a memory address. The operators * and & are used when dealing with pointers:

import "mod_say"

Global
    int my_int;
    int* my_int_pointer;
End
Process Main()
Begin
    my_int = 4;
    my_int_pointer = &my_int; // the & operator returns the address of the variable
    say("my_int: " + my_int);
    say("my_int_pointer: " + my_int_pointer);
    say("*my_int_pointer: " + *my_int_pointer); // the * operator dereferences the variable and returns the contents
End

Output:

my_int: 4
my_int_pointer: 003FD134
*my_int_pointer: 4

So you see how you can use pointers. If you don't understand them yet or don't see their use, just forget about them for now, but remember they are there for when you need them.

Usermade datatypes

Making your own types can be useful at times. In Bennu, your own types are structs, meaning they can hold multiple variables. You can read more about it here. A quick example:

Type Point
    float x;
    float y;
End

Global
    Point A,B;
End

Process Main()
Begin
    A.x = 0;
    A.y = 1;
    B.x = A.x;
    B.y = 6;
End

ProcessID, Public and Local

A process or function returns its ID when it reaches a frame-statement. We can assign this to a variable and use it later on.

We could rewrite the earlier example to this:

import "mod_say"
import "mod_time"
import "mod_proc" // import a process manipulator module, for signal()

Process Another()
Begin
    Loop
        say(get_timer() + " - Another");
        frame;
    End
End

Process Main()
Private
    Another a;
    int b;
End
Begin

    a = Another(); // start another process
    b = Another(); // start another process

    while(get_timer()<1000)
        say(get_timer() + " - Main");
        frame;
    end

    signal(a,S_KILL); // kill a
    signal(b,S_KILL); // kill b

End

You might wonder what the difference is between Another a and int b. The difference is that when you make a variable with the datatype the name of a process, Bennu can use this information to address public variables. Bennu does not know the type of process b is holding, so it can't access its public variables, because it can't know it has any. Locals however apply to all processes, so they can be accessed with both a and b.

Consider:

import "mod_say"
import "mod_time"
import "mod_proc"

Local
    int loc = 1;
End

Process Another()
Public
    int pub = 2;
Begin
    Loop
        frame;
    End
End

Process Main()
Private
    Another a;
    int b;
Begin

    a = Another(); // start another process
    b = Another(); // start another process

    say("a.loc = " + a.loc);
    say("b.loc = " + b.loc);
    say("a.pub = " + a.pub);
    //say("b.pub = " + b.pub); // not possible

    signal(a,S_KILL); // kill a
    signal(b,S_KILL); // kill b

End

Moving Up

Arguments, Parameters and Return

You can pass processes and functions information using parameters. They can also return a value. When the process or function reaches a frame-statement, its processID is returned, which can be saved in a variable, or otherwise used. If it reaches a return statement first, then it will return the value supplied with the return statement. If return; is used, or the process or function reaches neither a return-statement or a frame-statement, the ID is also returned. This sums up to: a process or function returns its ID, unless otherwise specified.

For example:

import "mod_say"

Process Main()
Begin
    say("multiply(3,4) = " + multiply(3,4));
End
Function int multiply(int a, int b)
Begin
    return a*b;
End

And:

import "mod_say"
import "mod_proc"

Process Main()
Begin
    say("an object with ID: " + object());
    say("an object with ID: " + object());
    say("an object with ID: " + object());
    let_me_alone();
End
Process object()
Begin
    Loop
        frame;
    End
End

Declare

Sometimes, you want to know how a process is declared before it is defined. When you declare something, it is not yet defined, but if you define something, it is declared. It is good practice to only use things that are declared when used, otherwise you could get lost by tracing weird bugs you caused. But what if you want to call process A inside process B and the other way around? You would have to declare both processes beforehand, using Declare:

Declare Process A()
End
Declare Process B()
End

Process A()
Begin
    B();
End
Process B()
Begin
    A();
End

You may have already noticed that this code is pretty bad: A will call B, B will call A, A will call B, etc.

You can use Declare for private variables and public variables too:

Declare Process A()
    Public
        int i;
    End
End
Declare Process B()
    Public
        float f;
    End
End

Process A()
Private
    B b;
Begin
    b = B();
    b.f = 5;
End

Process B()
Begin
   f = 1;
   say("f: " + f);
   frame; // let A continue
   say("f: " + f);
End

Strings

Strings are a special kind of datatype. They could be described as a dynamic array of characters, meaning that the length of the characters can vary. Strings are mostly used for holding text.

Const
    MSG_ERROR = "Oops, error occured!";
    MSG_SUCCESS = "Success!";
End

Process Main()
Private
    int error; // doesn't really do anything
Begin
    if(error)
        say(MSG_ERROR);
    else
        say(MSG_SUCCESS);
    end
End

Used in example: string, say()

Of course, you can do stuff with strings too. Let a and b be strings: a + b = the concatenation of a and b ("ab"+"cd"="abcd"). More functionality for strings can be found in mod_string.

Goto Labels

Further reading

language:

  • basic statements

modules:

  • angle,x,y,z,graph,pi etc with processes (make this article!)
  • fpg,fgc: graphics libraries (make this article!)
  • blendops (this one is somewhere)
  • graphics article (make this article!)
  • text
  • window manager

actually every module should have an article about it, telling what's it for