Skip to main content

Compile time, run time, coffee time

[ Please note that in the following discussion I will be using PM 0.5 notation, which is slightly different to PM 0.4 and also in some flux as PM 0.5 is still in development.

 Many programming languages (and most newly developed ones) include some form of compile-time programming, ranging from simple preprocessors (#define, #if in C) to fully blown macro systems capable of re-writing the abstract syntax tree during compilation (Rust, Julia, etc.). In line with its philosophy of keeping things as simple as possible, but not simpler, PM takes a middle road with compile-time programming supported primarily through the type system. There is nothing too radical here – this is not an area where the language aims to break new ground. 

The PM approach centres around the use of compile-time types. Many languages use special types to denote literal values and PM follows this trend. Literal integers, reals, strings and Booleans each have their own types: literal(int), literal(real), literal(string) and literal(bool) respectively. A value of a literal type has no associated run-time storage, it is a purely compile-time entity. A run-time value is only created when a literal type is converted to a corresponding basic type, e.g. literal(int) to int. This occurs automatically when the literal value is passed as an argument to a procedure call. Thus, in the classic:
  print(“Hello World”)   
the string “Hello World” is a compile-time value, which is only converted to a run-time representation when it is passed to print. So far, so pedantic. However, it is possible to define a procedure that specifically accepts a literal value, and in this case no automatic conversion occurs: 
      proc double(x:literal(int))=x+x  
      double(2)  

Here double(2) returns the compile-time literal value 4 rather than computing 2+2 at run time. As you might expect there are literal versions of the standard arithmetic, logical and string operators. Thus 2*3 returns the literal value 6, rather than a run-time integer. Going beyond this, it is possible to treat each literal value as having a unique type: 
 proc name(x:literal(1))=”One”  
 proc name(x:literal(2))=”Two”  
 proc name(x:literal(3))=”Three”  
 one_two_three=name(1)++” “++name(2)++” “++name(3)  
Here one_two_three is the literal string “One Two Three”.  

Literal values get more powerful when combined with another PM mechanism, variable arguments. A parameter will match any number of arguments and can in turn be passed as the last argument to a procedure call. Using this mechanism, it is possible to implement a type-secure version of a C-style printf
 proc printf(format:literal(string),a,b,…) {   
    printf(first_entry(format),a)  
    printf(remaining_entries(format),b,…)   
 }  
 printf(format:literal(string),a:int): printf_integer(format,a) check format_is_integer(format)  
 printf(format:literal(string),a:int): printf_real(format,a) check format_is_real(format)  
 // … etc  
given suitable literal string functions first_entry, remaining_entries, format_is_integer, etc. The one-two-three example gives an idea about how these can be implemented. 

There are also a number of compile-time reflection intrinsics. These are used extensively by the PM standard library but are being made available in the main language in PM 0.5 [exact details still being finalised]. A simplified version of structure/record value equality shows how some of these intrinsics work together: 
 proc ==(a:a_struct or a_rec, b:a_strunct or a_rec) {  
      test same_type(a,b)  
      _compare_elements(a,b,1,number_elements(a)-1)  
 }  
 proc _compare_elements(a,b,index:literal(int),remaining:literal(int))   
      = element(a,index)==element(b,index) and   
           _compare_elements(a,b,index+1,remaining-1)  
 proc _compare_elements(a,b,index:literal(int),remaining:literal(0))   
      = element(a,index)==element(b,index)  
Here same_type checks for type equality returning a literal Boolean, number_elements gives the number of elements in a structure or record as a literal integer and element selects a given element using a literal integer index. Type specialisation [See previous post] ensures that the recursive traversal of the data structures is resolved entirely at compile time. 

So that’s all for now. I hope I have demonstrated how PM uses a combination of compile time types, function type matching and specialisation, variable arguments and recursion in place of macros. There is of course more to this that I have shown, including a variant on literal types that provides a form of procedure currying or forced global constant propagation. Stay tuned…

Comments

Popular posts from this blog

Data in, data out

When a program is running over multiple processors or processing cores then it is important to be able to map where data are flowing. One way in which data flows can be translated into programming language semantics is though argument passing to subroutines. In the absence of global variables, data transfer to and from a subroutine straightforwardly can be directly related to communication to and from another processor. In PM, the emphasis is on data parallelism through the for statement. for index in array do index=process(index) endfor      So what are the rules governing data flow into and out of this statement? These are primarily based around variable locality. x:= 1 for index in array do y:=func(index) index=process(x,y) endfor Here x is relatively global to the for statement and y is local to it (a variables scope only extends to the end of the statement block in which it is defined).  There are two ways t...

The PM Type System

In common with other aspects of PM design, the type system is designed to facilitate the use of flexible programming constructs, such as polymorphism and dynamic dispatch, while emphasising the generation of fast, static, code.  In common with many other modern languages, PM also attempts to combine the safety of static typing with the expressivity of dynamically typed languages such as Python . The type of any PM expression may usually be determined by a static examination of the code. Variables take their types from an initialising expression using the ‘:=’ declaration syntax popularised by Go . a:= 100 ! Declare ‘a’ as an integer (int) variable Variables do not change type during the course of their lifetime – polymorphic programming requires the use of special values, described later. Composite values such as structures are generated using specialised expressions rather than through the invocation of type-specific creator functions: b:= struct var_descriptor...