Trust
On this page
Note: None of this is implemented, except the as operator and the rule for its sizes, under Type Reinterpretation.
Safety is enriched when the programmer and the compiler work together towards that end, each bringing different gifts to bear on the problem. The compiler's gift is its obsessive ability to detect every time code breaks its declared constraints. This is manifested in the compiler's built-in guard rails for protecting your code against a number of common safety hazards. However, the compiler can only quickly check a small collection of very-simplistic rules; it has no intelligence to detect when rule-breaking code is still 100% safe. The compiler is trying to be helpful, but is just getting in the way!
This is where we rely on the programmer's gift (and ultimate responsibility) to figure this out, using techniques more sophisticated than simple safety rules. In such situations, Cone provides a built-in escape hatch from some of the over-anxious compiler's guard rails, in the form of an annotation. By preceding any any expression or block with trust (as in: "trust me on this"), several safety checks are turned off, thereby allowing:
- Raw pointer dereferences, arithmetic, and re-casts. This provides a way to bypass the safety constraints imposed on references by permissions, lifetimes, regions and bounds-checks. On the next page, we'll explore this.
- extern function calls. This is normally restricted, as Cone has no way to assure that some other language has been as rigorous in verifying the safety of their function logic. We'll explore this when introducing extern.
- Type reinterpretation, where Cone has no way to know that the re-cast value is being safely digested.
- Non-discriminated unions field access, where Cone has no way to know what disciminant to pattern match against.
By using this "trust" mechanism, compiler-unverifiable code can be isolated and highlighted as such, while still acknowledging that the programmer, who has the ultimate responsibility for safety, may very clearly see what the compiler cannot.
Type Reinterpretation#
Reinterpretation takes some existing encoded value and allows it to be re-cast and manipulated as if it were a value of a different, but same-sized type. For example, we can use re-interpretation to explicitly coerce a non-nullable reference to a nullable one:
imm nullref = someref as ?&i32;
Reinterpretation uses the as operator. Unlike other operators, as expects a type to the right of the operator.
The two types must be the same size on every target, not just the one being compiled for. usize and isize are pointer-sized, 64 bits on some targets and 32 on others, such as WebAssembly, so neither may be reinterpreted as a fixed-width number type (u64, i32, f64, ...), nor a fixed-width number as either of them. Convert the value instead, with the type's from (Explicit Conversion):
imm count = usize.from(total); // not total as usize imm wide = u64.from(count); // not count as u64
Being pointer-sized, usize and isize are the same size as each other and as a raw pointer or reference on every target, so each may be reinterpreted as the other, or as a pointer or reference, and back. The bits are kept: -1isize as usize is all ones, the largest usize. A slice, two words wide, is reinterpreted as neither.
Unless the re-interpretation represents a legal coercion, its use could pose a risk to type safety. A value could behave badly when treated and manipulated within the constraints of a different type.
When Cone is unsure of the risk, it requires use of trust to reassure it that the reinterpretation is safe. Use of this keyword marks that the programmer knows and accepts the reponsibility for handling it in a safe and useful way. For example, re-interpreting a nullable reference as non-nullable without a visible check for null-ness requires such reassurance:
imm someref = trust {nullref as &i32};
Although reinterpretation is mostly used with references and pointers, other valid uses are possible. For example, a IEEE 754-compliant floating-point number can be reinterpreted as an unsigned integer to get direct access to its encoded bits:
imm signbit = bool.from(trust {pi as u64} >> 63);
This sort of capability is needed to safely build advanced floating point functions not supported by the CPU, such as trigonometric operations.
Untagged overlays#
An enum carries a discriminant, so which variant a value holds is in the value and a pattern match is what recovers it. An untagged overlay — several differently-typed fields sharing one piece of storage, with nothing saying which is live — gives that knowledge up in exchange for the space. The compiler then cannot verify that the field read was the one written, because there is nothing for it to check against. trust is the programmer's way to tell the compiler not to worry here:
fn getInt(v Overlay) usize trust {
if (v.nbr & 3 == 0) {
*v.ref;
} else {
v.nbr>>2;
}
}
As this example shows, the entire function block can be marked as trustworthy.
Cone has no spelling for an overlay. The tagged form is an enum, and it is the one to reach for; what an overlay adds is the obligation to track the live field by hand, which is exactly what trust would be asserting.