Context for PR_DSL

How does PR_DSL fit into DSL implementation?

One thing that happens in DSL design is determining what the types and values are that will be manipulated by the language and what the operations will be on those values. As a language implementer it is my job to figure out what these operations translate to in some target language. In the case of PR_DSL, that target language is the combination of C + PR_DSL, perhaps with PR_DKU as an additional target to provide dynamic task-sizing..

PR_DSL just has a small number of constructs (calls), which we are figuring out in the course of this collaboration. The starting list is:

  • PR_DSL__create_task
  • PR_DSL__attach_propendent_to_task
  • PR_DSL__attach_dependent_to_task
  • PR_DSL__wait_for_descendent_tasks_to_complete
  • PR_DSL__wait_for_target_task_to_complete

All the DSL constructs will be translated either into straight C, or into C plus some combination of PR_DSL calls.

As a side-note, within proto-runtime work, it is common to call a particular runtime system plus its wrapper library as a "programming language" or "proto-language". This brings up the question: what is the definition of "programming language"?

This page discusses what drove me to call such things programming languages. In short, I've taken to thinking of a programming language as the embodiment of the semantics, with a syntax and grammar front-end interface. Looked at that way, it's natural to think of a function call API as a perfectly valid interface, acting as the equivalent of a more traditional syntax, while the grammar is the valid combinations of calls that a user is responsible for respecting. Hence, even though PR_DSL behavior is only invoked via calls to its wrapper-library, I think of it as a programming language. The discussion page points out the subtleties and gray areas around choices of definition of "programming language".

More about proto-runtime, and why make a PR_DSL

From past experience, I realise that the concepts involved with proto-runtime take some getting used to. For example, proto-runtime is something that is "incomplete" in the sense that it is incapable of doing anything without behavior being plugged in. What proto-runtime provides is the time-related aspects around that behavior.

So, to shield DSL implementors from having to learn those concepts, we provide PR_DSL, as a full proto-language. That way, DSL implementors just target PR_DSL constructs, and don't have to worry about implementing their own proto-runtime plugins. For some DSLs, their execution model will be such that they don't map onto PR_DSL so well.. for them, PR_DSL might still serve as a first baby step, and then custom proto-runtime constructs will be created that directly implement the execution model of the DSL.

For those who _really_ want to learn about proto-runtime, here's a link to some wiki pages that explain how to download a proto-runtime project and get it working.. however, based on past experiences, I suspect too much of it will be "mystical" and confusing, and so counter-productive. But, here it is: (just to clarify, this project uses version 1.0 of proto-runtime, which was called "VMS".. it is primitive compared to where things are now.. but that makes it a good start for learning.. the application in the project is blocked matrix multiply, and the language is "SSR", which is Synchronous Send-Receive.. a toy language I created in order to illustrate using proto-runtime..)

"Hello World" Sample Code

Now, here is some code written in a simple custom language, followed by what the language tool will translate that code into.

PR_DSL__create_task( &taskBirthFn, ptrToTaskParams);

   ------- Original Source, using custom DSL syntax -------
   int size = 1000;
   int data[];
   data = makeArray( size );

   IterateIndependently i in 1:size
    { data[i] = square( i );

   -------  What it translates into -------

   //the main fn just acts as a "shell" that starts up proto-runtime and creates a process within it
   int main() 
      PRProcess *myProcess;
      SeedParams *paramsForSeed = malloc( sizeof(SeedParasm) );  //only carries data as return result

      myProcess = PR__create_process( &my_seed_birth_Fn, paramsForSeed );

      PR__wait_for_process_to_end( myProcess );
      do_something_with_results( paramsForSeed->data );


   void my_seed_birth_Fn( void *_params, SlaveVP *animatingVP) 
      int size = 1000 ;
      int *data = (int *) PR__malloc (size * sizeof(int)) ;
      SeedParams *seedParams = (SeedParams *)_params; //used to comm with main()

      PR_DSL__start(); //starts the PR_DSL langlet -- does internal stuff inside proto-runtime..
      //can start additional languages here, and then freely mix "constructs" from them  

      int i ;
      int chunkSize = 100; //for portability, lang would choose this dynamically at run time
      for (i=0; i < size; ++chunkSize) 
       { params = PR__malloc(sizeof(TaskParams)); //these define work the task performs
         params->start = i;
         params->end = i + chunkSize -1;
         PR_DSL__create_task( params, &task_birthFn, animatingVP );
      seedParams->data = data; //sends results back to main()

   int square (int x) { return x*x; }

   void task_birth_Fn( void *_params)   //all birth functions have the same, fixed, prototype
      TaskParams *params = (TaskParams *)_params;
      for (i=params->start; i < params->end; ++i) 
       { data[i] = square(i) ;

some notes on the code -----------

On PR_DSL__start() -- The proto-runtime requires each proto-language to supply a function that creates language-internal data structures and initializes them. This must be invoked before any constructs from that language can be used.

On Process vs Task: a proto-runtime process is equivalent to a Linux process -- it runs a whole-program, where the program uses multiple languages and creates multiple pieces of work. In contrast, a task is one piece of work, created by a language, within application code. Of course, a task can create other tasks.. but those other tasks exist inside the process, not inside the task that caused their creation.. the need for this distinction is largely inside fundamental implementation patterns..

Put another way, the process of running application code goes as follows: -] before the parallel application code can run, a process is created, which is what then runs the application. -] Inside that process, proto-runtime languages can be started, after which constructs from those languages can be used -] after a proto-runtime language has been started, inside a process, then constructs from that language can be used to create tasks (or virtual processors, equiv to threads). -] each task is one atomic piece of work that has constraints on when it can run, relative to other tasks. When it is free to run, then an assigner places it onto a particular core. The work of the task happens on that core.

A deeper understanding will come from time spent implementing runtime systems and/or operating systems.. direct experience appears to be the only way to acquire the mental patterns needed to understand.