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
Each instantiation creates a possibly duplicate instance of the function which a linker must deduplicate.
Errors are surfaced to the wrong programmer: If generic function f calls generic function g incorrectly, an error will be surfaced to whoever calls f, rather than the author of f who wrote the incorrect call.
Each of these are concerns in large C++ codebases. The former affects build times, and library authors go to great lengths to reduce template instantiations. The latter was partially addressed by C++ Concepts added in C++20.
We want a mechanism that will reduce the pain for each of these issues. Ideally it would:
Provide a mechanism that allows us to reduce the number of required generic instantiations.
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:
callable(i32) represents functions that can be passed an i32.
callable(i32, bool) represents functions that can be passed two arguments, the first being an i32, and the second being a bool.
callable(bool, n: i32) represents functions that can be passed two arguments, the first being a bool, and the second being an i32, which may bind to a named parameter with name n.
callable(callable(bool)) represents functions that can be called with a single argument that could be any function-type callable with a single bool parameter.
callable(bool; i64) represents functions that are callable with a single boolean argument and return an i64 (note the semicolon separating arguments from return types).
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.
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
f
calls generic functiong
incorrectly, an error will be surfaced to whoever callsf
, rather than the author off
who wrote the incorrect call. Each of these are concerns in large C++ codebases. The former affects build times, and library authors go to great lengths to reduce template instantiations. The latter was partially addressed by C++ Concepts added in C++20.We want a mechanism that will reduce the pain for each of these issues. Ideally it would:
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 ofa + b
or with which typesf
can be invoked; only thatf
can be invoked with whatever the type ofa + 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:
callable(i32)
represents functions that can be passed ani32
.callable(i32, bool)
represents functions that can be passed two arguments, the first being ani32
, and the second being abool
.callable(bool, n: i32)
represents functions that can be passed two arguments, the first being abool
, and the second being ani32
, which may bind to a named parameter with namen
.callable(callable(bool))
represents functions that can be called with a single argument that could be any function-type callable with a singlebool
parameter.callable(bool; i64)
represents functions that are callable with a single boolean argument and return ani64
(note the semicolon separating arguments from return types).Also missing from above is the important distinction of whether it is called with
f(a, b)
ora'f(b)
syntax. These differ in that the latter will look in the module associated witha
s type to findf
, 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
, thenT ~ sortable
would evaluate to true if and only ifT
adhered to thesortable
interface.In function parameters, this would look like:
One can also bind a type with the backtick:
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 anysortable
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.