Shapes & impl#
> shape is the only user-defined type in V3. impl attaches methods to it.
Declaring a shape#
shape Name {
field: type
field: type
}Fields are name: type, where the type is int, num, word, bool, or another shape's name. Separators are optional: newlines, commas, or a trailing comma before } all work.
A shape field may not hold a List<T>, even though the annotation parses everywhere else: ``text type error: V3 owned shape fields do not yet support List values ` Keep the list in a separate binding, or store a legacy int array handle in the field instead. List<T>` is allowed as a task parameter and return type.
-- `shape` declares a record type. Each field is `name: type`,
-- where the type is a single identifier.
shape Vector2 {
x: int
y: int
}
shape Squad {
callsign: word
members: int
ready: bool
}
task main() -> void {
pilot v: Vector2 = Vector2 { x: 3, y: 4 }
say word_from_int(v.x)
say word_from_int(v.y)
-- Fields are assignable.
v.x = 10
say word_from_int(v.x)
pilot s: Squad = Squad { callsign: "Valkyries", members: 4, ready: true }
say s.callsign
say word_from_int(s.members)
say word_from_bool(s.ready)
}
3
4
10
Valkyries
4
true
Construction#
pilot v: Vector2 = Vector2 { x: 3, y: 4 }Every field must be supplied, by name, in any order. Field labels are checked against the declaration; an unknown or missing field is a compile error.
The parser only treats Name { ... } as a constructor when Name is already a registered shape. A shape must therefore be declared before it is constructed in the token stream. Declaring shapes at the top of the file, as every example here does, keeps this from ever mattering.
Field access and assignment#
say word_from_int(v.x)
v.x = 10Compound assignment works on fields too: v.x += 1.
Access chains through nested shapes, including inside string interpolation.
-- Shapes may hold other shapes. Field access chains through them,
-- including inside string interpolation.
shape Engine {
thrust: int
}
shape Tsf {
name: word
engine: Engine
}
impl Tsf {
task report(self) -> word {
give back "{self.name} thrust={self.engine.thrust}"
}
}
task main() -> void {
pilot unit: Tsf = Tsf {
name: "Takemikazuchi",
engine: Engine { thrust: 880 }
}
say unit.name
say word_from_int(unit.engine.thrust)
say unit.report()
}
Takemikazuchi
880
Takemikazuchi thrust=880
Methods#
impl Shape { ... } may contain only task declarations.
-- `impl Shape { ... }` attaches methods.
-- A method whose first parameter is `self` is an instance method,
-- called as `value.method(args)`.
-- A method without `self` is an associated method,
-- called as `Shape::method(args)`.
shape Rect {
w: int
h: int
}
impl Rect {
task square(side: int) -> Rect {
give back Rect { w: side, h: side }
}
task area(self) -> int {
give back self.w * self.h
}
task perimeter(self) -> int {
give back 2 * (self.w + self.h)
}
task scaled(self, factor: int) -> Rect {
give back Rect { w: self.w * factor, h: self.h * factor }
}
}
task main() -> void {
pilot r: Rect = Rect { w: 3, h: 5 }
say word_from_int(r.area())
say word_from_int(r.perimeter())
pilot big: Rect = r.scaled(3)
say word_from_int(big.area())
pilot sq: Rect = Rect::square(4)
say word_from_int(sq.area())
}
15
16
135
16
The receiver decides the call form:
| Declaration | Kind | Call |
|---|---|---|
task area(self) -> int | instance method | value.area() |
task scaled(self, f: int) -> Rect | instance method with args | value.scaled(3) |
task square(side: int) -> Rect | associated method | Rect::square(4) |
self is written bare — never self: Rect. The compiler substitutes the owning shape's type once the impl target is known, so self.field resolves properly and duplicate field/method names across shapes do not collide.
Methods are compiled to a flat global task named Shape_method. This is why two shapes may both define area without conflict, and why the checker tracks impl provenance separately: a free task literally named Rect_area is not accepted as proof that Rect.area exists.
Operator doctrines#
V3 accepts impl Doctrine for Shape and records the doctrine name as provenance. The methods become callable exactly like any other method.
-- `impl Doctrine for Shape` registers an operator implementation.
-- V3 does not accept a `doctrine` *declaration*, but it does accept
-- this impl form for the built-in operator doctrines, and the method
-- is callable by name.
shape Vec2 {
x: int
y: int
}
impl Add for Vec2 {
task add(self, other: Vec2) -> Vec2 {
give back Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
impl Vec2 {
task show(self) -> word {
give back "({self.x}, {self.y})"
}
}
task main() -> void {
pilot a: Vec2 = Vec2 { x: 1, y: 2 }
pilot b: Vec2 = Vec2 { x: 10, y: 20 }
pilot c: Vec2 = a.add(b)
say c.show()
}
(11, 22)
The operator token is not rewritten. Implementing Add does not make + work on your shape: ``text type error: operator '+' does not accept Vec2 and Vec2 ` Call the method by name — a.add(b). The bible's pilot v = v1 + v2` dispatch is V4.
The doctrine name is not validated. impl Whatever for Vec2 { ... } compiles cleanly — the name is recorded and otherwise ignored. There is no check that the doctrine exists, that its methods are all implemented, or that signatures match. Use Add, Sub, Mul, Div, Neg, Eq by convention, and expect V4 to start enforcing them.
doctrine declarations do not parse at all:
error: unexpected 'doctrine' — this token cannot start an expressionSo a doctrine is never a real contract in V3, and there is no generic bound (task f<T: Displayable>(...)), no dyn Doctrine, and no dynamic dispatch.
Shapes as values#
Shapes live in runtime storage and are handled by reference. Two consequences:
- Passing a shape to a task and mutating a field inside is visible to the caller. There is no implicit copy.
- Assigning one shape binding to another aliases the same storage.
To get an independent value, construct a new one — a small copy associated method is the usual idiom:
impl Rect {
task copy_of(self) -> Rect {
give back Rect { w: self.w, h: self.h }
}
}Under --strict-borrow the Phase-1 checker additionally treats user shapes as single-owner move types, so a use after move is reported.
Backend support#
Shape construction, field access, method execution and dotted {shape.field} interpolation are verified executable evidence on the LLVM backend only.
The C backend (--c) may transpile shape declarations, but packaged shape runtime storage is explicitly not a claimed executable path. If your program declares a shape, build it with the default LLVM backend.
What shapes cannot do#
| Feature | Status |
|---|---|
Generic fields shape Pair<A, B> | V4 |
| Field default values | V4 |
variant sum types | V4 |
alias type aliases | V4 |
doctrine declarations | V4 |
| Doctrine bounds on generics | V4 |
dyn Doctrine and vtables | V4 |
Real operator dispatch through Add etc. | V4 |
Field visibility / launch on fields | V4 |
@layout(C), @repr(u32) | V4 |
Shared<T> / Weak<T> | V4 |
A destructor or drop hook | V4 |