← Back to projects
Summary

Gazprea is a research language whose whole point is rich types: sized vectors and matrices, tuples, structs, intervals, and user-defined aliases, with implicit promotion rules between them. This is the subsystem that decides what every expression's type is and what is allowed to become what. It is the largest piece of the static-analysis half of the compiler and the one where being wrong is quietest, since a bad promotion produces a program that compiles and computes the wrong answer.

Stack

What it's built with.

C++17Type SystemsPolymorphic HierarchiesStatic Analysis
Details

How it works.

Resolution and checking are different jobs

A common shortcut is to infer and check at once, deciding an expression's type and validating it in a single traversal. It works until the language has promotion rules, at which point the check has to know the type of something that has not been resolved yet, and the pass grows special cases that run in tree order rather than dependency order.

Separating them means resolution's only job is to annotate every node with a concrete type, and the checker's only job is to ask whether the now-fully-typed tree obeys the rules. It costs an extra traversal and it makes both halves explainable, which matters when a fourth type kind gets added late.

Unknown is a type, not a failure

Representing an unresolved type as a null pointer means every consumer either checks for it or crashes, and in practice they do not all check. Making UnknownType a real member of the hierarchy means an unresolved node flows through the passes like any other type and gets reported as a diagnostic at the point where a concrete type was actually required, rather than as a segfault three passes later with no line number attached.

Highlights

The things I'm proudest of.

  • ▹Eight type kinds under one polymorphic base: BasicType for scalars, ArrayType and VectorType, TupleType, StructType, IntervalType, TypeAliasRef for user-defined names, and an explicit UnknownType rather than a null, so unresolved types are a value the passes can reason about instead of a crash waiting to happen.
  • ▹Factored the shape-carrying types behind a shared ShapeProvider interface, so the code that cares about dimensions works against that rather than testing which concrete type it holds.
  • ▹Modelled type aliases as a reference node in the hierarchy rather than by eagerly substituting the underlying type, which keeps the user's own name available for diagnostics instead of reporting errors in terms the programmer never wrote.
  • ▹Split the rules into three services rather than one god-class: promotion (which implicit widenings are legal), casting (which explicit conversions are legal), and conversion (how a value actually changes representation). They answer different questions and mixing them is how a type system acquires contradictions.
  • ▹Resolution runs as its own 3,374-line pass that annotates the tree with concrete types before the checker runs, so type checking is a verification step over a fully typed tree rather than an inference pass that also enforces rules.