Cone Language Reference

Type safety: coercion and conversion

On this page

A type says what a value means, so Cone holds every value to its type. But real programs must cross from one type to another: a byte into a wider sum, a count into a float, a float's bits into an integer. Cone has three ways across, and each says how much may be lost: coercion, which happens by itself and loses nothing; conversion, which you ask for and which may lose something; and reinterpretation, which keeps the bits and changes the type. Here are all three, on one colour:

mod colour;

import stdio use *;

fn main() {
  // An orange, one byte for each of red, green and blue
  let red = 255u8;
  let green = 128u8;
  let blue = 0u8;

  // Coercion: a u8 goes into a u32 by itself, since nothing can be lost
  let r u32 = red;
  let g u32 = green;
  let b u32 = blue;
  let packed = r << 16u32 | g << 8u32 | b;
  print <- "packed into one u32: ", packed, "\n";

  // Conversion, asked for: u8.from keeps the low 8 bits
  let back = u8.from(packed >> 8u32);
  print <- "green, unpacked: ", back, "\n";

  // Conversion to a float, to weigh each channel by how bright it looks
  let light = 0.2126d * f64.from(red) + 0.7152d * f64.from(green)
    + 0.0722d * f64.from(blue);
  print <- "brightness: ", light, "\n";

  // Reinterpretation: the same 64 bits, seen as an integer
  let bits = light as u64;
  print <- "its exponent field: ", bits >> 52u64 & 0x7FFu64, "\n";
}

It prints:

packed into one u32: 16744448
green, unpacked: 128
brightness: 145.759
its exponent field: 1030

Try this: unpack red with u8.from(packed >> 16u32), then try let small u8 = packed; and read what the compiler says.

Types are strict#

A type promises a great deal about its values: which values are allowed (a bool is only true or false), how they are encoded and in how many bits, what can be done with them (its methods: two integers can be added, giving another integer), and the constraints that keep them handled safely. Every value's type is known when the program is compiled, written in a declaration or worked out from the context, which also lets the compiler lay out memory and pick fast instructions.

So a value cannot quietly change its meaning. The compiler refuses a program that stores a value of one type in a variable or collection of another, passes it where a parameter of another type is wanted, returns it from a function declared to return another, or makes a reference of one type to a value of another:

let small u8 = packed;      // **ERROR** 1013: Initialization value's type does not match
                            //   variable's declared type
let t = takes(5i64);        // **ERROR** 1013: Expression's type does not match declared parameter
                            //   (fn takes(n i32) i32)

A value handled as though it had another type could behave in ways that make no sense, or do harm. By convention a type's name is capitalized, which keeps type names apart from variable names; the number types (i32, f64 and the rest) are the exception.

Strictness does not mean a value can never cross. It means every crossing is one of three kinds, and each is visible for what it is: a coercion is safe, so it happens by itself; a conversion may lose something, so it is asked for by name; a reinterpretation trusts the bits, so it is marked with as.

Coercion: the substitutions subtyping allows#

A coercion is a subtype's value standing where its supertype is wanted. What makes it safe is subtyping: the supertype has an equal value for every value of the subtype, and supports everything that can be done with one (Judging a subtype). So Cone does it by itself whenever a value is copied or moved into a variable, parameter, return value, field or reference that expects the supertype: in an assignment, a call or a return. Each coercion is one of two kinds (Generics and coercion): a recast changes only the type and runs nothing, because the value's bits already mean the same as the supertype's; a conversion runs code that makes the supertype's value. These are the coercions Cone makes:

  • A smaller number into a larger one of the same kind, a conversion: a u8 into a u32 gains zeros above its bits, an i32 into an i64 copies its sign upward, an f32 into an f64 is re-encoded in the wider format. Only within a kind: a u8 is not coerced into an i16, though every value fits (1013), and a whole number never into a float; convert them. In arithmetic today, only the right-hand operand widens: wide + k works for an i64 and an i32, while k + wide is refused (1056: No method declared by `+` accepts the call's arguments). Put the wider value first, or convert. Widening keeps the value but not every result: a sum of two u8s wraps before it is widened (nearly, but not strictly, a subtype).
  • A variant into its enum, a recast, since every value of an enum takes the room of its largest variant: let s Shape = Shape.Circle(5);, and a read-only reference to a variant where one to the enum is wanted. A mutable reference is not coerced so, since through it another variant could be stored (References and variance). Not built yet: a plain value where an Option or a Result is wanted, which would be a conversion; write Some(5) or Ok(5).
  • An owning reference into a borrowed one, a recast: a Uni[Point] or an Rc[mut, Point] passed where a &Point is wanted is lent, with the borrowing rules of Borrowed references applied to the owner. An owner whose permission is a lock is the exception: its borrow takes the lock while the program runs, so it is written out, &*p (Lock permissions).
  • A reference into one with fewer permissions, a recast: a &mut or &imm into a & (read-only, ro); a &uni into a &mut, &mut1, &imm or &; any reference into a &opaq (Permissions).
  • A borrow into one that lives less long, a recast: a global's borrow where a local's is wanted (References: region, permission and lifetime).
  • A reference to a struct into a virtual reference to a trait it meets, a conversion, which attaches the struct's table of methods: &cat passed where a &<Named is wanted. So too an owner, Uni[Circle] into Uni[Shape], and a reference to an enum into a virtual reference to a trait its variants meet, which attaches the table of whichever variant the tag names (Virtual references).
  • A reference to a struct into a plain reference to a trait whose fields the struct declares first, in the trait's order, a recast: let r &Id = &node; reads the trait's fields where they lie (What the declaration takes).
  • A reference to an array into a slice, a conversion, which attaches the count: &Array[u8, 4] where a &Array[u8] is wanted.
  • Text into its bytes, a recast: a &str where a &Array[u8] is wanted. Every text is bytes, but not all bytes are text, so the way back is asked for (Text and bytes).
  • A function into a function reference whose signature it meets, a recast: each parameter may accept more than the signature passes, and the result give more than it promises, so long as no value needs converting (Variance).
  • A reference into a raw pointer to the same type, a recast: let p *i32 = &n; (Raw pointers).
  • Not built yet: a reference into an optional one. A &P is a subtype of ?&P, and the two take the same room, so this would be a recast; today it is not made implicitly (let r ?&P = &pt; is refused, 1013), and is asked for with &pt as ?&P.

Values of a struct, an array or a tuple are never coerced into another type: no struct is a subtype of another struct, and an Array[u8, 4] is not converted into an Array[u16, 4] (1013). References to them are coerced instead, as above (Structs and traits). Two more implicit crossings follow below which are conveniences rather than substitutions: a value tested as a condition, and the borrow a method call makes of its receiver.

Even a recast is more than a new label to the compiler: lending an owner as a borrow starts the borrowing rules on the owner's variable, and coercing a value that moves applies the move. Where an expression needs a coercion said out loud, a reference type is named with as, and any other type with its conversion:

let owner = new Uni[Point](3, 4);
let r = owner as &Point;            // the owner, lent as a borrowed reference

Not every reference coercion can be asked for this way yet: &cat as &<Named is refused (1013: May only reinterpret value to the same sized primitive type), though passing &cat where a &<Named is wanted coerces it.

Where a type is wanted#

A coercion needs a type to aim at, and the place a value goes supplies it. That includes the value of an if, a match or a block: where it is wanted as a known type (a variable's declared type, a function's return type, the parameter it is passed to, or the field it gives a value to), each branch's value is coerced to that type. So two string literals, each a &str, are each taken as the slice of bytes wanted, and a function returning an i64 may end in a branch that gives an i32:

let label &Array[u8] = if orbiting {"orbit";} else {"flight";};  // each branch a &Array[u8]

fn width(orbiting bool) i64 {
  if orbiting { 7i64; } else { 3; }     // the i32 3 is coerced to the i64 wanted
}

Where no type is wanted, the branches must agree on one: either the same type, or a supertype they share (a u8 branch and a u32 branch give a u32). The same holds for the value of a block and of a match.

A call gives its arguments their parameters' types only when the function or method it calls is a single declaration that is not generic. The arguments of an overload set, such as those of print <-, a generic function's arguments, and an operator's operands are given no type, since their own types are what choose what is called. There the branches must agree on their own, or be given a type first, as in let s &Array[u8] = if ...;.

Coercion to bool#

A condition must be a bool, and one more coercion makes that convenient: any value whose type has an isTrue method coerces to bool where a condition is wanted. The number types and raw pointers have one: a number is true when it is not zero (a float's 0 and -0 are false, and NaN is true), and a pointer when it is not null. So if count { ... } means if count != 0. An Option has no isTrue; test it with is.

The other direction is never implicit. A bool is not a number, so it coerces to none, anywhere a number is wanted: an initializer, an assignment, an argument, an index or an operand of arithmetic. The compiler names the conversion to write instead:

let count usize = flag;              // **ERROR** 1238: A bool is not a number, and usize is wanted
                                     //   here. Convert it explicitly, 'usize.from(b)', which gives 0 or 1.
let count usize = usize.from(flag);  // 0 or 1

A char is no number#

A char is a Unicode code point held in 32 bits, compared but never counted with, so it coerces to no number, a wider unsigned type included, and no number coerces to it: not a u8, whose values above 127 would be Latin-1 characters, nor a u32, which may be no code point at all. Each direction is a conversion. The one exception is a character literal that is ASCII, which is a u8 where a u8 is wanted:

let code u32 = c;                    // **ERROR** 1334: A char is not a number, and u32 is wanted here.
let code u32 = u32.from(c);          // the code point
let low u8 = u8.from(c);             // the low 8 bits: a code point past ASCII is not a byte
let c char = b;                      // **ERROR** 1334: u8 is not a char, and a char is wanted here.
let c char = char.from(b);           // U+00xx: a Latin-1 character when b is 128 or more
let b u8 = 'a';                      // fine: the literal is the byte 97

A slice is no pointer#

A slice is an address and a count, so it is not a raw pointer, and it coerces to none: the count would be dropped without a word. A pointer to its first element is asked for with as, and only to the slice's own element type, since it retypes nothing. Text is neither: a &str is no pointer, and reaches C as a cstr, which promises the zero that ends it. A raw pointer is no cstr either, except through cstr.fromPtr(p), whose caller makes that promise:

let p *u8 = bytes;                   // **ERROR** 1335: A slice is an address and a count, not a pointer.
                                     //   Take its address explicitly, 'xs as *u8'
let p *u8 = bytes as *u8;            // the address of bytes[0]
fwrite(bytes as *u8, 1usize, bytes.len, f);
puts(text);                          // **ERROR** 1335: Text or a slice promises no NUL after its bytes,
                                     //   so it is no cstr. A literal is one, and so is a String's 'cstr()'
let c cstr = p;                      // **ERROR** 1335: A raw pointer is no cstr, which promises a NUL
                                     //   after its bytes.
let c cstr = cstr.fromPtr(p);        // the caller promises it

Coercing a method's self#

A method call coerces its receiver, self, more freely than any other argument, between a value and a reference to it, so that calls read naturally. A method called by name on a value held in a variable (or a field of one), when no method accepts the value itself but one declares self as a borrowed reference, is handed a borrow of it:

list.push(x);   // the same as:  (&mut list).push(x)
list.len();     // the same as:  (&list).len()
++a;            // the same as:  Type.`++`(&mut a)
  • A method that only reads is handed a read-only borrow. One declaring self &mut is handed a mutable one, which the variable must permit, and so are the operators that change their operand, such as ++, += or a "set" method.
  • One declaring self &uni is handed a unique borrow, which only a place that lends &uni can give (a var variable, or a part of one held by value: From 'uni'), and the call is held to what (&uni v).bump() would be.
  • A temporary value has nowhere to be borrowed from, so such a call on one is an error, and a pointer is never borrowed from.
  • The other way, when self is a reference and no method takes a reference, it is dereferenced in search of one that takes the value: &a + 5 may be a + 5.
  • On a reference to a reference (&&T, &mut &T, a borrow of an owner), a method called by name reaches through every level, the outermost level winning a name, with the access the dereferences written out would give: an owner reached through a & lends only &, so a self &mut method is not reached through it (Implicit de-referencing).

Conversion: from#

Sometimes the crossing cannot be perfect. The integer 1 becomes the float 1.0, but 2.75 becomes the integer 2, and 300 has no u8 at all. A conversion asks a type for its closest equivalent to a value, knowing some of the meaning may be lost, so it is always asked for. It is a method of the type converted to, named from, called on the type with the value to convert:

let six = i32.from(6u8);           // 6
let sixf = f64.from(six);          // 6
let name = String.from("node");    // a String, from text

Every number type has from, which takes a value of any number type. It may be called on the type named directly, through an alias of it, or on a generic's type parameter: T.from(x) converts to whichever type T is. (The old into operator is retired; from is the conversion.) Between numbers it does what the machine does:

  • To a narrower integer, the low bits are kept: u8.from(300) is 44.
  • To a wider integer, a signed value going to a signed type is sign-extended, and every other value is zero-extended from its own width, so u64.from(-1i8) is 255. Between a signed and an unsigned type of one width the bits are kept: i8.from(200u8) is -56.
  • A float becomes an integer by truncating toward zero: i32.from(-2.75) is -2. A float whose truncated value the integer type cannot hold, a negative one to an unsigned type among them, has no defined result.
  • An integer becomes the nearest float, and a float the nearest float of the other width.
  • An integer literal with no suffix is an i32 and converts as one: u64.from(-1) is 4294967295. Converting a literal gives a literal, so the result may be a global's value.
  • A number, reference or pointer converts to bool as its isTrue would answer: bool.from(2) and bool.from(0.5) are true, and only 0, -0 and a null pointer are false. A bool converts to a number as 0 or 1.
  • A char converts to a number as its code point, and a number to a char as one (above).

A number type takes no square brackets: u64[count] is refused (1207: A number type takes no '[...]': a conversion is its method, u64.from(value)). And from is only the type's default conversion: a different one, such as one that saturates at the type's limits or one that checks the value fits, is an ordinary method with a name of its own.

Conversions of your own#

Strict types earn their keep when two values share a representation but not a meaning. In 1999 the Mars Climate Orbiter, a $125 million probe, was lost because one program reported a thruster's impulse in pound-seconds and the next read it as newton-seconds. Both were just floating-point numbers. Give each unit a type of its own, a struct wrapping the number, and the mix-up cannot compile; give the receiving type a from, and the crossing is written once, correctly:

mod units;

import stdio use *;

struct PoundSeconds {
  pub n f64;
}

struct NewtonSeconds {
  pub n f64;

  // The conversion into NewtonSeconds, by the conventional name 'from'
  pub fn from(ps PoundSeconds) NewtonSeconds {
    new NewtonSeconds(ps.n * 4.448222d);
  }
}

fn burn(impulse NewtonSeconds) {
  print <- "burning for ", impulse.n, " newton-seconds\n";
}

fn main() {
  let thrust = new PoundSeconds(10d);
  burn(NewtonSeconds.from(thrust));
}

It prints burning for 44.4822 newton-seconds. Write burn(thrust) instead, and it is refused: 1013, Expression's type does not match declared parameter.

Units are one reason to define a number type of your own. Others: a type can make different promises about what is otherwise left undefined, such as refusing to overflow, or guarantee its values stay within a range; and integers that support atomic operations can be typed apart, as the core library's Atomic[T] is. Methods added to the struct say what may be done with its values. Such a type's from is an ordinary function of the type, as above; the standard number types' from is built into the compiler with them, and so are an integer's bit methods.

Reinterpretation: as#

A reinterpretation keeps a value's bits exactly and treats them as a value of another type of the same size. as does it, with the type on its right. The colour program used it to read a float's exponent: light as u64 is the same 64 bits, now an integer whose bits can be shifted and masked. This kind of access is what floating-point functions a processor does not provide are built from.

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, nor a fixed-width number as either of them. Convert instead:

let w = n as u64;                // **ERROR** 1257: usize and isize are pointer-sized, so 'as' may not
                                 //   reinterpret one as a fixed-width number ... convert with
                                 //   usize.from(x) or u64.from(x)
let count = usize.from(total);   // a conversion, not total as usize
let 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 (18446744073709551615 on a 64-bit target). A slice, two words wide, is reinterpreted as neither; bytes as *u8 takes the address of its first element (above).

Between two references, as may change the referenced type, but the permission only as a coercion would: the permission it gives must be the source's or one it coerces to. &uni may be cast &mut, &imm or &, and &mut or &imm may be cast &; a slice or a virtual reference is no different. The others would write through a reference that promised not to (& as &mut), or make the only reference one that other names still reach (&mut as &uni), and are refused, naming both permissions:

fn poke(r &P) { let m = r as &mut P; m.x = 99; }
  // **ERROR** 1502: `as` changes the reference's permission from ro to mut, which is not a coercion.
  //   Between references `as` may only be a legal coercion (`uni` to `mut`, `imm` or `&`; `mut` or
  //   `imm` to `&`); anything else needs `trust`, which is not built yet.
fn share(r &mut P) &uni P { r as &uni P; }   // **ERROR** 1502, mut to uni

A raw pointer cast to a reference is not asked this (it is how a library hands out memory it manages), and neither is a reference cast to a pointer. One more exception stands for now: a type declaring Immutable makes a reference written with no permission imm, and & may be cast to it. That is how a slice of bytes becomes text: bytes as &str, or p as Uni[str] (Text and bytes). A permission written out is checked like any other, so bytes as &imm str and, from a &, r as &imm P are refused. The cast promises that the bytes are text and that nothing changes them while the text is used, which only the programmer can know.

That is the character of every reinterpretation that is not also a coercion: the compiler cannot see whether the value will behave sensibly as its new type. In Cone's design such a cast is allowed only inside trust, where the programmer takes that responsibility in writing. trust is not built yet: today the casts above that need it are refused, and those the compiler accepts, such as a float as an integer, need nothing.

void: the type of no value#

void is the type of no value at all. It is the return type of a function that does its work and returns nothing, and it fills a type parameter where nothing is wanted: a function that returns nothing on success, or an E on failure, returns Result[void, E]. Its one value is nil, and a return or break with no value gives nil.

A value of size zero is never stored in memory, so a variable may not be declared void, nor space allocated for one (not yet checked: the compiler accepts let v void; today). A struct may have a void field, as long as some other field has a size. A function that never returns at all is declared Never instead, which Panics explains.

You can now move a value from one type to another and say what each crossing may lose. Next: what happens when something that must never happen does, and the program stops.