I was thinking about what type system I wanted for ten and I think I made up my mind on many parts.
Scalars
I like Zig's arbitrary length integers, so if the implementation permits it (I still don't know how the language will be implemented) I will also do it.
So:
- signed ints:
i8
,i16
,i32
,i64
,i128
maybeiN
- unsigned ints:
u8
,u16
,u32
,u64
,u128
maybeuN
- sizes:
usize
,isize
For floats I guess there aren't a million of them:
- floats:
f16
,f32
,f64
,f128
maybef80
Of course, booleans: true
and false
I was thinking about a char type that would hold a single unicode character, but unicode is very hard so I'll keep this for later or for the stdlib.
The last thing to explore is native vector types. It might be interesting to have language support for them.
Void
I also need a void type for functions that don't return anything useful.
const print = fn(message: []u8) -> void {
// prints to stdout
};
I think the void
keyword is clear enough. It's what functions that do side
effects return.
Optional Types
Of course, optionals are explicit.
const maybe_number: ?i32 = 42;
const nothing: ?i32 = null;
if (maybe_number) |value| {
// value is i32 here
dbg.print(value);
} else {
dbg.print("no value");
}
Strings
Now I don't know about strings. Strings are hard. I think the first iterations of the language will just use a byte array with no understanding of it (like what Zig does).
Struct
Structs for record-type data.
const Point = struct {
x: f32;
y: f32;
};
const Person = struct {
name: []u8;
age: u32;
// Self-reference MUST be optional to avoid infinite type expansion
friend: ?Person;
};
// Usage
const p1 = Point{ x = 1.0, y = 2.0 };
const alice = Person{
name = "Alice",
age = 30,
friend = null
};
When a struct references itself (like Person.friend
), the reference must be
optional (?Person
) to prevent infinite type expansion.
I was considering two syntax options: const Point = struct {}
versus struct Point {}
. At first I thought the special syntax would be simpler, but after
thinking about generics and looking at Zig's stdlib, I realized most types
need comptime code for validation, optimization, and choosing implementations.
So I'm going with first-class types and functions:
const List = fn(T: comptime Type) -> Type {
return struct {
items: []T;
const add = fn(self: *@This(), item: T) -> void {
// methods can use T directly
};
};
};
const add = fn(a: i32, b: i32) -> i32 { return a + b; };
const IntList = List(i32); // Generic instantiation
const operations = [_]fn(i32, i32) -> i32{ add, subtract };
This gives maximum composability and makes comptime programming natural. Functions and types are just values you can pass around, compute, and combine. The order dependence is worth it for this level of power.
At the language level, struct fields will use pointer semantics by default. The
memory system will use custom allocators where pointers are offsets within
memory zones, avoiding traditional pointer dereferencing overhead. However,
there should be a way to force direct inclusion of a struct within another
struct. An inline
keyword would serve this purpose.
const Person = struct {
name: []u8;
age: u32;
friend: Friend; // pointer by default - stored as reference
inline address: Address; // forced inline inclusion - stored as value
};
Now packed struct might also be something required for some usages.
Tuples
I was wondering if I should include tuples. They are basicaly anonymous struct.
I think it will wind up to the implementation and requirements.
But if I do, I guess I would use a syntax like: {a, b, c}
as constructor, but
it could clash with block delimiter, I will have to study this in more details.
But this is a syntax problem.
Array
Real array, so a consinuous block of memory is a requirement for any low level programming and I want to support this.
I guess I will use the same approach as zig. [4]u8
is a 4 bytes array and
[]u8
is a slice, a slice being a pointer with length.
But I want a higher level memory management (and safety) than zig, so I'll have to think about it.
List
Lists will be implemented using structs in userland. I don't want to include them at the language level, this keeps the language core small. Use the standard library for that.
Union sum type
I think sum types are crucial for a good type system. They let you model "this OR that" relationships cleanly.
I'm planning to use enum
for tagged unions (safe) and union
for raw unions
(unsafe systems programming):
// Tagged union - compiler tracks which variant is active
const Color = enum {
Red; // no payload (implicit void)
Green; // no payload
Blue; // no payload
Custom: []u8; // has payload (color name)
RGB: struct { r: u8, g: u8, b: u8 }; // struct payload
};
const red = Color.Red;
const custom = Color.Custom("purple");
const rgb = Color.RGB{r = 255, g = 0, b = 128};
// Raw union - you manage the variant yourself, no tag
const Data = union {
i: i32;
f: f32;
};
The tagged version is what you'll use 99% of the time. The raw version is for when you need maximum control. I guess an activation system like zig, which is enabled in debug could also work to make them safe.
Other types I need to think about
There are a few more types that will probably be needed:
Error types - I want a special error union system (not regular enums) for functions that can fail:
const parse_number = fn(text: []u8) -> !i32 { ... }; // might fail
Pointers - For systems programming, I'll need explicit pointers:
const ptr: *i32 = &value; // single pointer
const many_ptr: [*]i32 = &array; // many-item pointer
But I don't know how to make them fit with the vision I have of the language (memory safety).
Type type - Since I have comptime, I need a type that represents types:
const GenericList = fn(T: comptime Type) -> Type { ... }; // Type is the type of types
Type
for the actual type-of-types. Like how i32
has type Type
.
I keep lowercase type
as a keyword.
Never type - For functions that never return (panic, exit):
const panic = fn(msg: []u8) -> never { ... };
These are more advanced and I can add them later. But error unions are definitely needed early on.