Skip to content

The future of types #2

Description

@distractedlambda

I wanted to jot down thoughts that have brewed in my head since we put type-system work on hold; eventually, I'm hoping this could yield a design sketch to guide implementation.

Background

Why Types are Needed

In the strictest sense, Pille does not really need much; it needs to be able to ascribe a machine representation to each and every value produced by the program, but this can be (and is, in the bytecode language) represented by a small type grammar roughly matching a subset of LLVM.

In practice, however, I think Pille needs ways to associate an extensible set of static information with expressions and bindings; the spmd language is one concrete use-case, but more generally "type-driven metaprogramming" is ubiquitous in systems-programming languages (that aren't C), and IMO for very good reasons.

We could base our design off of the statinfo mechanism in Rhombus, but I don't think that that quite fits the Pille use-case. Rhombus does not really use static information to check assignability, and instead usually relies on runtime-checked annotations to enforce API invariants. With Pille, we need to be able to perform such checks statically by default, with runtime checking being some sort of opt-in or derived construct (if present at all). It's not immediately obvious to me how Rhombus-style static information would be adapted for assignability checks; we could get some sort of subtyping-like relation if were able to treat static information as lattice points, which seems like it might be possible, but also seems like it could introduce plenty of its own problems (especially in reporting "type mismatches"). Alternatively, I could imagine having an entry in static information that indicates some nominal type identifier, but at that point it feels like we have just added traditional types anyways.

Therefore, my current thinking is that Pille needs some sort of type system (where each value has a single type that you can talk about as its own thing, and in relation to other types in terms of assignability, convertibility, etc.).

Why The Current System Is Insufficient

Pille already has a static type system, which I attempted to make extensible and supportive of pseudo-polymorphic constructs. In particular, it supports definition of arbitrary new type constructors, it does not much constrain the arguments to those type constructors (they're just syntax objects), and it has a "type method" system that delegates certain extensible operations (such as implicit coercions) to types themselves (and which permits definition of new and arbitrary type methods).

Unfortunately, I've not found the extensibility to work out how I hoped. Type methods end up being an extremely low-level mechanism, and are somewhat error-prone in how they distribute functionality. For instance, implicit coercion is one type method, while picking "common types" (to resolve type mismatches that could be fixed with coercions) is another; it is up to the type definition to make sure these are implemented in a manner consistent with each other. Beyond what might have just been my own poor API-design choices, type methods suffer heavily from the bidirectional method problem when you try to express operations that relate multiple types (which turns out to be many useful operations).

More fundamentally, the type system does not assume any relationship between different instantiations of the same type constructor, and there is no notion of holes or type variables; accordingly, there is no way to express "partial constraints" in the bidirectional type system, which would seem to hinder its power. For instance, an operation that knows it needs someInt(...) from a subexpression, would still not be able to give an expected type for that subexpression, unless it knows that it needs exactly an Int(N) for some N. Along the same lines, there is no standardized notion of polymorphism or parametriticity, not even in the sense of e.g. C++ templates, which means that generic operations end up using hand-written type-dispatching code (for which it is not always straightforward to document a "type signature").

Design Thoughts

Research?

If a new type system is something that could itself be an interesting contribution--maybe because it does something new and interesting with types and macros, or maybe just because it does something new and interesting with types in the systems-programming context--then it would seem like something to really dig into, and plan to write a paper about.

If that's not going to be the case, then the goal changes to unblocking other avenues of exploration. To be clear, I think the current type system is a blocker, but there's possibly a large delta between what would sufficiently un-block, and what would actually be an intellectually-interesting thing.

"Checked" Generics?

Looking at prior art (C++, Zig, Rust, Swift, etc.), a major design point would seem to be whether "generic" code is type-checked on its own (Rust, Swift, seemingly Mojo), or if instead type-checking is just part of monomorphisation (C++, Zig). The former seems a massive win for tooling (and, as a consequence, language ergonomics), but in my experience languages that take that path end up playing catch-up to the latter in terms of compile-time expressiveness.

Since C++ and Zig defer much of their semantic analysis until monomorphisation, they get to keep most interesting semantics defined in terms of concrete types. Checking of type-equivalence remains trivial, overload resolution can easily incorporate priority logic and side-conditions, and type-level computation can be more or less arbitrary (which in the case of C++ and Zig, means compile-time evaluation of runtime-language code). So, for instance, a return type can be expressed as an if condition on a parameter type, but type-checking only ever actually has to deal with the branch of the if that ends up taken on a given monomorphic path.

Rust and Swift aim to do all interesting type-checking and overload resolution prior to monomorphisation, and so they end up employing sophisticated parametric polymorphism with typeclasses, including dedicated (but limited) constraint languages; even so, they struggle to do quite the same things that C++ and Zig enable. Rust is still working on support for specialization, for instance. A part of me wants to try to push the needle here, since somehow having the tooling of Rust/Swift and the power of Zig is extremely appealing; the rest of me feels in no real position to contribute, given the resources that these production languages are already putting into it.

Where Do Macros Fit In?

When I initially started exploring types in Pille, I imagined a powerful back-and-forth between macros and type-checking, and was ecstatic (well, maybe not quite) about macros that could observe and react to contextual type information (something Lean also supports). The more I dwell on this, though, the more I feel like this is probably not a good idea for Pille.

In a language in which each bit of surface syntax is type-checked once, it probably can work well (Lean would seem to be evidence); the problem I'm seeing is in Zig/C++-style typechecking-during-monomorphisation, where a piece of surface syntax in a generic context cannot be type-checked until a use-site, and may end up being type-checked multiple times (with different concrete types). If types can alter the interpretation of syntax (by way of a macro that observes the expected type), then a corollary in this context is that you don't know the interpretation of syntax at the initial definition of generic code, and also that there could be multiple interpretations in effect simultaneously in a full program. This seems like an unacceptable compromise to me.

The direction which I imagine now is somewhat less exciting: there would be a type-oblivious surface language, built out of traditional type-unaware macros, and the result of expanding those would be an intermediate form that we somehow know how to type-check and monomorphize. Then, likely, this intermediate form would also allow a different kind of macro ("typechecking" or "monomorphisation" macros?), but these would not be able to call the surface-language expander on sub-terms (because of the aforementioned issues with delaying this, and also because the original expansion-time environment of the surface-language would have been lost by this point).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions