A Scaly program can be split across several source files, and it can depend on other packages. There are no build files, manifests, or configuration files — three statements inside the source itself organize everything:
module loads another source file into the current program or package.
package declares a dependency on another package.
use makes types defined in another module visible in the current file.
Every source file is a module. The statement module shapes loads the file shapes.scaly from the directory of the file that declares it and makes it part of the program:
; shapes.scaly
define Box(width: int, height: int)
{
function area(this) returns int
width * height
}
; myprog.scaly
module shapes
use myprog.shapes.Box
let b Box(3, 4)
if b.area() = 12
print("area is 12")A module may declare child modules of its own. Those live in a directory named after the parent module: if compiler.scaly declares module lexer, the child is loaded from compiler/lexer.scaly. This mirrors how the standard library and the compiler themselves are laid out.
Top-level statements of a loaded module are ignored — only its definitions become part of the program. Programs have top-level statements; libraries consist of definitions only.
A package is a source distribution. Its version is encoded in its filesystem path — packages/scaly/0.1.0/ — and that path is the single source of truth; there is no separate version manifest. The statement
package scaly 0.1.0
declares a dependency on that package. Packages are searched in
./packages/ first, then
/usr/share/scaly/packages/, then any paths given
with -I. When SCALY_HOME is set, the
standard packages are resolved under
$SCALY_HOME/packages/.
Multiple versions of the same package can coexist in one program: the version is baked into the mangled symbol names, so they never collide. Dependencies are not transitively visible — a package sees only the packages it declares itself — and circular package dependencies are forbidden.
The standard library package scaly is the implicit
prelude: every program depends on it automatically,
and its types are visible everywhere without any use
statement. Pass --no-prelude to compile without it, for
bare-metal or embedded targets. See the Section called Backend Preludes (Future Direction) in Chapter 4 for
what the prelude provides.
Type definitions are private to their module by default. A type defined in another module — even one loaded into the same program with module — is hidden until the current file imports it with a use statement. This keeps every file's dependencies explicit: the list of use statements at the top of a file is a complete inventory of the outside types the file relies on.
Three kinds of types need no use statement:
Types defined in the same file.
Types of the prelude — the standard library (String, Vector, Option, …) is always visible.
The namespace of a declared child module. When a file declares module geometry and geometry.scaly defines a namespace of the same name, that namespace is visible in the declaring file, so its members can be reached qualified:
; geometry.scaly
define geometry
{
function unit_area() returns int
1
}
; myprog.scaly
module geometry
let a geometry.unit_area() ; qualified access, no use neededVisibility applies to named type definitions. Free functions are resolved through their namespaces and are not gated separately.
A use statement names a type by its full dotted path. The path starts with the program or package name, followed by the module path, ending in the type name:
use myprog.shapes.Box ; Box from module shapes of program myprog use scalyc.compiler.Modeler ; Modeler from package scalyc
A module that is named after the single type it defines — the file Modeler.scaly defining Modeler — is addressed once, as the path spells it; the module and the type do not appear as two separate path segments.
A trailing * imports every type a module defines:
module shapes
use myprog.shapes.*
let b Box(3, 4) ; Box is visible through the glob
if b.area() = 12
print("glob works")Globs are convenient for a module whose types are used pervasively. Prefer single-type imports when a file uses only one or two names from a module — they document the dependency more precisely.
Referencing a type that exists in the program but has not been imported fails the compile. The error names the module that defines the type and states the exact use line that would import it:
myprog.scaly:3:7: error: function not found: Box - defined in
myprog.shapes but not visible here; add: use myprog.shapes.BoxTwo compiler flags support working with visibility:
--no-use-vis disables enforcement entirely (every
type in the program is visible everywhere, the pre-visibility
behavior), and --use-vis prints an advisory report of
every reference that strict visibility would reject — useful when
migrating an existing codebase, to collect the needed
use lines before turning enforcement on.