Cyen
Types

I may redesign types significantly in the future. This is just an early idea.

Cyen's type system is fully object-oriented.

Properties of a type

Cyen is strictly object-oriented, meaning that even primitive types take part of the type tree. Every type has a supertype (e.g. float inherits num), except any. In fact, any is a supertype of all types.

Cyen allows the ability to limit which types may inherit a certain type:

  • An open type may be inherited by any type.
  • A restricted type may be inherited only by types defined the same namespace.
  • A terminal type is prohibited from inheritance entirely.

Cyen’s types consist of two components: a definition and an implementation. Both are inherited from a supertype. The definition defines what the implementation should look like, and the definition of inherited types can be extended. The implementation defines the actual behaviour behind the definition.

Even though the object orientation pattern prohibits one type from inheriting multiple others, Cyen actually allows the inheritance of multiple types. This causes one problem, where definitions and implementations clash. If definitions clash, Cyen will prohibit you from inheriting both together.

Builtin type names

Builtin types are basic types default to Cyen. Their names are reserved type names and may not be redefined. However, they are only reserved type names: you may in fact just name your variable or function after one (e.g. it’s perfectly valid to make a variable of type float and name it float as well).

Cyen’s builtin types provide the basics for certain language operations. Not every

The following type names are builtin:

  • any: The supertype of all types
  • num: The supertype of all numbers
  • u8: An unsigned 8-bit integer
  • u16: An unsigned 16-bit integer
  • u32: An unsigned 32-bit integer
  • u64: An unsigned 64-bit integer
  • s8: A signed 8-bit integer
  • s16: A signed 16-bit integer
  • s32: A signed 32-bit integer
  • s64: A signed 64-bit integer
  • f32: A 32-bit floating point number
  • f64: A 64-bit floating point number
  • int: Alias for s32
  • uint: Alias for u32
  • float: Alias for f32
  • long: Alias for s64
  • ulong: Alias for u64
  • double: Alias for f64
  • bool: A boolean type, either true or false
  • str: A character string, similar to [!u16]
  • type: A meta type

However, the type tree involving these types is a bit more complicated, and involves some more advanced types. These advanced types are named by special syntax, to not pollute the type namespace with too much builtin names: <type>. It is illegal to define a type with such naming, but they are valid type names for Cyen’s advanced builtin types.

Cyen’s advanced types are:

  • <incr>: An open type that allows incr to be applied.
  • <decr>: An open type that allows decr to be applied.
  • <comparable>: An open type that allows the use of comparison operators such as < and >.
  • <sequential>: An open type that inherits <incr>, <decr> and <comparable>, allowing to be used as a range.
  • <signed>: An open type to allow the use of unary + and - operators.
  • <integral>: A restricted subtype of num for all integral types. This allows the use of bit shifting operators.
  • <bitlogic>: An open type that allows the use of bitwise logic (&, | and ^).
  • <bitshift>: An open type that allows the use of bitwise shifts (&, | and ^).
  • <decimal>: A restricted subtype of num for all floating point types.
  • <iterable>: An open type that allows iteration syntax using a for loop.
  • <generator>: An open type that generates a finite or infinite stream of values.
  • <sizable>: An open type that has a length that can be read through the length operator: #val.
  • <sequence>: An open type inheriting <sizable> and <iterable> that allows reading values through indexing[syntax]. The <iterable> is implicitly implemented by this type.
  • <array>: An open type inheriting <sequence> that allows writing values through indexing[syntax] = value.

Composite types

Composite types are types that are made up of other types. Composite types may be defined by the language syntax, or by the programmer. Cyen’s builtin composite types are arrays, sequences, tuples and optionals.

Arrays and sequences

An array is a mutable collection of zero or more elements of one specific type. For example, [int] is an array whose elements are of type int. The length of an array cannot change, but its elements can.

A sequence works exactly the same as an array, except one may not set its values. For example, [!int] is a sequence whose elements are ints.

Arrays and sequences may have their length defined in their type name: [int*5] or [!int*5]. Fixed-length array types inherit from any-length array types.

One can cast an array to a sequence, but not vice versa. Casting an any-length array to a fixed-length array is possible, but will fail if the actual array length is not the required array length. A fixed-length array is per definition assignable to it’s matching any-length array type.

Arrays also inherit from the array types of its elements’ supertypes. E.g. [int] inherits from [num].

Tuples

A tuple is an immutable collection of two or more elements whose types may be entirely independent. Tuples are always fixed-length. Their order is fixed. For example, (int, str) is a tuple of an int and a str.

It is impossible to set the values of a tuple. Getting the values of a tuple is only possible by assigning the entire tuple to a bunch of variables.

(a, b, c) = (6, 2, 1)
println a   ~~ 6
println b   ~~ 2
println c   ~~ 1

~~ If you need one specific value only, assign others to 'void'
(void, a, void) = (6, 2, 1)
println a   ~~ 2

Optionals

An optional type is a type that may be null. For example, int? is either an integer value, or null. They can be acted upon just like its internal type, but it will implicitly dereference and cause exceptions if the value is null. I.e. one may add an int and int?, but if the latter is null it will fail. Casting an optional to a non-optional type will fail if the optional is null. Non-optional types and optional types may be assigned interchangably, but assigning an optional type to a non-optional type will fail if it’s null.

An optional of an optional is illegal: it is not allowed to have a type like int??.

Note that since any optional type also inherits any, it is perfectly legal to assign null to any.

Diagram

The following diagram displays inheritance between Cyen’s builtin types. The green types are open types, yellow types are restricted types that may only be inherited by other builtin types, red types are terminal types, blue types are aliases and purple types are composite types.

Cyen type diagram

Type definition

An important feature of a language is the definition of types. Cyen allows definition of types in several different ways.

Alias

The easiest way to define a type is to directly refer to another type. This is what is done with some numeric types for easier naming. E.g. float is an alias for f32. Aliases may not be written for composite types.

To define an alias, write:

alias byte: u8

Struct

A struct is a simple user-defined type that inherits any. It has a bunch of assignable fields.

To define a struct, write:

struct StructName:
    field1: int
    field2: int
    field3: int = 3
::

To instantiate a struct, simply call the struct as a function and supply all field values in order, or supply no values for the default struct values. You may then read the fields however you’d like.

loc myStruct: StructName = StructName(6, 2, 1)
println myStruct.field1  ~~ 6
println myStruct.field2  ~~ 2
println myStruct.field3  ~~ 1

loc defStruct: StructName = StructName()  ~~ Default struct value
println myStruct.field1  ~~ 0  (default for unassigned 'int' fields)
println myStruct.field2  ~~ 0
println myStruct.field3  ~~ 3

You may define a custom struct constructor:

struct StructName:
    field1: int
    field2: int
    field3: int = 3
::

@ int, int
constructor StructName(a, b):
    field1 = a
    field2 = b
    field3 = a + b
::

~~~ Or alternatively write
@ int, int
constructor StructName(a, b) -> constructor(a, b, a + b)
~~~

loc myStruct: StructName = StructName(6, 9)
println myStruct.field1  ~~ 6
println myStruct.field2  ~~ 9
println myStruct.field3  ~~ 15

A struct may, by definition, not be inherited.

Record

A record is similar to a struct, but its values may only be assigned once. It allows its constructors to write its values (and they may even write them multiple times), but the values may not be written elsewhere. They may only be read. Just like with structs, records have a default constructor that takes all fields as arguments. However, they do not have a nullary default constructor.

Notable is that if a constructor does not assign a record field, the field’s value is forever the default value.

record RecordName:
    field1: int
    field2: int
    field3: int = 3  ~~ You may in fact pre-assign fields for custom constructors
::

@ int, int
constructor RecordName(a, b):
    field1 = a
    field2 = b
    field3 = a + b
::

@ int
constructor RecordName(a):
    field1 = a
    ~~ In this case, field2 is always 0 and field3 is always 3
::

loc myRecord: RecordName = RecordName(6, 9)
println myRecord.field1  ~~ 6
println myRecord.field2  ~~ 9
println myRecord.field3  ~~ 15

Like a struct, a record may not be inherited.

Union

A union type allows a bunch of differen types to be assigned, but only stands for one value. An union looks like a struct with fields, but internally only one field is assigned. Reading a union’s field will dereference the field value, but if that field value was not assigned it will fail.

While records and structs behave almost nearly like classes, unions behave a bit differently. A union is defined like a struct and a record:

union UnionName:
    field1: int
    field2: float
    field3: str
::

Unlike with structs and records, fields may not be assigned within the union. Neither may the union have constructors or methods. To instantiate a union, we instead call one of its fields as a static member of the union:

loc myUnion: UnionName = UnionName.field2(3.1415)

println myUnion.field2  ~~ 3.1415
println myUnion.field1  ~~ Fails

Class

Classes are the most flexible way to define types, as they allow inheritance. However, I wanna dedicate this to another article.

class Vec2
inherit <signed>

fixed x: float
fixed y: float

@ float, float
constructor(x, y):
    me.x = x
    me.y = y
::

@ -> float
func magnitude:
    return math.pythagoreanSolve(x, y)
::

@ -> Vec2
inherit func neg:
    return Vec2(-x, -y)
::

Hi! I'm Samū, a furry, artist and game developer from the Netherlands. This is my blog, where I write about my projects and ideas.

Copyright © 2023 Shadew

All rights reserved

Powered by Jekyll

License - Privacy statement