Thiago Adams
2024-09-17 21:27:09 UTC
I found some use cases where we may want disable const and _not_null (if
C had this qualifier) at same time. Kind of "anti-qualifier".
For the impatient go direct to "mutable qualifier allows certain
exceptions to the usual contract of immutability"
Nullable Pointers
The concept of nullable pointers is introduced to refine the type system
by explicitly indicating when pointers can or cannot be null.
Take, for instance, the standard function strdup:
char * strdup(const char * src);
In this function, the argument src must reference a valid string. The
function returns a pointer to a newly allocated string, or a null
pointer if an error occurs.
In Cake, the _Opt qualifier extends the type system by marking pointers
that can be null. Only pointers qualified with _Opt are explicitly
nullable, providing better clarity about which pointers may need null
checks.
The _Opt qualifier is placed similarly to const, after the * symbol. For
example, the declaration of strdup in Cake would look like this:
char * _Opt strdup(const char * src);
Static analysis tools need to know when these new rules for nullable
pointers apply, particularly for unqualified pointers. This is managed
through the #pragma nullable enable directive, which informs the
compiler when to enforce these rules.
Example 1: Warning for Non-Nullable Pointers
#pragma nullable enable
int main(){
int * p = nullptr; // warning
}
In this example, a warning is generated because p is non-nullable, yet
it is being assigned nullptr.
Example 2: Converting Non-Nullable to Nullable
The conversion from a non-nullable pointer to a nullable one is allowed,
as shown below:
#pragma nullable enable
char * get_name();
int main(){
char * _Opt s = get_name();
}
Here, the return value of get_name() is non-nullable by default, but it
is assigned to a nullable pointer s, which does not trigger any warnings.
Example 3: Diagnostic for Nullable to Non-Nullable Conversion
Consider the following case:
#pragma nullable enable
char * _Opt strdup(const char * src);
void f(char *s);
int main()
{
char * _Opt s1 = strdup("a");
f(s1); // warning
}
In this scenario, s1 is declared as nullable, but f expects a
non-nullable argument. This triggers a warning, as the nullable pointer
s1 could potentially be null when passed to f. To resolve this warning,
a null check is required:
if (s1)
f(s1); // ok
This warning relies on flow analysis, which ensures that the potential
nullability of pointers is checked before being passed to functions or
assigned to non-nullable variables.
Non nullable members
The concept of nullable types is present in some language like C# and
Typescript. Both languages have the concept of constructor for objects.
So, for objects members, the compiler checks if after the constructor
the non-nullable members have being assigned to a non null value.
The other way to see this, is that during construction the non nullable
pointer member can be null, before they receive a value.
In C, we don t have the concept of constructor, so the same approach
cannot be applied directly.
Cake, have a mechanism using the qualifier _Opt before struct types to
make all non-nullable members as nullable for a particular instance.
struct X {
char * name; //non nullable
};
struct X * _Opt makeX(const char* name)
{
_Opt struct X * p = calloc(1, sizeof * p);
if (p == NULL)
return NULL;
char * _Opt temp = strdup(name);
if (temp == NULL)
return NULL;
x->name = temp;
return x;
}
Just like in C# or Typescript, we cannot leave this function with a
nullable member being null. But the particular instance of p is allowed
to have nullable members.
This is also useful to accept for some functions like destructor,
partially constructed object.
void x_destroy(_Opt struct X * p)
{
free(p->name); //ok
}
Note that this concept also could be applied for const members.
The introduction of a mutable qualifier allows certain exceptions to the
usual contract of immutability and non-nullability during transitional
phases, such as in constructors and destructors. This means that objects
marked as mutable can temporarily violate their normal constraints, such
as modifying const members or assigning null to non-nullable pointers
during these phases.
Consider the following code example:
struct X {
const char * const name; // non-nullable
};
struct X * _Opt makeX(const char* name)
{
mutable struct X * p = calloc(1, sizeof *p);
if (p == NULL)
return NULL;
char * _Opt temp = strdup(name);
if (temp == NULL)
return NULL;
p->name = temp; // OK!!
return p;
}
In this example, struct X has a const member name, which is
non-nullable. Under normal conditions, modifying a const member after
initialization would be disallowed. However, the mutable qualifier
temporarily relaxes this rule during the object’s creation process,
allowing modifications even to const members, and allowing a
non-nullable pointer to be null before the object’s initialization
completes.
We also have an implicit contract for struct members. Generally, we
assume that members are initialized, but we lack a qualifier to
explicitly indicate "initialized member." For instance, when using
malloc, members are initially uninitialized, but they should receive a
value before being used.
struct X * _Opt makeX(const char* name)
{
mutable struct X * p = malloc(sizeof *p);
if (p == NULL)
return NULL;
char * _Opt temp = strdup(name);
if (temp == NULL)
return NULL;
p->name = temp; // OK!! name fixed
return p;
}
Transitional State:
During the object creation (or destruction), the instance is considered
to be in a transitional state, where the usual constraints—such as
non-nullable pointers and immutability—are lifted. For example, in the
makeX function, p->name can be set to temp, even though name is const.
This allows flexibility during initialization, after which the object is
returned to its normal state with the contract fully enforced.
Effect on the Final Object:
Once the transitional phase is over and the object is returned, the
contract that governs the object (such as immutability of name and
non-nullability of pointers) is fully reinstated. The mutable qualifier
only applies within the scope of the constructor or destructor, ensuring
that once the object is fully constructed, its state is valid and
consistent with the type system’s rules.
This approach allows for more flexibility during object creation while
maintaining strong contracts once the object is finalized, enhancing
both safety and expressiveness in the code.
OBS: mutable qualifier is not yet implemented in Cake. However, _Opt for
structs is implemented.
C had this qualifier) at same time. Kind of "anti-qualifier".
For the impatient go direct to "mutable qualifier allows certain
exceptions to the usual contract of immutability"
Nullable Pointers
The concept of nullable pointers is introduced to refine the type system
by explicitly indicating when pointers can or cannot be null.
Take, for instance, the standard function strdup:
char * strdup(const char * src);
In this function, the argument src must reference a valid string. The
function returns a pointer to a newly allocated string, or a null
pointer if an error occurs.
In Cake, the _Opt qualifier extends the type system by marking pointers
that can be null. Only pointers qualified with _Opt are explicitly
nullable, providing better clarity about which pointers may need null
checks.
The _Opt qualifier is placed similarly to const, after the * symbol. For
example, the declaration of strdup in Cake would look like this:
char * _Opt strdup(const char * src);
Static analysis tools need to know when these new rules for nullable
pointers apply, particularly for unqualified pointers. This is managed
through the #pragma nullable enable directive, which informs the
compiler when to enforce these rules.
Example 1: Warning for Non-Nullable Pointers
#pragma nullable enable
int main(){
int * p = nullptr; // warning
}
In this example, a warning is generated because p is non-nullable, yet
it is being assigned nullptr.
Example 2: Converting Non-Nullable to Nullable
The conversion from a non-nullable pointer to a nullable one is allowed,
as shown below:
#pragma nullable enable
char * get_name();
int main(){
char * _Opt s = get_name();
}
Here, the return value of get_name() is non-nullable by default, but it
is assigned to a nullable pointer s, which does not trigger any warnings.
Example 3: Diagnostic for Nullable to Non-Nullable Conversion
Consider the following case:
#pragma nullable enable
char * _Opt strdup(const char * src);
void f(char *s);
int main()
{
char * _Opt s1 = strdup("a");
f(s1); // warning
}
In this scenario, s1 is declared as nullable, but f expects a
non-nullable argument. This triggers a warning, as the nullable pointer
s1 could potentially be null when passed to f. To resolve this warning,
a null check is required:
if (s1)
f(s1); // ok
This warning relies on flow analysis, which ensures that the potential
nullability of pointers is checked before being passed to functions or
assigned to non-nullable variables.
Non nullable members
The concept of nullable types is present in some language like C# and
Typescript. Both languages have the concept of constructor for objects.
So, for objects members, the compiler checks if after the constructor
the non-nullable members have being assigned to a non null value.
The other way to see this, is that during construction the non nullable
pointer member can be null, before they receive a value.
In C, we don t have the concept of constructor, so the same approach
cannot be applied directly.
Cake, have a mechanism using the qualifier _Opt before struct types to
make all non-nullable members as nullable for a particular instance.
struct X {
char * name; //non nullable
};
struct X * _Opt makeX(const char* name)
{
_Opt struct X * p = calloc(1, sizeof * p);
if (p == NULL)
return NULL;
char * _Opt temp = strdup(name);
if (temp == NULL)
return NULL;
x->name = temp;
return x;
}
Just like in C# or Typescript, we cannot leave this function with a
nullable member being null. But the particular instance of p is allowed
to have nullable members.
This is also useful to accept for some functions like destructor,
partially constructed object.
void x_destroy(_Opt struct X * p)
{
free(p->name); //ok
}
Note that this concept also could be applied for const members.
The introduction of a mutable qualifier allows certain exceptions to the
usual contract of immutability and non-nullability during transitional
phases, such as in constructors and destructors. This means that objects
marked as mutable can temporarily violate their normal constraints, such
as modifying const members or assigning null to non-nullable pointers
during these phases.
Consider the following code example:
struct X {
const char * const name; // non-nullable
};
struct X * _Opt makeX(const char* name)
{
mutable struct X * p = calloc(1, sizeof *p);
if (p == NULL)
return NULL;
char * _Opt temp = strdup(name);
if (temp == NULL)
return NULL;
p->name = temp; // OK!!
return p;
}
In this example, struct X has a const member name, which is
non-nullable. Under normal conditions, modifying a const member after
initialization would be disallowed. However, the mutable qualifier
temporarily relaxes this rule during the object’s creation process,
allowing modifications even to const members, and allowing a
non-nullable pointer to be null before the object’s initialization
completes.
We also have an implicit contract for struct members. Generally, we
assume that members are initialized, but we lack a qualifier to
explicitly indicate "initialized member." For instance, when using
malloc, members are initially uninitialized, but they should receive a
value before being used.
struct X * _Opt makeX(const char* name)
{
mutable struct X * p = malloc(sizeof *p);
if (p == NULL)
return NULL;
char * _Opt temp = strdup(name);
if (temp == NULL)
return NULL;
p->name = temp; // OK!! name fixed
return p;
}
Transitional State:
During the object creation (or destruction), the instance is considered
to be in a transitional state, where the usual constraints—such as
non-nullable pointers and immutability—are lifted. For example, in the
makeX function, p->name can be set to temp, even though name is const.
This allows flexibility during initialization, after which the object is
returned to its normal state with the contract fully enforced.
Effect on the Final Object:
Once the transitional phase is over and the object is returned, the
contract that governs the object (such as immutability of name and
non-nullability of pointers) is fully reinstated. The mutable qualifier
only applies within the scope of the constructor or destructor, ensuring
that once the object is fully constructed, its state is valid and
consistent with the type system’s rules.
This approach allows for more flexibility during object creation while
maintaining strong contracts once the object is finalized, enhancing
both safety and expressiveness in the code.
OBS: mutable qualifier is not yet implemented in Cake. However, _Opt for
structs is implemented.