From 5f8f01d941598a421828c3604c052fecbe715b57 Mon Sep 17 00:00:00 2001 From: Magnus Madsen Date: Sun, 26 Apr 2026 15:34:41 +0200 Subject: [PATCH 1/2] feat: document companion type lifting Update the companion modules page to reflect the new style introduced in flix/flix#12518: companions are declared inside a module of the same name, must be the first declaration, and may be enums, structs, effects, or traits. Also document field visibility for struct companions and instance placement in companion modules. Add a corresponding section to for-llms.md so generated code uses the new style. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/companion-modules.md | 132 ++++++++++++++++++++++++++++++--------- src/for-llms.md | 41 ++++++++++++ 2 files changed, 144 insertions(+), 29 deletions(-) diff --git a/src/companion-modules.md b/src/companion-modules.md index f1976003..a39e55a1 100644 --- a/src/companion-modules.md +++ b/src/companion-modules.md @@ -1,21 +1,43 @@ # Companion Modules -In Flix every enum and trait declaration is associated with a _companion -module_. +Inside a module we can declare an enum, struct, effect, or trait with the +same name as the module. Such a declaration is called the _companion_ of the +module. -## Enum Companions - -When we declare an enum, its type and cases are automatically available inside -its companion module. For example, we can write: +For example: ```flix -enum Color { - case Red, - case Green, - case Blue +mod Color { + pub enum Color { + case Red, + case Green, + case Blue + } } +``` + +Here the `Color` enum is the companion of the `Color` module. + +The companion's name is exported from the module. This means that `Color` can +refer to both the module and the enum. We can refer to a case as `Color.Red` +or as `Color.Color.Red`. + +The companion must appear before any other declaration inside its module. +Otherwise the compiler raises an error. + +## Enum Companions +When an enum is declared as the companion of its module, the type and its +cases are automatically available throughout the module: + +```flix mod Color { + pub enum Color { + case Red, + case Green, + case Blue + } + pub def isWarm(c: Color): Bool = match c { case Red => true case Green => false @@ -24,40 +46,92 @@ mod Color { } ``` -Here the `Color` type and the `Red`, `Green`, and `Blue` cases are automatically -in scope within the companion `Color` module. +Here the `Color` type and the `Red`, `Green`, and `Blue` cases are in scope +within the companion `Color` module. -## Trait Companions +## Struct Companions -Every trait declaration also gives rise to a companion module. +A struct may also be declared as the companion of its module. The fields of a +struct are only visible from within its companion module, so any function that +reads or writes them must live there. -For example, we can define a trait `Addable` for types whose elements can be added: +For example: ```flix -trait Addable[t] { - pub def add(x: t, y: t): t +mod Point { + pub struct Point[r] { + x: Int32, + mut y: Int32 + } + + pub def area(p: Point[r]): Int32 \ r = p->x * p->y } ``` -The `Addable` trait implicitly introduces a companion module `Addable`. We -typically use the companion module to store functionality that is related to the -trait. +Here `area` can access the `x` and `y` fields because it lives inside the +companion module of `Point`. See [Structs](structs.md) for more on field +visibility. + +## Effect Companions -For example, we could have: +An effect may be declared as the companion of its module. The default handler +for the effect, if any, lives in the same companion module: + +```flix +mod Fs.Glob { + pub eff Glob { + def glob(base: String, pattern: String): Result[IoError, List[String]] + } + + // Handlers and helpers for the effect go here. +} +``` + +## Trait Companions + +A trait may also be declared as the companion of its module. We typically use +the companion module to store functionality that is related to the trait: ```flix mod Addable { + pub trait Addable[t] { + pub def add(x: t, y: t): t + } + pub def add3(x: t, y: t, z: t): t with Addable[t] = add(add(x, y), z) } ``` -When accessing a member of `Addable`, Flix will automatically look in both the -trait declaration and its companion module. Consequently, `Addable.add` -refers to the trait member `add` whereas `Addable.add3` refers to the -function inside the `Addable` module. Note that the `add` signature is in the -scope of the `Addable` module. +When accessing a member of `Addable`, Flix automatically looks in both the +trait declaration and its companion module. Consequently, `Addable.add` refers +to the trait member `add` whereas `Addable.add3` refers to the function inside +the `Addable` module. We should be aware that functions defined in the companion module of a trait -cannot be redefined by instances of the associated trait. Thus we -should only put members into the companion namespace when we do not intend -to redefine them later. +cannot be redefined by instances of the associated trait. Thus we should only +put members into the companion module when we do not intend to redefine them +later. + +## Instances in Companion Modules + +A trait instance may be declared in the companion module of its type. For +example, instances of `Add`, `Sub`, and `ToString` for the `Size` enum are +placed alongside the enum itself: + +```flix +mod Fs.Size { + pub enum Size(Int64) with Eq, Order, Hash + + instance Add[Size] { + pub def add(x: Size, y: Size): Size = + let Size(x1) = x; + let Size(y1) = y; + Size(x1 + y1) + } + + pub def zero(): Size = Size(0i64) +} +``` + +This is the recommended location for instances when the trait is defined +elsewhere. diff --git a/src/for-llms.md b/src/for-llms.md index a92fbbc8..291d601a 100644 --- a/src/for-llms.md +++ b/src/for-llms.md @@ -313,6 +313,47 @@ def testAdd01(): Unit \ Assert = Note: Use `@Test`, not `@test`. Other annotations are similarly uppercase, e.g. `@Parallel`, `@Lazy`, `@MustUse`. +## Companions Go Inside the Module + +A companion of a module is an enum, struct, effect, or trait whose name +matches the module's name. The current convention is to declare the companion +_inside_ the module, as the first declaration. The older sibling style +(declaring the enum, struct, effect, or trait next to the module) is no +longer idiomatic. + +❌ **Old (no longer idiomatic):** + +``` +enum Color { // Wrong -- Outdated + case Red, + case Green, + case Blue +} + +mod Color { + pub def isWarm(c: Color): Bool = ... +} +``` + +✅ **Current (correct, as of Flix 0.68.0):** + +```flix +mod Color { + pub enum Color { + case Red, + case Green, + case Blue + } + + pub def isWarm(c: Color): Bool = ... +} +``` + +Note: The companion must be the **first** declaration inside its module, +otherwise the compiler emits a `CompanionMustBeFirst` error. The same rule +applies to struct, effect, and trait companions. See +[Companion Modules](./companion-modules.md) for details. + ## Datalog `inject` Requires Arity Older versions of Flix allowed `inject` without specifying the arity of the From ab9dcb0cbfa6569a3379f458c9b775c79eb47ecc Mon Sep 17 00:00:00 2001 From: Magnus Madsen Date: Sun, 26 Apr 2026 15:46:29 +0200 Subject: [PATCH 2/2] refactor: nest structs inside companion modules in structs.md Update all struct examples in structs.md to declare each struct inside a module of the same name (its companion), matching the convention documented in companion-modules.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/structs.md | 67 +++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/structs.md b/src/structs.md index 0b2eebf6..2f1d96d1 100644 --- a/src/structs.md +++ b/src/structs.md @@ -22,21 +22,24 @@ Each operation has an effect in the region of the struct. ## Declaring a Struct -We can declare a struct as follows: +A struct is declared inside a module of the same name — its +[companion](companion-modules.md). For example: ```flix -struct Person[r] { - name: String, - mut age: Int32, - mut height: Int32 +mod Person { + pub struct Person[r] { + name: String, + mut age: Int32, + mut height: Int32 + } } ``` Here we declare a struct with three fields: `name`, `age`, and `height`. The `name` field is immutable, i.e. cannot be changed once the struct instance has -been created. The `age` and `heights` are mutable and hence can be changed after -creation. The `Person` struct has one type parameter: `r` which specifies the -region that the struct belongs to. +been created. The `age` and `height` fields are mutable and hence can be +changed after creation. The `Person` struct has one type parameter: `r` which +specifies the region that the struct belongs to. Every struct must have a region type parameter and it must be the last in the type parameter list. @@ -108,9 +111,11 @@ module. We can think of this as a form of compiler-enforced encapsulation. For example, if we write: ```flix -struct Point[r] { - x: Int32, - y: Int32 +mod Point { + pub struct Point[r] { + x: Int32, + y: Int32 + } } def area(p: Point[r]): Int32 \ r = @@ -140,12 +145,12 @@ The Flix compiler emits two errors: Instead, we should define the `area` function _inside_ the companion module: ```flix -struct Point[r] { - x: Int32, - y: Int32 -} +mod Point { + pub struct Point[r] { + x: Int32, + y: Int32 + } -mod Point { // Companion module for Point pub def area(p: Point[r]): Int32 \ r = p->x * p->y } @@ -173,10 +178,12 @@ been created. For example, we can define a struct to represent a user: ```flix -struct User[r] { - id: Int32, - mut name: String, - mut email: String +mod User { + pub struct User[r] { + id: Int32, + mut name: String, + mut email: String + } } ``` @@ -211,9 +218,11 @@ We remark that field immutability is _not_ transitive. For example, we can define a struct: ```flix -struct Book[r] { - title: String, - authors: MutList[String, r] +mod Book { + pub struct Book[r] { + title: String, + authors: MutList[String, r] + } } ``` @@ -236,11 +245,13 @@ mutable list. We can define a struct for a binary search tree that is recursive and polymorphic: ```flix -struct Tree[k, v, r] { - key: k, - mut value: v, - mut left: Option[Tree[k, v, r]], - mut right: Option[Tree[k, v, r]] +mod Tree { + pub struct Tree[k, v, r] { + key: k, + mut value: v, + mut left: Option[Tree[k, v, r]], + mut right: Option[Tree[k, v, r]] + } } ```