Skip to content
This repository has been archived by the owner on Sep 16, 2021. It is now read-only.

Plan: 0.5.0 Generics

Tom Barham edited this page Dec 11, 2018 · 1 revision

This is a planning document for the 0.5.0 Generics feature. The described feature is in flux and probably will be different from the final implementation, but any feedback is welcome and will be used to further evolve the feature.

High-Level Description

Generics allow calling a function with different types of arguments, resulting in multiple copies of that function being generated. In Maxim, all functions are declared by the compiler, so this feature mainly involves allowing functions in the compiler to easily be declared for multiple types at once. From a user perspective no syntax changes, however where applicable functions will allow being called with more types.

A major example of where this would be useful is the delay function (and several related functions). Currently this function only allows delaying num values by a certain duration, however there's no real reason why this shouldn't also be usable with MIDI and symbol values, as well as any possible future types.

There are two possible ways these functions could be generated:

  • Generate all possible variants of each generic function when building the library at startup - probably faster/easier once started up, but especially for functions with multiple type parameters could slow down the library generation a lot.
  • Defer generation of each variant until it's first used - reduces library generation time but possibly at the cost of slower node generation time, but also might complicate linking (would we need a module for each variant? how would lifecycles of those be handled?)

There's also the question of if functions should be allowed to change their behavior slightly for each type. For example, most functions process a num values left and right channels independently (e.g. delay uses two separate buffers to allow a different left and right duration). But midi values don't have two channels, so it wouldn't make sense there.

Changes to the MIR

Currently, the target function of a call in the MIR is identified entirely by an enum specifying the name. This feature would require adding the actual types for each generic type parameter when specifying the target function. Quite a minimal change.

Function specifications would also need to change to support type parameters. The easiest way to do this would probably be:

  • Allow each function specification to define a number of type parameters
  • Allow parameter types and the return type to either be a concrete type or an index in the list of type parameters

Note: while this works fine for most cases, it notably wouldn't allow individual components of something like a tuple to be generic. Since this would likely involve a lot more complexity, and wouldn't be utilized by the standard library in it's current state, this limitation is fine for now.

Note 2: since functions (and hence generic functions) can only be defined internally in the compiler, it's assumed that the type parameter definition is valid (i.e this isn't checked other than the compile-time checks - breaking the rules will probably result in a panic at a later stage). Namely, this means:

  • Type parameter indices must be less than the number of parameters specified
  • If the return type is generic, the type index it uses must be used for a regular parameter as well (otherwise it can't be determined)

Changes to the AST->MIR lowering pass

When a function is called that takes type parameters, the lowering pass would need to be able to determine the concrete types for the parameters. Since type inferencing in Maxim is top-down, this should be possible in a relatively straightforward way by going through each parameter and filling in the blanks as needed. If there's a conflict (different types passed in for two parameters with the same generic type) an error can be displayed.

Changes to function generators

Function generators simply need to know the concrete types of their type parameters. From there, some generators will need to be adapted to handle the possibility of different types being passed in. This will also be necessary for getting the storage type of the function, for the data analyzer.