Skip to content

Design directions

Shachar Shemesh edited this page Sep 18, 2018 · 3 revisions

Please remember that many things are still in flux. If the naming styles are not consistent, this is not intentional, and will be fixed as things progress.

There are two purposes for this page. The first is to flash out problems with the language before implementation begins. The second is to let new-comers get a taste of how programming in Practical will look like.

This page is written before any work has started on coding, implementing or writing code using the language. It will be very interesting to see how many of those objectives would be reachable.

Items marked with (?) are items I'm not sure about.

Syntax

  • So far, the language most similar to Practical in syntax is Rust.
  • Like C, Practical will use curly braces, semicolons, and white-space indifference.
  • Function return types, if not implicitly derived, are denoted after the arguments list.
  • Explicit keywords for definitions and declarations of variables and functions, as well as those that already have explicit keywords in C++ (classes, enums, etc.)
  • Practical is an expression language: blocks evaluate to expressions, if statements are expressions.
  • Standard libraries to use CamelCase following the Java standard: functions and variables start with lower case, classes and types with upper case.
    • Built-in types will also follow that standard, meaning they, too, start with upper case.
  • /* .. */ comments are nested.

Compatibility to C/C++

  • Fix operator precedence bugs that invite errors in C. Same for confusing evaluation rules of C (like integer promotion of operations). Compiler warning if an expression would evaluate in its entirety differently between C and practical.

Example:

func func1(S16 a, S16 b) => S16 {
  return a+b; // No warning. C and practical would always return the same result
}

func func2(S16 a, S16 b) => S32 {
  return a+b; // Warning: C would promote to int, treating overflows differently than practical
}
  • No implicit narrowing conversions.
    • Unsigned integer is only implicitly casted to signed integer of wider type.
    • Signed integer is never implicitly casted to unsigned integer.
    • It is ok to violate the above rules if the compiler either proves that values we risk losing are not possible, or if the user asked that those values be UB (see explicit UB below).
    • This means that signed/unsigned comparison is an error, as defined by the language.
  • Rethink some C and C++ UBs.
    • Evaluation order in function calls
    • integer overflows(?)
  • Remove the comma operator.

Practical features

Default const

The default mode for new variables is const. Use the mutable keyword to indicate that a variable is, well, variable.

Default unsigned (?)

The default mode is unsigned.

Discussion

Actually, so far it seems like we will not have a default at all, at least for explicitly defined types (i.e. - U32 is unsigned, S32 is signed, there is no such thing as int). With that said, the lack of implicit casting between the types will probably push the programmer toward using unsigned anyways, so no further encouragement will likely be needed.

Allow introducing explicit UBs

Exact mechanism TBD. For the time being, this will be phrased as an assert.

function func(S32 a, S32 b) {
  assert(a>5);
  // It is undefined what happens if a==4 at this point, even in release build
}

This can affect the C compatibility rule above:

function func(U16 a, U16 b) => S32 {
  assert(a<10);
  assert(b<20);

  return a+b;  // No warning, as no short overflow could happen
}

Function attributes

I'm using the D notation here. Final syntax TBD.

Built-in attributes have the same syntax as compiler-defined ones.

Compile time execution

Every code written can be either run-time, compile-time or both compatible. If the code is compile-time only, it will not be generated at the run time at all.

Certain capabilities are compile time only or run time only.

Using a run-time only capability from a compile-time only context is a compilation error and vice versa. This is true whether the context is such by explicit attribute or by inference.

function func1(U32 value) @compiletime;

// Run time only by explicit attribute
function func2(U32 value) @runtime;

// Both run and compile time
function func3(U32 value);

// Run time only: uses a pointer
function func4(U32* value);

// Compile time only: accepts a type
function func5(Type T) => U32;

Compile time only first class citizens

In compile time context, there are some first-class citizens that are not available at run time

The Type type

A variable of type Type can be any (and only) type.

C++ decltype primitive can, therefor, be implemented as a library function (using the C++ syntax for templates):

template <class T> Type decltype(const T& arg) {
   return T;
}

We do not need to annotate decltype with @compiletime, as only a compile time function can return Type.

Also note that we only need the templating in order to derive the type. A function returning (or accepting) type need not be templated at all.

Here is an example of arrayify:

function arrayify(Type baseType) => Type {
  return baseType[];
}

This is not a template function! It is a function manipulating types.

ArgumentsList

There are two kinds of argument lists.

The first is an argument list as used by function declarations:

struct FunctionParameter {
  Type type;
  String name;
}

typedef FunctionParameter[] FunctionParameters;

The second is an arguments list as passed to function:

struct FunctionArgument : FunctionParameter {
  unique_ptr<type> value;
}

typedef FunctionArgument[] FunctionArguments;

The pseudo definitions above non-withstanding, the really interesting thing about FunctionParameters and FunctionArguments is that you can:

  1. Return them from a function
  2. Use them to call a function

This means you can use (compile time) functions to calculate the arguments you want to pass to a function, and then use that:

function someFunction( S32 arg1, String arg2 );
function calcSomeFunctionArguments( S32 seed ) => FunctionArguments;

someFunction( calcSomeFunctionArguments(5) );