Skip to content

Source files and modules

A dusk source file uses the .dusk extension. It has two kinds of top-of-file syntax. Directives start with @ and configure the file. Declarations define types, functions, and values, and can carry modifier keywords like export and mut.

Directives appear at the top of the file, before declarations. The compiler scans the leading lines of a file and stops at the first line that is neither blank, a line comment, nor a directive; blank lines and // comments may sit between directives. An unknown @ directive is a compile error, as is an unknown paradigm name or an empty import path.

@paradigm functional
@paradigm procedural
@import std.io
@import std.functional.maybe

There are two directives.

  • @paradigm <name> declares a paradigm the file uses. It can be repeated to stack paradigms. A file with no @paradigm directive defaults to procedural. See Paradigm system.
  • @import <path> brings a module or a symbol into the file.

Imports are independent of paradigm directives. Importing a module does not grant any paradigm, and a paradigm directive does not change what can be imported. The two systems do not interact.

Imports are based on directories and files. A dotted path walks the project tree.

/
myLib/
myFile.dusk
main.dusk

In main.dusk:

@import myLib.myFile.someFunc // import a leaf symbol

A dotted path resolves to one of two things.

  • A module: a file, reached through its directory path. You then call through the qualified name.
  • A leaf symbol: a function, type, or value inside a file. The leaf name is then in scope unqualified.
@import std.io // module: call std.io.print_line(...)
@import std.io.print_line // symbol: call print_line(...)

A bare directory is not a module: @import std does not resolve, because there is no std.dusk file. The path must reach a file (std.io) or a symbol inside one (std.io.print_line).

Resolution walks directories, then files, then symbols, so the compiler can tell where the file path ends and the symbol name begins. A dotted path is looked up against the importing file’s directory first, then the standard library root. For each root the compiler first tries the whole path as a module file; if that fails, it tries the path minus its last segment as the module file and treats the trailing segment as a leaf symbol, which the module must export.

Standard library modules live under std and local modules resolve relative to the importing file. Both use the same dotted form. The following program imports std.io as a module and str_len as a leaf symbol, and it compiles today:

imports.dusk
@import std.io
@import std.string.str_len
func main() -> int32 {
std.io.print_line("hello")
std.io.print_int(str_len("hello"))
return 0
}

A qualified call like std.io.print_line(...) reaches an imported module’s function through its module path. A leaf import like str_len puts the bare name in scope. See Standard library overview for the module list.

An external package uses its git path in quotes.

@import "github.com/user/repo/module"

The first three segments, host/user/repo, name the repository. The rest names a file inside it. The import string carries no version yet. The dawn tool fetches the repository into a local cache ($DAWN_CACHE, or ~/.dawn/cache when unset), and the compiler resolves the module from there. github.com/user/repo/module resolves to <cache>/github.com/user/repo/module.dusk, or falls back to repo.dusk with module as a leaf symbol, the same way a dotted path resolves. This fragment is valid syntax but is not shown as a compiled program here because it requires a populated cache. There is currently no version pinning, lock file, or integrity check; those are planned. See Dawn for the commands and cache layout.

By default every declaration is private to its file. The export keyword makes a declaration visible to other files.

export struct Point { x: float64, y: float64 }
export func area(s: Shape) -> float64 { ... }

Only exported names can be imported elsewhere. There is no paradigm restriction on exports: an exported function or type is usable from any file regardless of either file’s paradigm directives. This keeps the cross-file story simple and matches the rule that user-defined names are paradigm agnostic: a file without @paradigm functional cannot call map directly, but it can call an imported function that uses map internally.

A private name never crosses a file boundary: neither as a qualified call, nor as a bare call, nor as a leaf import. Two imported modules may each keep a private helper of the same name without colliding; before merging modules, the loader isolates each file’s private top-level names so they cannot be reached or clash across files.

The following two files compile together. square is private to geometry.dusk; Point and dist2 are exported and imported as leaf symbols.

geometry.dusk
// square is private: it never leaves this file.
func square(x: float64) -> float64 {
return x * x
}
export struct Point {
x: float64,
y: float64,
}
export func dist2(a: Point, b: Point) -> float64 {
return square(b.x - a.x) + square(b.y - a.y)
}
main.dusk
@import geometry.Point
@import geometry.dist2
func main() -> int32 {
a := Point { x: 0.0, y: 0.0 }
b := Point { x: 3.0, y: 4.0 }
println(dist2(a, b))
return 0
}

Every route to a private name is rejected. Each fragment below fails to compile with the diagnostic shown.

A bare call cannot reach an imported module’s private name:

@import geometry
func main() -> int32 {
println(square(2.0)) // error: undefined name 'square'
return 0
}

A qualified call to a private name is rejected rather than silently folded:

@import geometry
func main() -> int32 {
println(geometry.square(2.0)) // error: 'square' is private to module 'geometry'
return 0
}

A leaf import of a private name is rejected at the import itself:

@import geometry.square
// error: import 'geometry.square' names 'square',
// but 'geometry' exports no such symbol

The println used above is a language builtin, always in scope without any import; it is distinct from the functions exported by std.io. See Builtins.