asoffer / Icarus

An experimental general-purpose programming language
Apache License 2.0
9 stars 2 forks source link

Interfaces proposal, part 1. #111

Open asoffer opened 2 years ago

asoffer commented 2 years ago

Context

What This Proposal Is

This is a proposal to add a mechanism by which one can validate that a value's type satisfies certain requirements before being passed to a generic function/scope/struct.

What This Proposal Is Not

This proposal is not a general purpose mechanism for constraints on compile-time values. Such a mechanism would likely be valuable, but we believe the desired semantics for types are sufficiently different that it would not be valuable to design these mechanisms simultaneously.

Requirements and Motivation

While generic functions, scopes, and structures fit firmly with the goals of Icarus, one concern we have is the ability to scale compilation in the presence of generics. If a generic function cannot be compiled until types of arguments at the call site are known, a large swath of common code ends up not being compilable early. This means that

We want a mechanism that will reduce the pain for each of these issues. Ideally it would:

  1. Provide a mechanism that allows us to reduce the number of required generic instantiations.
  2. Surface errors if a function cannot safely be called, even if it is in a generic context.

Proposal

We propose adding a mechanism to specify the interface that a type adheres to. The mechanism not only needs to be a predicate on types (to determine of the type matches the predicate), but also sufficiently expressive that we can apply a type-checking pass even in generic contexts.

Note that C++ Concepts suffice as predicates, but do not enable type checking in generic contexts. Specifically, a concept may tell us that f(a + b) is syntactically valid, but this is insufficient for type-checking. We don't have any information about the type of a + b or with which types f can be invoked; only that f can be invoked with whatever the type of a + b is.

Defining an interface

The first part of the proposal is for a builtin function callable, which accepts a collection of types or interfaces and returns an interface representing the predicate that an object of a type adhering to the interface is callable with values of types passed as arguments.

A few examples:

Also missing from above is the important distinction of whether it is called with f(a, b) or a'f(b) syntax. These differ in that the latter will look in the module associated with as type to find f, which is relevant to callability.

To handle this case, we propose passing all of the types which may be used for argument dependent lookup in a bracketed list before the parentheses, as in: callable[Ts...](Args...). Omitting the brackets defaults to an empty list, avoiding argument dependent lookup entirely.

There will be more to follow in future proposals, but this seems to be a reasonable primitive operation.

Using an interface

Interfaces can be used directly in pattern matching. Supposing we had an interface sortable, then T ~ sortable would evaluate to true if and only if T adhered to the sortable interface.

In function parameters, this would look like:

(v: ~sortable) -> () { ... }

One can also bind a type with the backtick:

(v: ~sortable`T) -> () { ... }

This enables us to check the body of the function to ensure that it only uses v in ways that adhere to the prescribed interface, ensuring that it will work with any sortable type.

This also enables us to precompile a type-erased version of the function that is usable during compilation of any dependent modules. While not the most efficient implementation, it can be verified correct independent of its usage. This also allows us to delay actual instantiations of generics until a linking step. Details on this will be in another proposal to be written.