Type system
Principles
Section titled “Principles”- Expressive but simple: algebraic types (sum types, product types), generics, pattern matching, no borrow checker, no lifetimes
- Explicit and “mathematically provable”: the compiler can reason about the code without lifetime analysis
- No classes: composition over inheritance, always
What exists
Section titled “What exists”- Structs (product types)
- Enums / sum types
- Interfaces (see section 9)
- Generics (see section 10)
- Exhaustive pattern matching
- Raw
union: byte reinterpretation, unsafe, for FFI and bit-tricks (escape-hatch; see section 5)
Declaring types: decl
Section titled “Declaring types: decl”One keyword, decl, declares struct, enum and interface, and the body says which of the three, as match commutes over the subject and loop repeats over the condition. There is no struct/enum/interface as separate keywords: the act is defining one of these three forms, and the body (fields, variants or signatures) disambiguates which.
decl Point { x: int; y: int } // fields 'name: type' → product (struct)decl Shape { Circle(int); Square } // variants 'Name(...)' → sum (enum)decl Drawable { fn draw() } // signatures 'fn ...' → behavior (interface)Field (name: type), variant (Name/Name(...)) and signature (fn ...) are syntactically distinct, and since a method is a function-with-a-receiver kept apart (below), a body is homogeneous, never mixing the three. “Struct”, “enum” and “interface” remain the names of the three forms (product, sum, behavior); decl is the single keyword.
And there is a deeper reason for a single keyword to cover all three: they are, all of them, contracts. A struct is a contract of structure: these fields, with these types. An enum is a contract of values: one of these variants, and none outside the list. An interface is a contract of behavior: these functions exist, with these signatures. What struct/enum/interface always shared was not the body’s shape; it was the act, and the act is one, declaring a contract, which is exactly what decl names. The body only says what the contract is about.
An interface body is signatures only — there are no default methods (a method with a body inside the interface). A default method would need to name the implementing instance, but Makoto has no ambient self (section 5: no value that magically appears; receivers are always explicit, section 2), so a body in the contract has nothing to call on. Shared behavior derived from an interface’s primitives is written as an ordinary generic free function over the constraint — fn greet[T + Greeter](x: T) -> string { return "hi " + x.name() } — the same reuse mechanism as Display/Serializable/map (section 10). This keeps the contract a pure statement of what must exist, and keeps the three decl bodies symmetric (fields / variants / signatures).
An enum is a closed sum, and its variants form a domain that sub-sets itself: Shape{Circle} is “a Shape, guaranteed to be Circle”, a set of variants, the same mechanics that unifies with error-sets (section 7).
A recursive type passes through indirection. A struct or enum may not contain itself by value — directly (decl Node { next: Node }) or transitively through another by-value aggregate (decl Cons { head: int; tail: Optional[Cons] }) — because a flat layout would then have infinite size. The self-reference goes through a pointer *Node, a many-pointer [*]Node, or a slice []Node, each a fixed-size handle to elsewhere, so decl Node { value: int; next: Optional[[*]Node] } is the shape of a list node. This is the rule C and Rust share (Rust’s Box): the compiler reports the by-value cycle and names the fix, it never silently boxes it — hidden allocation would break the “you only pay for what you summon” contract of section 5.
union stays out: its body is fields, identical to the product’s, so the body does not disambiguate it. It needs its own keyword, and that keyword carries the unsafe that the byte-reinterpretation system requires.
The empty body: Unit, Never, and the deliberately-absent Top. Each form’s empty body is a distinct type, and inhabitance keeps them apart. decl Marker {} is the empty struct, a Unit: inhabited by exactly one zero-size value — a marker/tag (the value slot of a Set[T], a phantom parameter, a “this happened” token). decl Never { _ } is the empty enum, the Never type: zero variants, uninhabited, no value constructible — the bottom type in the value position (the same uninhabited {} that error{} names on the error-set side, section 7, and that noreturn names in the return position, section 14). The lone _ in the body is the explicit “no variants” marker; a truly empty {} resolves to the struct (Unit), the most useful of the three, so Never asks for the _ to say “I meant zero, not forgot”. The empty interface would be Top: no required behavior, so every type satisfies it — which is exactly Any, and Makoto deliberately has no way to write it (section 9). The asymmetry is the point: Unit (one value) and Never (no value) are expressible; Top (all values, i.e. a bound that promises nothing) is not, because a contract that guarantees no behavior guarantees nothing you can do, and reopens the lazy-typing that Optional and interfaces closed. Unit / Never / Top — two you can name, one you cannot, on purpose.
Type aliases: alias
Section titled “Type aliases: alias”decl creates new structure. When you just want a second name for a type that already exists, it is alias:
alias UUID = u8 // UUID is u8, interchangeableIt is a transparent synonym: UUID and u8 are the same type, so uuid + 32 compiles (UUID is u8). The language already brought this built in (char is an alias of grapheme, section 16); alias is the keyword that names the act. A distinct type, with its own identity and non-interchangeable, is not alias: it is a decl (a struct wrapping the value).
The = separates the two acts: decl Point { ... } takes no =, because the body {...} is the definition (the type is born from the structure); alias UUID = u8 takes =, because there is no new structure, just the equation “this name is that type”. Has = alias (already exists); does not have new structure. And type stays untouched: it remains just the kind (T: type, -> type, section 10). alias names types, type is the type of types.
What does not exist
Section titled “What does not exist”- Classes / inheritance
- Borrow checker
- Explicit lifetimes
Box<Arc<'_Type<'c<uint>>>>and the like
Scope linearity (no borrow checker)
Section titled “Scope linearity (no borrow checker)”Instead of lifetimes, the guarantee comes from scope linearity at the transfer points: when a value is sent with @transfer, the compiler invalidates the variable in the current scope immediately. There is no “borrow”: either the data is yours, or you transferred it. The compiler only needs to check: “has this variable already been transferred?”. If so, compile error. (This same local-checking idea reappears in mutation inside a process, with mut and pointers; see section 6.)
Composition: @embeds
Section titled “Composition: @embeds”The language has no inheritance: composition is the mechanism of reuse, in Go’s model (embedding), but with promotion declared instead of inferred from an anonymous field. Two facts of the type model that this presupposes:
- A struct has only fields. Methods do not live inside the struct; they are free functions with a receiver:
fn (d: *Dog) speak() -> string { ... }(Go model; the.of the call is the UFCS of section 14). - Methods are normal functions, so they take their own generics:
fn (d: *Data) compute[T](x: T) -> T { ... }. The[T](section 10) works in a method as in any function, because a method is a function with a receiver, not a separate construct. - Associated functions (constructors, factories) have the receiver without binding:
fn (Arena) new() -> Arena. The type in the receiver, without a name, marks “associated with the type, no instance”; with a name (fn (a: Arena) used()) it is a method, without a name it is associated. The call is by dot, like any member of the type:Arena.new(),string.from[u8](bytes), the same.of UFCS (section 14), resolving an associated member instead of an instance method. (Nostatickeyword: the presence or absence of the binding is already the mark, andstaticwould drag in C’s linkage connotation, coloring in disguise.)
The @embeds(Type, methods|fields|all) declares that the struct composes another type, promoting what you specify (methods, fields, or everything); the promoted thing becomes accessible directly on the embedder:
decl Animal {}fn (a: *Animal) speak() -> string { return "..." }
@embeds(Animal, methods)decl Dog { animal: Animal nome: string}
d.speak() // promoted: calls Animal.speak directly, without naming 'animal'd.animal.speak() // the named path also worksWhat separates this from Go’s embedding is that the promotion is announced: the @embeds at the top says “this struct promotes methods from Animal”. In Go, the promotion is inferred from an anonymous field (an Animal without a name lost inside the struct), and nothing in the definition warns that methods will rise. Here you read the decorator and you know: the promotion is explicit, not a side effect of omitting a name.
Two positions, same language rule. @embeds follows the convention the asm already uses (section 18): decorator on top for whole-thing config (like @supervisor above the spawn), decorator beside to bind to a specific field (like @asm(in, …) on the parameter). You choose by readability: on top is lean for a small struct; beside reads better when the struct grows (each field shows, on its line, that it promotes):
// ON TOP: config of the whole (anonymous, at struct level)@embeds(Animal, methods)decl Dog { animal: Animal nome: string}
// BESIDE: binds the promotion to the fielddecl Robot { motor: Motor @embeds(methods) // field 'motor: Motor', promotes the methods sensors: Sensors @embeds(all) // field 'sensors: Sensors', promotes everything serie: string}The difference between the two forms of @embeds is only where the type lives, and this follows from whether or not there is a field:
- Named field:
field: Type @embeds(what). The field is normal (motor: Motor), and@embeds(what)only annotates what to promote; the type is in the field, it is not repeated. (whatismethods/fields/all.) - Anonymous:
@embeds(Type, what)alone. No field before it, so the type goes in the decorator (the only way to state it). It is embedding without a name.
And the positions coexist in the same struct (as @supervisor can be on top or before the spawn): you can have @embeds on top, beside fields, and anonymous inside, all at once:
@embeds(LivingThing, all) // (on top) embeds LivingThing at struct leveldecl Android { brain: Brain @embeds(methods) // (beside, named) field 'brain: Brain', promotes methods power: Power @embeds(all) // (beside, named) field 'power: Power', promotes everything @embeds(Chassis, fields) // (anonymous, inside) no field → the type goes in the decorator serie: string}Here Android promotes from four sources at once (LivingThing from on top, Brain and Power from the fields, and Chassis anonymous), and the conflict rule below applies over everything that is promoted, no matter which position it came from.
Named vs. anonymous field, and the conflict. You can embed with a name (animal: Animal) or without (Animal loose). The difference only shows up on a method conflict:
// ERROR: anonymous + method collision@embeds(Animal, methods)decl Dog { Animal // Dog and Animal both have speak() → d.speak() would be ambiguous nome: string}
// OK: name the field, and the collision resolves by path@embeds(Animal, methods)decl Dog { animal: Animal nome: string}fn (d: *Dog) speak() -> string { return "Woof" }fn (a: *Animal) speak() -> string { return "..." }
d.speak() // Dog.speak, its ownd.animal.speak() // Animal.speak, via the named path- No collision the anonymous works cleanly (
d.speak()calls Animal’s, without a name). - With collision the anonymous is a compile error (
d.speak()ambiguous, with no way to disambiguate). The fix is to give the field a name.
No shadowing, the promotion is selective. With a named field + @embeds, the methods that do not collide are promoted (you call d.any_animal_method() directly), and only those that collide fall to the named path (d.animal.speak()). The embedded thing’s method never disappears: when there is a collision, the own one (d.speak()) wins, but Animal’s stays with a clear address (d.animal.speak()), not hidden. That is the difference of “no shadowing”: Go would let Dog.speak silently mask Animal.speak; here both have a name, and the collision resolves by explicit path, not by an invisible who-wins rule.
The same rule holds for sibling embeds (several @embeds in the same struct): if no method collides among the embedded ones, nothing needs a name; if two siblings have the same method, the fields need a name to disambiguate. And when two siblings collide, the unqualified call is an error (ambiguous), resolved only by the explicit path: clash.foo() is rejected, clash.a.foo() / clash.b.foo() are accepted — the promotion never picks a silent winner among siblings.
Promotion is transitive. If A embeds B and B embeds C, then C’s members promote all the way up to A (a.c_method() works): embedding composes, so a chain of embeds behaves as one flattened promotion, subject to the same collision rule at each level.
Promotion works through a pointer field. Embedding a pointer field (motor: *Motor @embeds(methods)) promotes the members exactly as embedding by value does — the promoted call dereferences the pointer. (The pointer’s own nullability, if any, is the ordinary Optional rule of section 6; a plain *Motor is non-null.)
Interface satisfaction follows from promotion. If @embeds(Animal, methods) promotes methods that satisfy an interface (section 9), the embedder satisfies it, because the methods are its now. And it is not magic: it follows from the @embeds visible at the top. Animal satisfies Speaker, Dog embeds promoting methods Dog satisfies Speaker.
Extracting the embedded part: b as A. Embedding promotes methods/fields for access, but B does not become A: a concrete type is nominal (do_something(value: A) wants an A, not “something that embeds A”). When you need to pass the A-part of a B, the extraction is explicit with as:
fn do_something(value: A) { ... }
b := B{ ... } // B embeds A (@embeds(A, all))do_something(b) // ERROR: B is not A (nominal)do_something(b as A) // OK: extracts the embedded A, dropping what is specific to Bb as A produces the embedded A by value (copy). It is the only form when the embed is anonymous (there is no b.a to access); for a named embed it is a synonym of b.a. The rule keeps explicitness: no implicit B-becomes-A, you write the conversion. (For structural use, “anything with these methods”, the path is an interface, not the concrete type; embedding makes the embedder satisfy the interfaces of the embedded, section 9.)