The Belte language is syntactically very similar to C#; Belte is a “C-Style” language that focuses on the object-oriented paradigm.
Currently, the Belte compiler, Buckle, supports interpretation and building to a .NET executable.
// ... and /* ... */ in code samples refer to an arbitrary statement or expression or continuation of a pattern.
For example, if a code sample showed:
int a = /* ... */;
The usage of /* ... */ is meant to imply the actual expression used does not matter for the sake of demonstrating the
feature being discussed.
Similarly, if a code sample showed:
void Func() {
// ...
}
The usage of // ... is meant to imply the body of the function Func does not matter for the sake of the feature
being discussed.
Note that these shortcuts do not necessarily ensure correctness and are used for brevity. For example:
int Func() {
// ...
}
In this example, Func in this state is invalid because it fails to return even though the function signature has a
return type of int. In this case the // ... implies an arbitrary valid return.
Some features are not supported across all endpoints for various reasons.
The following list describes all of the features where full parity is not currently implemented or was not always implemented.
--evaluate builds, and compile-time expressions.--execute builds and compile-time handles.--dotnet builds and build scripts.| Feature | Evaluator | Executor | IL Emitter | Explanation |
|---|---|---|---|---|
--type=graphics projects |
✓ | ✓ | ✕ | Standalone graphics DLL under development |
| Non-type templates | ✓ | ✕ | ✕ | Not supported by the .NET runtime |
| Non-integral enums | ✓ | ✕ | ✕ | Not supported by the .NET runtime |
| Pointers | ✕ | ✓ | ✓ | Partially supported the Evaluator but not stable due to internal memory structure |
| Function pointers | ✕ | ✓ | ✓ | Disallowed in the Evaluator due to internal memory structure |
| Externs/DllImport | ✕ | ✓ | ✓ | Incompatible with the Evaluator |
| Inline IL | ✕ | ✓ | ✓ | Incompatible with the Evaluator |
| .NET DLL references | ✕ | ✓ | ✓ | Incompatible with the Evaluator |
The following lists all keywords used in the language. No type names (e.g. int) are reserved.
Some keywords have multiple meanings depending on context. Those keywords will be disambiguated in the lists below.
These keywords are reserved names and cannot be used as identifiers.
These keywords only act as keywords inside specific contexts. As such they can be used as identifiers in most places.
Nullability is treated in a non-standard way in Belte. As such, a close read of the following section is recommended, which is a consolidation of important nullability semantics found in the rest of the documentation.
To summarize:
! removes nullability? adds nullabilityA type is “nullable” when it permits null.
Classes are reference types meaning they are heap allocated, garbage collected, and not copied when passed.
For example:
var a = new MyClass();
var b = a;
b.i = 5;
class MyClass {
public int i = 0;
}
In this example, a.i is 5 because both a and b refer to the same object in memory.
Reference types are non-nullable by default. To make it nullable, a ? annotation can be used:
var a = new MyClass();
a = null; // Invalid
var? a = new MyClass();
a = null; // Okay
Notice how nullable annotations apply normally even when implicitly typing. The following are identical:
MyClass! a = new MyClass();
var! a = new MyClass();
Value types are also non-nullable by default (this includes primitives, structs, and pointers).
To make a non-reference type nullable, a ? annotation can be used just like reference types:
int a = 3;
a = null; // Invalid
int? a = 3;
a = null; // OK
Most value-types (non-reference-types) are able to made nullable. Pointers and function pointers are not.
In much of the documentation and standard library, redundant annotations are used for clarity. Note that the following
are identical because int defaults to being non-nullable:
int a = 3;
int! a = 3;
Nullability in pointers is not covered with the type system, but rather with the nullptr keyword. A nullptr under
the hood is just a 0-valued pointer. The following are equivalent:
int* ptr = nullptr;
int* ptr = (int*)0;
int* ptr;
Even though pointers and function pointers themselves do not use the nullable type system, their elements can:
int?* ptr; // OK
int?*? ptr; // Invalid
void(int?)* ptr; // OK
void(int?)*? ptr; // Invalid
Local initializers are required for non-nullable types. For nullable types, initializers are optional and a local without one will be set to null. The following are identical:
int? a = null;
int? a;
The exception to this is pointers and function pointers. They are set to nullptr by default if no initializer is
present. The following are identical:
int* ptr = nullptr;
int* ptr;
When implicitly typing, the inferred type is the direct type of the initializer. The following are identical:
int a = 3;
var a = 3;
Unlike locals, non-nullable struct fields do not allow initializers and instead are set to a default value. The default
value of numeric types is 0, for booleans false, and an empty string for strings.
An mentioned earlier, pointers and function pointers default to nullptr.
A non-nullable struct’s default value is a created struct where each field is set to it’s default value. For example:
var a = new MyClass();
var b = a.str.num;
// b equals 0
class MyClass {
MyStruct str = default;
}
struct MyStruct {
int num;
}
Class fields always require an initializer or definite assignment, which can be read about here.
Since structs set their fields to their default value, the default value of a struct is a struct where every field is set to it’s default value. If a struct contains a field that has no default value, the struct also has no default value:
class A { }
struct MyStruct {
A a;
constructor(A a) {
this.a = a;
}
}
MyStruct myStruct = default; // Invalid because type `A` has no default value
var, const, final, and constexpr can be used when defining a local indicating that their type should be
inferred. The inferred type is usually the exact type of the initializer. The following are identical:
int a = 3;
var a = 3;
Nullability persists when inferring a type. The following are identical:
int? a = Func();
var a = Func();
int? Func() { /* ... */ }
var means a normal local declaration. const, final, and constexpr infer the type of a local with the respective
modifiers. The following are identical:
const int a = 3;
const a = 3;
constexpr int a = 3;
constexpr a = 3;
final int a = 3;
final a = 3;
const means the local cannot be assigned to or otherwise modified. constexpr means the value of the local is a
compile-time constant that will be substituted at compile time. For classes, a const modifier means fields can only be
read but not written to, and only methods marked const can be called. Class-types cannot use the constexpr modifier
because they are not compile-time constants. final means the local cannot be assigned to but can be modified. This
means any class method can be called or array elements can be modified.
Belte does not currently perform automatic null-flow analysis. Instead, explicit null checks and control flow should be used.
When a value is needed to be non-null at a certain time, the ! operator can be used that asserts
that the value is not null, otherwise a runtime error occurs. For example:
int? a = Func();
int! b = a!; // If a is null, runtime exception
To conditionally execute code if a value is not null, a null-binding contract can be used:
int? a = Func();
if (a -> b!) {
// ...
}
In this example, b is declared as a non-nullable local set to the value of a, so in this case the type of b is
int!. b only lives inside of the block.
To use a type’s default value in the case of null, the ? operator can be used. For example:
bool? a = Func();
if (a?) {
// ...
} else {
// ...
}
In this example, the else block is executed if a is false or null. Note that conditions inside if, for, and while
constructs can be nullable. If the condition is null at runtime, an exception is thrown. For example:
bool? a = Func();
if (a) {
// ...
}
In this example, if a is null, a runtime exception is thrown at the if condition.
Arrays are a collection of elements. Elements cannot be read before they are written:
var arr = new int[10];
arr[0]; // Exception
var arr = new int[10];
arr[0] = 45;
arr[0]; // Okay
A Buffer<T> can be used to avoid these checks, but should be avoided if
possible because it potentially allows reading invalid values for a type:
class A { }
var buffer = new Buffer<A>(10);
A a = buffer[0]; // null
In the above example, the non-nullable local a is given a null value from the buffer. Buffers should only be used when
performance is critical or working with unconstrained templates.
Belte is similar enough to C# so that the differences are more notable than the similarities. The following is a list of most of the differences to make it more clear where the language is unique with links to relevant doc sections:
defer statementswith expressions and statementsscoped statements instead of using statementsdestructor keywordint) don’t have membersconstructor and finalizer keywordsunreachable statementsconst and final locals and fields with reference types instead of readonlyconst methodsconstexpr locals and fieldsflags enumsextends keyword for base listsfor “each” loops with index supportisnt instead of is notlowlevel contextsnullptr literalout parameters don’t require assignmentpinned localsinternal/private protected/protected internal accessibilities (yet)break statementsbool? instead of boolunsafe contextsfixed keyworduint16)winbool type instead of marshalling bool as 4-bytes in externsbool marshals as 1 byte in externsf"" instead of $""var, const, and constexpr and nullable annotationsimplicit keywordx!, x!!, x?, x /\ y, x \/ y, x >< [y, z], x ?! y, x..y, x?..y)unions and anonymous unionsout parameters can have a default valueusing aliases can be placed anywhere instead of only before all memberspacked keyword instead of StructLayout attributeUpdate() pointIdentifiers are used to name symbols. For example, in the statement var a = 3;, the name of the symbol is a, which
is the identifier.
Identifiers are continuous strings of letters, digits, and the underscore (_) character, where the first character has
to be a non-digit. Legal identifier could be myLocal, My_Local, MyTemp3, or _, whereas 3myLocal would be
illegal because it starts with a digit. Identifiers cannot be the same as any
non-contextual keywords. For example, var class = 3; would be illegal because class
is a non-contextual keyword.
The verbatim specifier @ can be used to treat what would be a keyword as an identifier. For example, var @class = 3;
would be legal where class is the identifier (note that the @ is not included, so myLocal and @myLocal are the
same identifier).