Module System

Use Statements

A module is a .fv file. A use statement imports items from another module. Take a project with these files:

project/
├── main.fv
├── types.fv
├── components.fv
├── widgets/
│   └── controls.fv
└── utils/
    └── helpers.fv
pub struct User { name: String, age: I32 }

impl User {
  fn is_adult(self) -> Boolean { self.age >= limit() }
}

// Private: other modules cannot import it, but the module's own code
// still calls it.
fn limit() -> I32 { 18 }
pub struct Button { label: String }
pub struct Text { value: String }
pub struct VStack { gap: I32 }
pub struct Slider { value: I32 }
pub fn formatDate(day: I32) -> String { "day " }

main.fv imports from each of them:

// Import a single item
use types::User

// Import multiple items
use components::{Button, Text, VStack}

// Import from a nested path: widgets/controls.fv
use widgets::controls::Slider
use utils::helpers::formatDate

pub fn run_checks() {
  let user = User(name: "Ada", age: 36)
  assert(condition: user.is_adult())        // the impl block comes too
  assert(condition: Button(label: "ok").label == "ok")
  assert(condition: Slider(value: 3).value == 3)
  assert(condition: formatDate(day: 1) == "day ")
}

Module Resolution:

  • Modules map to .fv files
  • Path separators use ::. The segments before the last one name the file: use widgets::controls::Slider reads widgets/controls.fv. The last segment, or each name in { }, names an item of that file
  • A use imports items. It does not import a module as a whole: use utils::helpers looks for an item helpers in utils.fv
  • A path that no file matches is the error ModuleNotFound. A name that the file does not declare is the error ImportItemNotFound
  • Can only import pub items. A private item is the error PrivateImport
  • No circular imports allowed: two modules that import each other are the error CircularImport

What an Import Brings

An import brings the item and what belongs to it:

  • A struct or an enum comes with its impl blocks, so its methods work in the importer.
  • A trait impl is part of its type. The importer can call the methods of the impl with no use of the trait.
  • A trait comes with its blanket impls. Each type of the importer that meets the bounds gets the trait: see Blanket Impls.
  • A blanket impl of the importer is for each type of an imported module that meets its bounds, also a type that the importer does not import by name but gets as a value, as for a type of an inline module.
  • Two module files can each declare a trait of one name. They stay two traits in the importer: a bound on one does not accept an impl of the other, and each call runs the impl of its own trait.
  • The imported code keeps its own private helpers. is_adult above calls the private limit, and the importer cannot name limit.
pub trait Area { fn area(self) -> I32 }

pub struct Square { side: I32 }

impl Area for Square {
  fn area(self) -> I32 { self.side * self.side }
}
use geom::Square

pub fn run_checks() {
  assert(condition: Square(side: 3).area() == 9)   // no `use geom::Area`
}

Which module may write an impl

An impl of a trait is in the module that declares the trait or the target type. This is the orphan rule. The prelude declares the primitives. An impl in another module is the error OrphanImpl (E179). So two modules can never give one type two impls of one trait. An impl with no trait, impl String { ... }, has no such rule.

Re-exports

A plain use makes the name available inside the importing module only. Another module cannot import the name through it. A pub use exports the name again, so a module can collect items from several files:

pub use types::User
pub use geom::Square
use prelude::{User, Square}

pub fn run_checks() {
  assert(condition: Square(side: 2).area() == 4)
  assert(condition: User(name: "Bo", age: 9).age == 9)
}

The module plain.fv imports Button with a plain use:

use components::Button

So another module cannot import Button from it:

use plain::Button

Nested Modules

Use mod blocks to create nested namespaces within a file:

mod alignment {
  pub enum Vertical {
    top,
    center,
    bottom
  }

  pub enum Horizontal {
    left,
    center,
    right
  }
}

// Use with namespace path
pub let vertical: alignment::Vertical = .top
pub let horizontal: alignment::Horizontal = .center

// Can also import nested items
use alignment::Vertical

pub let v: Vertical = .bottom

Items of a module are private unless they are pub. Code outside the module reaches only the pub items, by path (alignment::Vertical) or by use. A private item is the error VisibilityViolation when a path names it, and PrivateImport when a use names it. A nested module that is not pub is private too:

mod tools {
  pub fn shown() -> I32 { hidden() }   // ok: inside the module
  fn hidden() -> I32 { 1 }
}

pub let a: I32 = tools::shown()        // ok
pub let b: I32 = tools::hidden()       // error: 'hidden' is private

An inline module and a module file can have the same name. Then a use of that name has two meanings, and the compiler refuses it with the error AmbiguousModulePath. This page has the file types.fv:

mod types {
  pub struct User { name: String }
}

use types::User

Rename the inline module, or the file.

Multiple Levels:

mod ui {
  pub mod layout {
    pub enum Direction {
      horizontal,
      vertical
    }
  }

  pub struct Theme {
    primary: String,
    secondary: String
  }
}

pub let direction: ui::layout::Direction = .horizontal
pub let theme: ui::Theme = ui::Theme(
  primary: "#007AFF",
  secondary: "#5856D6"
)

Paths in a module, and traits by path:

A path in a module starts from that module first, then from each module that holds it, then from the top level. In ui, the path layout::Direction names ui::layout::Direction.

A trait of a module is one trait by each name that reaches it: Show inside shapes, shapes::Show outside, and Show after use shapes::Show. A bound and an impl header can name it by path:

mod shapes {
  pub trait Show {
    fn show(self) -> I32
  }
}

pub struct Square { side: I32 }

impl shapes::Show for Square {
  fn show(self) -> I32 { self.side }
}

fn shown<T: shapes::Show>(x: T) -> I32 { x.show() }

pub let s: I32 = shown(x: Square(side: 2))

A Public Signature Names Public Types

A pub definition is what another module sees. Naming a private type in one hands the reader a value whose type they cannot write down: they can call the function, but they cannot declare a binding for the result, pass it on, or name it in their own signature.

struct Hidden { x: I32 }

pub fn f() -> Hidden { Hidden(x: 1) }   // error E141
pub fn g(h: Hidden) -> I32 { h.x }      // error E141
pub struct Shown { h: Hidden }          // error E141

fn build() -> Hidden { Hidden(x: 1) }   // ok: not public

The rule reaches inside containers, because [Hidden] and Hidden? name the same type a bare Hidden does. It covers each part of a public definition that another module sees: the parameters and the return type of a function (an extern fn too), the fields of a struct, the payload fields of an enum, the fields and methods of a trait, the type of a let (written or inferred), a type argument, a tuple field, a closure type, and a generic bound. The expression of a default value is not part of the signature: it may use a private type, if the value has a public type. The rule holds inside a nested module too.

This pairs with the closure rule: a struct that holds a closure field cannot be pub, so no public signature may name it either. Such a struct is built and read inside its own module, and the module exposes the results instead. See Closures / Rules.