Type-C: Type System

2023-12-24

Type-CType-V

The name Type-C, actually comes from two things. The first is the fact that it is a statically typed language and rich in terms of data types, the second is C, the language that inspired it. In this post, I will be talking about the various data types that type-c supports. Some interesting types are intentionally left out, such as variant, string, array and process. These will be addressed in a separate post. Hence in this post we focus on: Basic Types, Enums, Structs, Interfaces, Classes, Joins, Nullables and Functions. We finalize by looking into the strict type modifier.

Data types:

Here are the various data types that type-c supports:

  • Integers: i8, i16, i32, i64, u8, u16, u32, u64
  • Floats: f32, f64
  • Booleans: bool
  • Enums: enum
  • Strings: string: Implemented as built-in interface and part of the VM runtime library.
  • Arrays: T[]: Implemented as built-in interface and part of the VM runtime library.
  • Structs: struct
  • Interfaces: interface
  • Classes: class
  • Type Join: U & V where U and V must be interfaces or joins themselves.
  • Nullables: T? where T is a class, interface, join Type or Process.
  • Variants: variant
  • Processes: process
  • Functions: fn (x: U) -> V

In this post, I will be talking about a few of those. Most likely, variants, strings, arrays, and processes will get their own post each. So we are simply addressing the rest.

Integers & Floats

Integers are the basic building blocks of any programming language. Type-C supports 8, 16, 32 and 64 bit integers. Their size is fixed. Since type-c is a statically and strongly typed language, some integers are not implicitly converted to other types, when there is a potential loss of precision. For example, the following code will not compile:

However,

In the virtual machine, casting is done by using the following cast instructions:

InstructionDescription
cast_i8_u8Casts the value in register R from i8 to u8
cast_u8_i8Casts the value in register R from u8 to i8
cast_i16_u16Casts the value in register R from i16 to u16
cast_u16_i16Casts the value in register R from u16 to i16
cast_i32_u32Casts the value in register R from i32 to u32
cast_u32_i32Casts the value in register R from u32 to i32
cast_i64_u64Casts the value in register R from i64 to u64
cast_u64_i64Casts the value in register R from u64 to i64
cast_i32_f32Casts the value in register R from i32 to f32
cast_f32_i32Casts the value in register R from f32 to i32
cast_i64_f64Casts the value in register R from i64 to f64
cast_f64_i64Casts the value in register R from f64 to i64
upcast_iUpcasts the value in register R from given bytes to target bytes
upcast_uUpcasts the value in register R from given bytes to target bytes
upcast_fUpcasts the value in register R from given bytes to target bytes
dcast_iDowncasts the value in register R from given bytes to target bytes
dcast_uDowncasts the value in register R from given bytes to target bytes
dcast_fDowncasts the value in register R from given bytes to target bytes

As you can see, in the VM instructions there are no direct casts between some of the types, such as u8 and f32. The idea is, to ensure safety, we need to be explicit about the cast. This is why, we need to cast to i32 first, then to f32. This is done by the following code:

Let's try few samples:

First we upcast u8 to u32, then we cast u32 to i32, finally we cast i32 to f32.

Booleans:

Booleans are treated as u8. Any instruction that checks for a boolean value will check if the value is 0 or not. If it is 0, then it is false, otherwise it is true.

Same logic, we cast to from float to int, then downcast to i16, then cast to u16.

While this doesn't always guarentee the best performance, it reduces the number of instructions needed to perform the cast, and ensure a good level of safety.

Enums

Enums are very basic, the most interesting part is how they are represented in the VM. Type-C allows you to choose a specific type on how to represent the enum.

You can only an integer type, signed or un signed. By default, it is i32.

Structs

Structs has been addressed previously, but they are flexible data structure, designed to be used for grouping data together.

When checking for type compatibility, type-c checks for structural compatibility. This means that a function call to printPoint with a Point struct will work just fine. However, if the field have different types, then it will not work.

Interfaces, Classes, Join Types and Nullables

Interfaces are the most interesting part of type-c. They are the most powerful feature of the language. Type-C is not a fully object oriented language, but it does support some object oriented features. Type-C many drops OOP in favor of interface oriented programming. This means that classes cannot extend other classes. Instead, classes can implement interfaces. Interfaces are a set of methods that a class must implement. This is similar to the concept of interfaces in Java or Go. Interface can extend other interfaces, but not classes.

Now we can easily create an array of birds who can fly for example,

Types are allowed to be anynoumous, and inference is automatic.

Would be exactly the same as

The inference system is smart enough to group the interfaces together. This is called a join type. A join type is a type that is a combination of two or more interfaces. This is similar to the concept of intersection types in typescript.

Now the last type is nullable. Nullable is a type that can be null. This is similar to the concept of nullable types in typescript. TypeC supports the denull operator !! and the optional chaining operator ?..

Strict Types

The last category of types is strict types. strict is not really a data type but rather a type modifier. It is used to indicate that a type is strict. Strict type forces the compiler to check for strict structure equality. This means that the following code will not compile:

This is because the type of x is not structurally compatible with the value it is being assigned to. This is similar to the concept of strict types in typescript.

Strict types are useful when dealing with FFI, since you will need to ensure that the data is exactly the same as the one you are expecting.

Strict types can be used with interfaces, classes and structs. Since at its core, strict is a feature designed to fix the layout, as long as the layout is guarenteed, strict can be very flexible:

The previous code will work just fine, because fly and swim are aligned in the same order in the interface and the class.

Conclusion

Now is your chance to criticise the design of type-c type system. What do you like? What do you dislike? What would you change? Feel free to let me know.

Articles written in this blog are my own opinions and do not reflect the views of my employer. Content is original unless stated otherwise, and licensed CC BY 4.0. Some passages may be AI-polished for clarity.