Type inference

Expected types

Many expressions in crow can have multiple possible types.
For example:

  • 7 can be a nat, int, or float (and more).
  • "foo" can be a string or symbol (and more).
  • () calls new with no arguments. This function has many overloads returning different types.
  • In general, any two functions may differ only in their return type.

The meaning of an expression may be disambiguated based on its context. The term for this is an expected type.

main void() b bool = () n nat = () s string = () info log "b is {b}, n is {n}, s is '{s}'"

When an expected type is missing, the type-checker will still attempt to check the expression. But, this may lead to a compile error.
Keep this in mind, since many compile errors can be fixed by adding an expected type. (And even if that's not sufficient, it's a good way to start.)

Contexts that have an expected type

An expected type will be present in these cases:

  • From a return type: The expression returned from a function has the type the function was declared to return. main void() () # void
  • From a function parameter: When a function has only one remaining overload possible, an argument to it has the type of the corresponding parameter. main void() () call-me # () is nat call-me void(_ nat) () # void

    Overloads are filtered by the expected return type.

    main void() () call-me # () is nat call-me void(_ nat) () # void # This overload can be ignored because it returns 'bool' # and the callsite expects 'void'. call-me bool(a bool) a
  • From special expressions: Any expression not at the end of a block must be void.
    The condition of an if, elif, unless, while, or until must be a bool. main void() () # void if () # bool () # void (since the 'if' must be void, so must the branch)
  • From a type on a local variable: The initializer for a local variable has the variable's type as its expected type. main void() x nat = () # Converting string to json to string causes it to be quoted info log "{x}"
  • From an inline type annotation: There is a ::t syntax that provides an expected type for the expression to its left.
    main void() info log "{1.5::float.to::nat.to::json}"

Contexts that do not have an expected type

  • For a local variable without an explicit type: When you write x =, the only way to determine the type of the expression to the right of the = is to check it without an expected type.
    (The type-checker doesn't try to infer based on where the variable is used later.) main void() x = () # This will fail to compile info log "{x}"
  • When there an unresolved overload: If there are multiple functions with the same name that can't yet be disambiguated, an argument won't have a single expected type. main void() () foo # This will fail to compile foo void(_ nat) () foo void(_ string) ()

    However, it can sometimes be disambiguated when only one of the overloads makes sense.

    main void() bar foo foo void(a nat) info log "got nat {a}" foo void(a string) info log "got string {a}" bar nat() 3 bar int() 4 In the above example, the call to bar must return a nat or string (the possible parameter types of foo), so the bar returning int is ignored.

Overloading is safe

Since there are no implicit conversions in crow, overloading is always unambiguous.
That means that adding a new function will never silently change an existing function call.
Instead, it will always be a compile error if multiple overloads match.

main void() info log 7.foo foo string(_ nat) "nat" foo string(_ int) "int"

Type checking runs left-to-right

Crow always checks expressions top-to-bottom and left-to-right.
It never goes back to re-check something based on future information.

When crow looks at all the overloads for a function, it first filters out those that can't return the expected type (if any). Then it checks arguments left-to-right, filtering out overloads that can't accept the actual argument types.
That means that in general, first argument of a function will often not have an expected type, while the last argument usually will.

main void() x nat = 0 a = 0::nat == x b = x == 0 info log "{a} {b}"

In 0::nat == x, the annotation 0::nat is needed because 0 is the first thing checked and there are many == functions that take a numeric type.
In x == 0, x is checked first and is known to be a nat. So, the only remaining == overload is == bool(a nat, b nat).