Puc Programmer Guide

1. Introduction

If you want to develop Puc programs, you should know details about Puc, especially the difference between Puc and C. And you should learn the usage of cc0 which compiles your Puc program into D-CISC binary code. To understand how your program works, you should know the brief mechanism of D-thinker.

The libi0 library provides supports for Puc programs to use supported functionalities of D-thinker. You can get a copy of libi0 first following this introduction.

2. Learning Advice

Here are some advices about learning how to develop programs on D-thinker.

  1. Homepage of D-thinker provides an introduction and some documents. One of the most important documents is the d-book, which aggregates most documents about D-thinker.

  2. If you have any questions or confusions, you can go to D-thinker forum. Researchers and developers of D-thinker will help you kindly.

  3. You can refer to Puc manual to know more detailed information about the Puc syntax.

  4. To get started quickly, you can follow two Puc tutorials to develop your first program. I highly recommend you do it step by step before reading this document.

3. Special Syntax of Puc

3.1. Supported Special Syntax

There is some special syntax in Puc that is different from those in C.

3.1.1. standalone

standalone is a special keyword of Puc. It is for global variables which are in the Shared Region (SR). When a global variable is standalone, it always monopolizes one or many memory pages. The main purpose of using standalone is to reduce commit conflicts on different global variables among multiple concurrent tasks, since the basic unit of memory space management is a page.

standalone long a;
standalone long b;

For the above example, if runner A modify a and runner B modify b, there will be no commit conflict.

3.1.2. watching

If a runner creation statement has a watching statement, the runner is a special runner named watcher. This watcher will be scheduled when at least one of the watched variables are modified and committed.

long a;
long b[10];

void func()
{
    a = 2;
    b[3] = 5;
    commit;
}

void watcher_func()
{
    ...
    commitd;
}
void main()
{
    runner func()
        using a, b[0,, 10];
    runner watcher_func()
        watching a, b[0,,10];
    commit;
}

For the above example, when a and b[3] are modified by func() and func() commits, watcher_func() will be ready to be scheduled for execution.

3.1.3. Array segment

An array segment is logically the same as an array (or a pointer). However, it restricts the access of elements to a specified range. The array segment is represented as array[start,,end], the start is inclusive and end is exclusive.

The code example in the above section for creating runner func() already uses the array segment b[0,,10] which means that elements a[0] to a[9] will be included in runner func()'s snapshot and func() can uses these elements.

A multi-dimentional array is very similar to an one dimentional array if you chcked how the C compilers generate the binary code for it.

Suppose you need an array: long A[N][M]. You can emulate it by a one dimentional array: long B[N*M]. A[][y] can be emulated by B[M*x + y].

3.1.4. using

As the memory model in Puc is different from the one for c, how global variables are accessed is different in Puc from that in C. In C, any function can access the global variables. However, in Puc, your runner cannot access a global variable acquiescently, unless you declare a using statement to permit this runner to access the global variable, like a and b[0,,10] in the following example.

long a;
long b[10];

void func()
{
    a = 2;
    b[3] = 5;
    commit;
}

void main()
{
    runner func()
        using a, b[0,, 10];
    commit;
}
3.1.5. main()

main() is a simple runner without any memory range in its heap. It can be considered created as a runner without the using statement. This means that main() cannot use any global variable.

long a;

void main()
{
    a = 2; // will cause errors;
    commit;
}
3.1.6. When runner starts

When executing runner func() statement in a current runner, the new runner is created but will not be started immediately. You have to commit the current runner first. Then you also have to wait for the scheduler to schedule the new runner to execute.

3.1.7. shalloc() & pralloc()

D-thinker supports dynamic memory allocation through the shalloc and pralloc mechanisms.

Programmers can use library functions shalloc() and shalloc_ext() to allocate memory in shared region, and pralloc() to allocate memory in private region.

Their prototypes are as follows.

// The argument size is based on byte.
void *shalloc(long size)
void *shalloc_ext(long size, shalloc_option_t option)
void *pralloc(long size)

You can use shalloc_ext() to allocate memory in different regions of shared region using corresponding options. There are 6 options currently: SR_N, AMR_N, AMR_P_NONREP, AMR_P_REP, PMEM_N_REP and PMEM_N_NONREP.

  • 'SR' means the general region.
  • 'AMR' means the Aliased Memory Region.
  • 'PMEM' means the persistent memory region.
  • 'N' means non-persistent/non-AMR. In a macro with PMEM/AMR prefix, it loses the meaning for non-persistent/non-AMR).
  • 'P' means persistent.
  • 'REP'/'NONREP' are for replicated/non-replicated.

The memory size you can allocate using shalloc() or shalloc_ext() is half the size of each regions by default. Refer this thread for more details about the memory layout and their options. Notice shalloc(size) is the same with shalloc_ext(size, SR_N).

Here is an example using shalloc_ext:

// include the header malloc.h from libi0
#include <malloc.h>

// the runner who receives data[0,,100]
void rev_runner(long* data, long size)
{
    // You can use data[0,,100] here.
    // ...

    commit;
}

void exec()
{
    long size;
    long *data;

    // Set allocated size of long.
    size = 100; //You can also read it from standard input.

    // Input argument is based on byte. Each long variable occupies 8 byte.
    // data[0,,100] is in the shared region.
    // Hence, it can be used in the whole lifecycle of the program.
    data = (long*)shalloc_ext(size * 8, SR_N);

    // You can use data[0,,100] here.
    // ...

    // If you want to transmit data[0,,100] to other runner
    runner rev_runner(data, size)
      using data[0,,size];

    commit;
}

void main()
{
    runner exec();
    commit;
}

You can use pralloc() to allocate memory in private region in D-thinker. The total memory size you can allocate using pralloc() is _PA_SIZE, which is defined in the malloc.h.

Here is an example using pralloc:

// include the header malloc.h
#include <malloc.h>

// the func who uses data[0,,100]
void other_func(long* data, long size)
{
    // You can use data[0,,100] here.
    // ...

    return;
}

void exec()
{
    long size;
    long *data;

    // Set allocated size of long.
    size = 100; //You can also read it from standard input.

    // Input argument is based on byte. Each long variable occupies 8 byte.
    // data[0,,100] is in private memory.
    // Hence, it can only be used in the lifecycle of this runner.
    data = (long*)pralloc(size * 8);

    // You can use data[0,,100] here.
    // ...

    //If you want to transmit data[0,,100] to other function
    other_func(data, size);

    commit;
}

void main()
{
    runner exec();
    commit;
}

You can read more advanced programs using shalloc or pralloc: smatmul, shalloc_writer_reader, shalloc_ext_writer_reader and pralloc_writer_reader.

3.2. Unsupported C Syntax

Some features or syntax is not yet supported in cc0. Here is a list of syntax that you should not use during programming using Puc with cc0.

  1. Do not support ++ or -- operators. Sometimes you will get strange error info, like Can not find main function.
  2. Do not support % operator.
  3. Do not support compound operators. Such as +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=, etc.
  4. Do not support logicical operators. Such as &&, ||, !.
  5. Do not support conditional operators (x?y:z).
  6. Do not support Shift operators. Such as <<, >>. Use *2 and /2 instead.
  7. Do not support initialization on defining. For example,

     void function()
     {
         // long a = 2;
         long a;
         a = 2;
     }
    
  8. Do not support automatic type casting. You must explicitly specify the type casting.

  9. The operations of float may have some bugs. But double can be used instead.
  10. main() does not support arguments, such as main(int argc, char *argv[]) as in C.
  11. Do not support bool, true and false.
  12. Do not support the sizeof keyword. But you can use some macros in the stdint.h instead.

    #include <stdint.h>
    
    uint64_t
    int64_t
    
    sizeof_long
    sizeof_double
    
    sizeof_uint64_t
    sizeof_int64_t
    
    sizeof_char
    
  13. Do not support struct. You have to use other ways to simulate struct. For example,

    //struct SA {long a; char b};
    //struct SA sa_array[5];
    #define SIZEOF_SA_STRUCT sizeof_long + sizeof_char
    char sa_array[SIZEOF_SA_STRUCT * 5];
    
    //sa_array[3].a;
    //sa_array[3].b;
    #define sa_get_pa(n) (long*)&sa_array[n * SIZEOF_SA_STRUCT]
    #define sa_get_pb(n) (long*)&sa_array[n * SIZEOF_SA_STRUCT + sizeof_long]
    *sa_get_pa(3);
    *sa_get_pb(3);
    
  14. Do not support subtraction between pointers directly. But you can cast pointer type into long type for computation and the cast it back to a pointer. You can also use array index to simulate operations between const number and pointer. For example,

    char *a;
    char *b;
    ...
    //if(a - b > 0)
    //{ ... }
    if((long)a - (long)b > 0)
    { ... }
    
    long *c;
    long *d;
    long count;
    //d = c + 3;
    d = &c[3];
    //count = d - c;
    count = ((long)d - (long)c) / 8;
    
  15. Add or sub a pointer by a const value is a trap. Actually, it is a bug in cc0. You should pay special attention to it. For example:

    long array[100];
    long *a;
    a = &array[0];
    *(a+8); //The value equal with a[1], rather than a[8].
    //Here, we assume sizeof(long) is 8.
    

    In C, *(a+8) equals a[8]. In Puc compiled by cc0, *(a+8) equals a[1]. This bug should be fixed in the future. Currently, if you need the address of a[1], just use &a[1]. However, you would better not mix it up with the index of using/watching.

    long array[100];
    voidmain()
    {
        ...
        runner func()
            using a[0,, 100]
            watching a[0,, 100];
        ...
    }
    

    a[0,, 100] here means set using/watching range as a[0] to a[100]. a[0,, 100*8] is wrong. If you write it like this, the behavior is undefined.

  16. Do not omit return statement even though the function returns void. For example,

    void func()
    {
        ... //do something
    
        return;//Never omit this, otherwise it causes strange error information.
    }
    
  17. If the function arguments and local variables have the same name, cc0 does not report the error. cc0 currently follows the C89 standard and the behavior is undefined for this situation.

  18. The declaration of local variables should be at the beginning of a scope block. The declaration position follows the rules of ANSI C or C89, rather than C99.

    void func()
    {
        long a;
        a = 2;
        //long b;
    
        ...
        return;
    }
    
  19. The name, such as arr of char arr[10], is not the address of the first element. This is a bug of the compiler. To get the address of the array, just get the address of the first element, such as &arr[0].

4. Input & Output

There are 2 IO methods in D-thinker currently.

4.1. Standard IO

Standard output of your program will be collected and stored in files after execution on the portal machine and the am command will print out the location of them. During the execution, you can use the command am stdout to check the latest last several lines of the stdouts on each VPC.

Standard input is also files distributed in all the machines. You can specify the stdin file when you run your program by dt run your_program.bin stdin_file.

There are some functions that can be used in libi0/stdio.h for stdin/stdout.

4.2. Persistent Memory

Persistent memory is another IO method in D-thinker. You can read or write the persistent memory ranges as normal memory ranges. But all the data in the persistent memory ranges will be persistent in D-thinker.

5. How to debug

Debug is an important part of development.

5.1. Debug mode of cc0

Similar to the GNU compiler, you can add the --debug option to get intermediate files.

$ cc0 example.c0 -o example.bin –debug

4 intermediate files will be generated: example.map, example.il, example.opt.il, example.objdump. You can see memory address map and D-CISC instructions in them.

5.2. Runtime State

When you use dt run command to run a program, besides of the info printed out on the console, you can check 3 kinds of logs to see the runtime state.

  1. Scheduler Log

     $ cat $thinker_tmp/scheduler.log
    

    where $thinker_tmp is a variable in the D-thinker’s configuration.

  2. Home Log

    The log of the home is in $thinker_tmp/home.log on the home node.

  3. VPC Logs

    You can check the latest several lines of the VPC logs by

     $ dt status
    

    The VPC log can show that some problems happened. For example, you may find a VPC’s log showing the following info

     ...
     [1350546034.549495s] #DSM: page is not found in PT (addr: 440000000)
     [1350546034.549502s] dsm handler page_addr:440000000, addr:440000008. %rip:
     1100102ea
    

    It means that your program tries to access an illegal memory range at address (0x440000008). In this case, you have to type Ctrl-C to stop your program on the portal.

    Sometimes, a message like

     -------------------------
     10.1.0.101:3048 (slot 0)
     ERROR: no vpc running: 10.1.0.101:3048 (slot 0)
    

    also suggests that your program may try to access an illegal memory range.

5.3 Output Debug

You can print some state of your program to STDOUT in your program for debugging using the stdout mechanisms introduced.

You can visit www.d-thinker.org for more Guides.