Design directions - Practical/practical-sa GitHub Wiki
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.
- 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.
- 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.
The default mode for new variables is const. Use the mutable
keyword to indicate that a variable is, well, variable.
The default mode is unsigned.
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.
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
}
I'm using the D notation here. Final syntax TBD.
Built-in attributes have the same syntax as compiler-defined ones.
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;
In compile time context, there are some first-class citizens that are not available at run time
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.
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:
- Return them from a function
- 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) );