Modules and packages
Each .nl file defines one source module. Each source directory is one
package. These two boundaries are separate: a package contains modules, but it
does not add another source-level namespace by itself.
Derive a module name from its file
Section titled “Derive a module name from its file”The filename price_tools.nl derives the module name PriceTools. A module
header, when present, must match that derived name:
module PriceTools
export func subtotal(price : Int, quantity : Int) -> Int do price * quantityend
func apply_internal_fee(value : Int) -> Int do value + 1endTop-level declarations are private by default. export adds a declaration to
the module’s public signature. Another module may call
PriceTools.subtotal, but cannot name apply_internal_fee.
A source file without a module header can still be checked or selected as a
program entry, but no other source module can reference it. This is why the
first-program tutorial could omit a header safely. The header is optional
syntax, not a second way to choose a module’s name: when present, it must still
match the name derived from the filename. A leading underscore is not part of
that derived name: _helpers.nl derives Helpers, so a matching
module Helpers header keeps the file package-internal.
Refer to another module
Section titled “Refer to another module”Noodle has no import declarations. A visible module is used through qualified
names. In main.nl:
module Main
func main() -> Unit do total = PriceTools.subtotal(12, 3); Debug.trace(total);endBoth files live directly in the same package directory:
shop/ main.nl price_tools.nlCheck the package and run the selected entry file:
noodle check shopnoodle run shop/main.nlThe entry file must declare exactly one non-generic main with no parameters
and a Unit body result. main does not need to be exported.
Split one module into source parts
Section titled “Split one module into source parts”Noodle also has a source-part mechanism for physically splitting one module
across several files. A part is not another module: it shares the primary
file’s module header, private namespace, checking state, and public signature.
Declarations in different parts can therefore refer to one another as if they
had been written in the same source file. A common use is moving a module’s
tests into one or more *.tests.part files while keeping the tests close to
the private implementation they exercise.
The primary .nl file selects parts with a top-level include directive. The
part filename is a basename, not a path, and its companion file is tied to the
primary file:
compiler/frontend/parser.nlcompiler/frontend/parser.nl.cursor.partcompiler/frontend/parser.nl.types.partThe corresponding directives are written in parser.nl:
module Parser
include "cursor.part"include "types.part"For a module whose implementation and tests are split separately, the primary file can include both kinds of part explicitly:
compiler/typing/function_bodies.nl: include "functions.part" include "functions.tests.part"The compiler accepts an optional module header and include directives only
in the primary file. A .part file contains complete top-level declarations;
it cannot declare another module or include another part. Part names use the
lowercase, dot-separated .part form. The position of each include and the
order of the directives determine where its declarations enter the module; the
compiler does not infer parts from directory order or filename sorting. Only
explicitly included parts participate in the module.
Parts retain their own file identity. Diagnostics and autofilled source
locations point to the .part file where the declaration or expression was
written, rather than pretending that all text came from the primary file.
Parts do not create separate modules, .nli signatures, JavaScript module
artifacts, package entries, or .extern.mjs companions; one primary module has
one artifact and one external companion boundary.
Treat .part as a last resort, not as the normal way to organize a package.
It is useful for quickly splitting tests out of implementation code, for
generated parser/compiler sources, or for a genuinely shared private scope
where extracting a module would create an artificial interface or a module
cycle. A test part is still part of the production module: it does not become
an independently importable test module or receive a separate signature.
For a reusable subsystem that will be maintained over time, prefer several
ordinary modules with explicit export boundaries, and keep tests in the
module or package layout that best reflects those boundaries. That refactoring
makes ownership, dependencies, and test scope visible to readers instead of
preserving one very large shared scope.
Package directories do not recurse
Section titled “Package directories do not recurse”Only .nl files directly inside the package directory belong to that package.
A child directory is a separate package; it does not inherit the parent’s
manifest or gain privileged access to the parent.
A package whose directory name starts with _ (such as app/_tools) is
package-internal: only the package rooted at its immediate parent directory
(app) may declare it as a dependency and import its modules. A sibling
package, an unrelated package, a grandparent package, or a transitive consumer
that reaches it directly gets an internal-package-dependency error. The
check uses canonical package directories, so .. path components and symlinks
cannot bypass it. This is separate from package-internal modules
(_helpers.nl): the module rule restricts which file inside a package may
import one file, while the package rule restricts which package may depend on
a whole child package.
The manifest is optional
Section titled “The manifest is optional”Without nlpkg.json, a package has no direct dependencies except the default
standard library. Add a manifest only when package-level configuration is
needed. A manifest’s dependencies names the packages whose modules the
current package may reference:
{ "dependencies": ["../domain"]}A dependency’s modules are then used under their ordinary qualified names,
exactly like modules of the same package. Here app declares domain as a
dependency and calls the Order module defined there:
app/ nlpkg.json main.nldomain/ order.nlmodule Order
export datatype Order Order{unit_price: Int, quantity: Int}end
export func subtotal(order : Order) -> Int do order.unit_price * order.quantityendmodule Main
func main() -> Unit do order = Order.make(?{unit_price: 120, quantity: 3}); Debug.trace(Order.subtotal(order));endThe same qualified-name syntax works whether the module lives in the current package or in a declared dependency; the manifest is what makes the other package’s modules source-visible.
Dependency paths are resolved relative to the manifest’s package directory.
They must begin with ./ or ../ (or be the exact special name stdlib).
Only direct dependencies become source-visible; dependencies are not
re-exported transitively.
A dependency may declare a source namespace in its own manifest:
{ "namespace": "Acme.Tools"}Consumers then use the full path, such as
Acme.Tools.PriceTools.subtotal(12, 3). Namespaces affect source references,
not the package’s physical identity.
Generated artifacts
Section titled “Generated artifacts”noodle build, run, and test write derived files under .nlc/. Source
modules remain separate modular ESM artifacts. .nlc is compiler-owned output:
it does not define modules, and handwritten .nli files are not authoritative
source interfaces.
You can now build useful ordinary programs. Continue to Advanced topics when you need reusable abstractions or explicit effect contracts, or open the Standard library to learn collections and common APIs.