FREAKV3 docs freak 0.14.2 (Maverick)

Shapes & impl#

> shape is the only user-defined type in V3. impl attaches methods to it.

Declaring a shape#

FREAK
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.

Careful

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.

examples/shapes.fk compilesruns
-- `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)
}
Program output
3
4
10
Valkyries
4
true

Construction#

FREAK
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.

Note

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#

FREAK
say word_from_int(v.x)
v.x = 10

Compound assignment works on fields too: v.x += 1.

Access chains through nested shapes, including inside string interpolation.

examples/nested_shapes.fk compilesruns
-- 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()
}
Program output
Takemikazuchi
880
Takemikazuchi thrust=880

Methods#

impl Shape { ... } may contain only task declarations.

examples/impl_methods.fk compilesruns
-- `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())
}
Program output
15
16
135
16

The receiver decides the call form:

DeclarationKindCall
task area(self) -> intinstance methodvalue.area()
task scaled(self, f: int) -> Rectinstance method with argsvalue.scaled(3)
task square(side: int) -> Rectassociated methodRect::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.

examples/operator_overload.fk compilesruns
-- `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()
}
Program output
(11, 22)
Careful

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.

Note

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 expression

So 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:

FREAK
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.

Careful

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#

FeatureStatus
Generic fields shape Pair<A, B>V4
Field default valuesV4
variant sum typesV4
alias type aliasesV4
doctrine declarationsV4
Doctrine bounds on genericsV4
dyn Doctrine and vtablesV4
Real operator dispatch through Add etc.V4
Field visibility / launch on fieldsV4
@layout(C), @repr(u32)V4
Shared<T> / Weak<T>V4
A destructor or drop hookV4

Every FREAK snippet on this site was compiled by freak 0.14.2 (Maverick), built from source with a verified self-host fixed point. 45/45 examples compile; regenerate with python tools/verify.py then python tools/build_docs.py.