Typechecking BaseLanguage

Project Home open_in_new

Typechecking BaseLanguage

Short introduction to the architecture of BL-specific type system built with CodeRules.

This sample is the main result of developing code rules. It is still work in progress, but the main areas of type checking BaseLanguage have been covered. Here we briefly touch on the implementation details.

First, all the BaseLanguage types have corresponding dataforms, and in addition there are definitions of types that are only ever used during type checking, such as capture type.

The macros in Types macro table define the rules how types are constructed, ensuring, among other things, that bounds on type parameters are correctly processed.

(examples of type dataforms)

Primitive types define allowed ranges, so the dataforms for these types have value slots capturing literal’s value.

(examples of primitive type dataforms)

There are two queries defined for type checking and for converting a type to another type. The latter is used when testing for subtyping. ConvertsTo query expands both its parameters before activating convertsTo() constraint, which launches the processing of the relation.

(the production from ConvertsTo query)

(the production from Typecheck query)

Typechecking itself starts with activating the checkAll() constraint, triggering the productions responsible for assigning types to literals, this expression, as well as processing type annotations — ensuring that these are built without violating bounds.

The rule for variable declaration is quite trivial: the type annotation gets expanded to dataform and assigned to the source location with typeOf() constraint.

(assigning the type to a variable declaration)

An integer literal is simply assigned the type int with the value being the value of literal.

(assigning the type to int literal)

From these starting points type checking continues up the syntax tree until there are no more productions left that can be triggered.

A dot expression propagates the type from operation to the whole expression.

(typechecking of dot expression)

Whereas assignment does something more: it ensures that the actual type on the right is compatible with the type on the left.

(typechecking of assignment expression)

Type Relations

Several kinds of relations on types are defined, surveyed in the following table.

Relation Constraint Description
compatibility compatibleWith() generalisation of conversion to include void type
conversion convertsTo() generalisation of subtyping, LSP
primitive subtyping primSubtype() subtyping among primitive types
subclassing promote() subtyping among classifier types
containment containedIn() type parameter containment


Constraint convertsTo() ensures that a type can be converted to another type, that is a type A can be used instead of type B. To test if a type is acceptable in certain locations, including those that allow any type, the constraint compatibleWith() is used, which delegates to convertsTo().

(resolution of compatibleWith() via convertsTo())

In its turn, convertsTo() delegates to either primSubtype(), which is responsible for solving primitive subtyping, or to promote(), which focuses on subtyping of classifier types.

(convertsTo() delegates to promote() for classifier type)


Resolution of promote() constraint, which represents subtyping among classifier types, is implemented around a simple idea of representing all subclass paths the root (Object) to a classifier as a set of lists. This representation deliberately ignores the class parameters. As a first step, the shortest path from supertype’s classifier to subtype’s one is selected. This path is then reversed and represented as a dataform list. This makes it possible to pattern-match on this list, since it is nothing more than a cons list represented as a dataform.

Subtyping is a reflexive and transitive relation, so promote() is replaced with another constraint dpromote(), which triggers one of the two productions responsible for either aspect of the relation. Transitivity is solved by advancing up the supertype path keeping track of all type variables and ensuring all bounds on type parameter are satisfied. Reflexivity delegates to containedIn() to ensure parameters are within bounds, which in its turn delegates to convertsTo().

Take a simple example of Long classifier type — the boxed long. Its superclasses are written as follows.

  Long : Comparable : Object
  Long : Number : Serializable : Object

Suppose we need to decide if Long <: Serializable, that is if Long is a subtype of Serializable. The shortest path between those consists of three nodes : [Long, Number, Serializable]. Constraint dpromote() is activated with this list as the 3rd parameter, and it requires two steps of inductive production and one step of reflexive to solve this relation.