An Advanced Type System for Gerbil v0.19 #1504
Labels
No labels
UX
active development
backlog
blocker
bootstrap
bounty
bug
dependencies
discussion
documentation
duplicate
enhancement
flaky test
help wanted
invalid
javascript
question
release
tendentious
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
mighty-gerbils/gerbil#1504
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
An advanced type system for Gerbil v0.19
Extending binding forms for types
The expander already supports type attachment to runtime bindings; we should use this machinery and make expansion aware of types so that we no longer have to use
usingfor something that the expander can infer.All top level bindings can have a type, and we should have custom typed expansion for things like let so that the type can be derived from the declaration. No complicated type inference algorithm is required, just use the annotatations and the type of expression procedures.
so we have something like this:
this should work without requiring a
usingincantation.We can also extend let to accept type annotation sigils, at which point
usingbecomes completely hidden from typical user programs.This is also critical for generic type constructions.
Type Constructors and Macros
We need syntactic support for constructing types and macros at the type level.
We can define the builtin type constructors by extending deftype:
We can define macros for types with constructs like
defsyntax-for-type,deftype-ruleanddeftype-rules.These define macros that expand at type resolution time.
Derived type constructors can be defined as type macros.
Union Types
We need to support union types for otherwise disjoint types, not just homogeneous types.
Let's define a
(U type ...)constructor for union types, which must have a representation supported by the compiler.For instance, a union betwen a string and a list for psettings passed in things like
call-with-input-file:Parameteric Types
Type Erasure at runtime
let's not carry higher order types in the runtime; the runtime should only preserve
low level, MOP-mapped types for efficiency and dynamic compatibility.
So each higher order type should have a concrete runtime type.
Parametric Procedure Types
We need to make procedures higher order typed, with concrete type
:procedure.Let's say we introduce a procedure type constructor
For instance:
Parameteric container types
builtin things like
:pair,:list,:vector,:values,:box,:promise,:parametershould get type parameter(s).Pairs and Homogeneous Lists
List can be a derived type from
Pairand:null:Pairs have concrete type
:pairand lists have concrete type:list.Homogeneous Vectors
The homogeneous vector type:
Values
Values is a variadic type describing how many values it contains and their types:
(Values type)is equivalent totype.Box
the parameter restricts the type of the box value
Promise
The type is implied by the return type of the procedure it contains.
Parameter
The type parameter restricts the parameterization type.
Generics
This is where things get more interesting: we can introduce generic type parameter syntax, so that we can have procedures and types that generalize over parameters.
Let's define a syntax first: i propose reusing the
{}method notation for generic types (reader macro wraps with@method).So we can have generic type constructors for all parameteric types; here is how a generic procedure definition would look like:
and here is syntax for defining a generic class and a generic interface:
Constructing this object would require a type annotation to declare the concrete type since this cannot be generally inferred.
Let's say we generalize the
HashTableinterface to specify key and value types:But hash tables are still constructed by make-hash-table, which has no way of seeing the concrete type.
So we can declare this as:
This may be problematic however so maybe we need special syntax for specifying the generic arguments.
We can consider this:
but there is a conflict with dynamic method dispatch and applicative order of evaluation.
A heavy handed solution would be to use wrapper generic instantiation:
We can make this more palatable with a shortcut unicode notation sigil:
with the semantics being apply template construction to the operator of the s-expression.
Omitting the template constructor for a procedure for which the template cannot be inferred by the arguments
would leave all the template parameters as :t.
Variadic Signatures
we can type a variadic procedure with its definition, reusing the ellipsis token:
where
bis the variadic argument list in the body, and an implicit contract check is installed at call sites.This can also make
applytype safe.As an alternative, we can also use the
::token:this is compatible with the list constructor signature.
No Monomorphization
In general monomorphization results in high compile times and code bloat; it is also somewhat incompatible
with the dynamic nature of Gerbil, so it is out of scope for v0.19. We can consider it as an optimization for v1.0.
Dependent Types
The
:~contract annotation system already provides a path to express dependent types; the compiler willhave to understand those contracts so that it can propagate them to the point of usage.
For instance, the builtin
vector-refcould be defined with a range contract and the compiler should seethis dependent type and eliminate all runtime checks in a properly range checked loop and emit the unchecked
##vector-ref. Same forstring-ref, builtin homogeneous numeric vectors, and so on.In general, once the compiler understands contract dependent types we
should never have to write a
##primitive for vector/string/etcaccess; the compiler should do it automagically.
Revisiting Procedure Signatures
If we want to support dependent types on procedure signatures and perform reduction at call site we need
to carry the full contracts and optional defaults in the procedure signature; that also means we can validate
the dependent contract at call site and reduce to the unchecked variant of the procedure; closures can be automatically
reduced to their unchecked variant when they are passed to a properly typed receiver.
So how would signatures look? We need to have a form that names arguments so that dependent types can work.
I propose we use the
[]reader macro (@list) for writing named argumentsI think we should have this general form:
made some updates for consistency.
I think the procedure higher order types should have the full signature, that simplifies dependent types with predicate contracts and does not introduce yet another syntactic construct.
I think for the first increment we can skip generics -- even push it after v0.19 so that we keep the work bounded.
I made the proposal more concrete.
Towards an Advanced Type System for Gerbil v0.19
Objective:
Simplify syntax for accessing objects
Right now accessing object slots requires either a type signature at procedure level
or a
usingform. Local bindings, withletforms do not carry the requisite typeinformation for syntactically accessing slots without a
usingform.This can be resolved in two parts:
emits type signatures attached to definitions.
such that it infers the type of the binding by the value expression.
there are also two natural syntactic extensions that can simplify things:
they are already typed and the type can be inferred, useful for values.
of the
usingmacro.The expander will have to do a limited form of type inference for let bindings that lack an
explicit annotation. This will allow us to use dotted notaion throughout the local scope
without the noise of
usingblocks.In addition,
deftypecan be extended so that it is not limited to type alias, but generaltype constructors
Union Types
We already have a limited form of unions with the Maybe pseudotype for nullable values.
We can extend this to introduce a full union type which allows us to express disjoint type signatures.
The general syntax would be:
which creates an
or-like disjuniction of possible types.Higher Order Procedure and builtin contain types
Currently we type procedures at source level with just
:procedure. Similarly for values, pairs,lists, vectors, boxes, promises. We omit parameters for now as they introduce complexity.
This loses static type information that can help simplify check safety without runtime checks and
also complicates the usage of builtin container types.
The solution to this is to introduce higher order typed builtins. Thus the follwing:
The semantics are as following.
Procedure Types
The type is the complete signature, with named arguments and contracts.
This allows the return type inference in the expander, and argument contract checks which can be statically eliminated in the compiler.
When a procedure is passed to another procedure as value, the exact type can help the compiler
emit the unchecked version of the procedure if the argument contract can be satisfied statically.
Pair and List Types
Pairs have the car and cdr types attached, and the exact types can be attached.
Homogeneous lists have a single type parameter. There is also an equivalence between pairs
and homogeneous lists with a recursive parameteric type declaration:
Values
This allows us to automatically type the elements of a multiple return value expression.
Boxes
This allows us to automatically type the value of a box, with appropriate runtime checks and static elimination by the compiler.
Promises
This allows us to automatically type the value of a forced promise; the delay constructions can check the requisite return type
of the expression statically, similarly to how procedure return types are checked.
Dependent Types
By virtue of having the complete contract signature, including predicates, visible to the compiler we can eliminate range checks
and other constraints that we know are satisfied. The compiler can accomplish this with partial abstract evaluation.
The builtin type signatures can be extended so that range checks can be lifted and partially evaluated out (say in a loop that
checks bounds) so that the unchecked operator can be invoked.
For instance let's consisder this hypothetical signature and example:
here the compiler should understand the dependent type range end
eliminate all checks in the vector access in the body of the loop; it
should simply emit
##vector-ref.Parameteric Type syntax
It would be very useful to have a generic parameteric syntax for
type declarations, much akin to generics in other language, only
without the implicit monomorphization at compile time.
This is simply syntax to aid the type inference and allow us to
precisely type container access; the closest analog is generics in
java, with the associated type erasure.
Here we propose a syntax using the
{}reader macro which expands to@methodfor declarations. When the signature declaration, be itprocedure signatures, classes, or interfaces, has an
{?t ...}specification, it then denotes a parameteric type declaration.We can also extend the syntax of
deftypeto create parameteric type constructors.For procedures, the type parameters are signified as the first argument:
For classes and interfaces the parameters are the first argument, before the declaration had:
It is not always possible to disambiguate the parameters from arguments to procedures and methods; for instance take the
hash table constructor:
Invoking the constructor does not provide any way to infer the actual parameter types; for this i propose a special construct
in application syntax that disambiguates the type parameter using the sigil
▷as the first argument:Putting it altogether
This will require changes both in the core prelude (in :gerbil/core/contract) and the compiler.
The prelude will have to accomodate the syntactic extensions for supporting all this and the
compiler will have to be adjusted to understand the extended type family.
I started the discussion with astra.