Modules and visibility
A module is a named group of declarations. Every source file belongs to one, and a module can span any number of files. Code reaches another module's declarations through qualified names like geometry.area or by importing them with use. Visibility keywords decide who can see a declaration: its file, its module, its project, or every project that uses it.
The mod line
Every source file starts with a mod line that names its module. Only comments and blank lines may come before it:
helper: fun -> i32 => 4 mod exampleModule name must precede all other declarations or imports main: fun{}A file names exactly one module, so a second mod line is an error, as Lexical structure shows.
The name of a file and the folder it sits in don't matter to the compiler. Only the mod line decides which module a file belongs to; which files belong to the project is covered in Projects and building. By convention, a file in game/physics/ declares mod game.physics.
Modules across files
All files with the same mod line form one module. Their declarations are visible to each other as if they were written in one file, so main.ce here calls twice from helpers.ce without anything extra:
main: fun{ printf("%d\n", twice(21))}mod example twice: fun(n: i32) -> i32 => n * 242
Declaration order doesn't matter within a file, and it doesn't matter across files either.
Qualified names
A declaration in another module is reached by writing the module's name, a ., and the declaration's name. This works for functions, types, constants, module variables and enum cases alike:
main: fun{ val p = geometry.Point(3, 4) printf("%d %d\n", geometry.area(2, 3), geometry.sides) printf("%d\n", geometry.distance(p)) val shape = geometry.Shape.square printf("%d\n", shape == geometry.Shape.circle)}mod geometry sides: const i32 = 4 Point: rec{ x: i32 y: i32} Shape: enum{ circle square} area: fun(w: i32, h: i32) -> i32 => w * hdistance: fun(p: Point) -> i32 => p.x + p.y6 4 7 0
Inside geometry, its own declarations need no qualifier. A module can also name itself, as in example.main, which helps when a local name hides one of its declarations.
Without a qualifier or an import, a name from another module isn't found:
main: fun{ val a = area(2, 3)Symbol 'area' not found in this scope}mod geometry area: fun(w: i32, h: i32) -> i32 => w * hA module isn't a value, so its name can only be used in front of a .:
main: fun{ val m = geometryCannot use modules as values}mod geometry area: fun(w: i32, h: i32) -> i32 => w * hNested module names
A module name can have several parts separated by dots, as in mod game.physics. The parts make a tree of names: game.physics is reached as game.physics.fall, whether or not a module named game exists. game and game.physics are separate modules, each with its own declarations:
main: fun{ printf("%d %d\n", game.physics.fall(2), game.speed())}mod game speed: fun -> i32 => physics.gravity * 2mod game.physics gravity: const i32 = 10 fall: fun(time: i32) -> i32 => gravity * time * time / 220 20
Inside game, its nested module game.physics can be written physics.
A module can't have a member with the same name as a nested module:
main: fun{}mod game physics: fun -> i32 => 1'game.physics' is both a module and a member of 'game'mod game.physics'game.physics' is both a module and a member of 'game' gravity: const i32 = 10Imports
A use line after the mod line imports names from another module, so the file can use them without a qualifier. use names what it imports:
use geometry.areaimports one name.use geometry.[area, Point]imports several.use geometry.*imports everything ingeometry.use game.physicsimports a nested module, so the file can writephysics.fallinstead ofgame.physics.fall.
use geometry.[area, Point]use shapes.*use game.physics main: fun{ val p = Point(1, 2) printf("%d %d %d\n", area(2, 3), p.y, perimeter(5)) printf("%d\n", physics.fall(2))}mod geometry Point: rec{ x: i32 y: i32} area: fun(w: i32, h: i32) -> i32 => w * hmod shapes perimeter: fun(side: i32) -> i32 => side * 4mod game.physics fall: fun(time: i32) -> i32 => 5 * time * time6 2 20 20
An import applies to the file it's written in, not to the rest of the module. Imported names can still be written with their qualifier.
Imports come right after the mod line, before any declaration:
mod example main: fun{} use geometry.areaImports must precede all other declarationsmod geometry area: fun(w: i32, h: i32) -> i32 => w * hAn import that names something that doesn't exist is an error, with a suggestion when a close spelling exists:
mod example use geometry.missingModule 'geometry' has no member 'missing'use gemoetry.areaModule 'gemoetry' not foundDid you mean 'geometry'?use geometry.area.more'geometry.area' is not a module main: fun{}mod geometry area: fun(w: i32, h: i32) -> i32 => w * hImporting the same name twice is "'area' is imported more than once".
Imports and the module's own names
A file sees its module's declarations and its imports together, and neither one wins over the other:
- Functions from both form one set of overloads. When the module declares a function with the same name and parameters as an imported one, the module's own function hides the import; the imported one is still reachable through its qualifier.
- A type, constant or variable that comes in through
*is hidden by a module declaration with the same name.
use geometry.areause geometry.scaleuse geometry.* area: fun -> i32 => 1scale: fun(a: i32, b: i32) -> i32 => a + blimit: val i32 = 3 main: fun{ printf("%d %d\n", area(), geometry.area()) printf("%d %d\n", scale(1), scale(1, 2)) printf("%d %d\n", limit, geometry.limit)}mod geometry area: fun -> i32 => 2scale: fun(x: i32) -> i32 => x * 10limit: val i32 = 71 2 10 3 3 7
Naming a type, constant or variable in an import while the module declares the same name is an error, since the import could never be used by its plain name. Both places are reported:
mod example use geometry.Point'Point' is ambiguousAlso declared in 'example' Point: rec'Point' is ambiguousAlso imported from 'geometry'{ z: i32} main: fun{}mod geometry Point: rec{ x: i32}The first error is on the import, with the note "Also declared in 'example'", and the second on the declaration, with "Also imported from 'geometry'".
Imports never hide each other. When two imports bring in the same name, using it is ambiguous; for functions, only when they have the same parameters:
mod example use geometry.*use shapes.* main: fun{ val a = area()Call to 'area' is ambiguousCandidate in 'geometry.ce' on line 3Candidate in 'shapes.ce' on line 3 val p = Point(1)Reference to 'Point' is ambiguous}mod geometry area: fun -> i32 => 2 Point: rec{ x: i32}mod shapes area: fun -> i32 => 3 Point: rec{ y: i32}The qualified names geometry.area and shapes.area still work.
mod. paths
A local variable, parameter or declaration can have the same name as a module and hide it. A path starting with mod. always starts from the top of the module tree, whatever is in scope:
main: fun{ val geometry = 4 val shape = mod.geometry.Shape(3) printf("%d %d %d\n", geometry, shape.side, mod.geometry.area(2))}mod geometry Shape: rec{ side: i32} area: fun(side: i32) -> i32 => side * side4 3 4
Visibility
Every top-level declaration has one of four visibility levels:
| Keyword | Visible in |
|---|---|
pvt | its own file |
mod | its own module, in every file |
| none | its own project |
pub | every project that references this one |
The keyword goes after the colon, before what the declaration is, as in helper: pvt fun or Point: pub rec. A declaration without one is visible in the whole project, which is all a program made of one project needs.
pvt keeps a declaration to its file, even from other files of the same module:
main: fun{ val n = helper()'helper' is private to its file}mod example helper: pvt fun -> i32 => 1mod keeps a declaration to its module. A nested module is a different module, so game.physics can't see mod declarations of game, and the other way around:
main: fun{ val n = game.tool()'game.tool' is private to its module}mod game tool: mod fun -> i32 => 1The same rules apply to imports: a hidden declaration that comes in through * still can't be used, and naming one in an import is an error.
Types, functions, constants, module variables, traits and ext declarations all take visibility. Local variables don't:
mod example main: fun{ val x: pvt i32 = 1Cannot use 'pvt' on local variables}Members of records, enums and other types have their own visibility rules, covered in Methods and properties.
Visibility blocks
A block gives every declaration in it the same visibility: pvt { ... }, mod { ... } or pub { ... }. It can hold any top-level declarations, ext blocks included, and pvt ext { ... } is short for an ext block inside a pvt block:
mod example pvt ext{ printf: fun(format: cstr, ...) -> i32} mod{ twice: fun(n: i32) -> i32 => n * 2 limit: const i32 = 100} main: fun{ printf("%d %d\n", twice(4), limit)}8 100
A declaration inside a block can't name a visibility of its own, and blocks of different levels can't be nested:
mod example mod{ helper: pvt fun -> i32 => 0Cannot use 'pvt' in 'mod' blocks pvtCannot use 'pvt' in 'mod' blocks { other: fun -> i32 => 1 }} twice: pvt mod fun -> i32 => 0Cannot combine 'pvt' and 'mod' main: fun{}Signatures can't expose hidden types
A declaration can't mention a type that is less visible than the declaration itself, since code that can see the declaration couldn't name the type. This covers parameter and result types, field types, payloads and the types of module variables:
mod example Node: pvt rec{ value: i32} Tree: rec{ root: Node'Node' is less visible than 'root'} make: fun -> Node => Node(1)'Node' is less visible than 'make' take: fun(node: Node) -> i32 => node.value'Node' is less visible than 'take' keep: pvt fun(node: Node) -> i32 => node.value main: fun{}keep is fine: it's pvt like Node, so it's never visible where Node isn't. The same holds for types built from a hidden type, like ptr[Node] or array[Node, 4], and for traits named in bounds and dyn types.
Libraries
A project can reference a library project, as Projects and building shows, and use its declarations. Only the library's pub declarations cross over; everything else is private to the library's project. The library's modules are reached by name, exactly like the program's own:
OutputType = "Executable" [[ProjectReferences]]Path = 'shapes/shapes.celp'OutputType = "StaticLibrary"mod shapes Square: pub rec{ side: i32} area: pub fun(s: Square) -> i32 => s.side * s.side * scale() scale: fun -> i32 => 1use shapes.[Square, area] main: fun{ printf("%d\n", area(Square(3)))}9
Using scale from the program would be "'shapes.scale' is private to its project". A pub declaration also can't expose a type that isn't pub, by the rule in the previous section.
Project imports
A project's .celp file can list imports that apply to every source file in the project, so each file doesn't need its own use lines. Imports takes the same forms as use, written as strings:
OutputType = "Executable"Imports = ["geometry.area", "shapes.*"]main: fun{ printf("%d %d\n", area(2, 3), perimeter(5))}mod geometry area: fun(w: i32, h: i32) -> i32 => w * hmod shapes perimeter: fun(side: i32) -> i32 => side * 46 20
Project imports combine with the module's own declarations by the same rules as use lines. An import listed there and in a file's use line is "'area' is imported more than once", and errors in the list point into the .celp file.
A library's .celp can list a Prelude instead: imports that every project referencing the library gets automatically. A use line that repeats one of them gets the hint "Redundant import".
OutputType = "Executable" [[ProjectReferences]]Path = 'greetings/greetings.celp'OutputType = "StaticLibrary"Prelude = ["greetings.greet"]mod greetings pvt ext{ printf: fun(format: cstr, ...) -> i32} greet: pub fun => printf("hello from the prelude\n")mod example main: fun{ greet()}hello from the prelude
Cella Documentation