Documentation / Packages and dependencies

Packages and dependencies

Dependencies make names visible, module roots define public boundaries and packages provide stable named surfaces. Moth keeps those jobs separate.

Unlike languages that expose any reachable filesystem path, Moth resolves source dependencies from the owning module root and stops at each module facade. Project-local support packages share code without opening private paths.

Choose the explanation level: Dependency paths

Dependency paths

Dependencies make names visible. Every source dependency resolves from the binding file's owning module root, not the file's directory.

A module-root-relative dependency

@card render_card

@card resolves to card.moth under the binding file's owning module root, regardless of how deep the binding file is inside the module.

A support package dependency

@markdown render

@markdown resolves to a scoped +*.moth support package visible in the binding module's lexical scope.

Dependency paths resolve declaration surfaces, not file bytes. Every source dependency clause resolves from the declaring file's owning module root, not the file's physical directory.

A dependency clause and a file path in expression position are two different semantic roles for the same @ spelling. A clause binds declarations or a namespace and produces no value. An explicit-extension path in expression position evaluates to a String. Neither owner reinterprets the other's paths: a clause never becomes a file value, and a file value never exposes a provider namespace. See File paths.

Path families

  • Module-root-relative: dependency clauses resolve from the declaring file's owning module root. Example: inside src/internal/deep/renderer.moth, @accounts Account resolves to src/accounts.moth, not src/internal/deep/accounts.moth.
  • Child module: reaching a child normal module ends filesystem traversal and exposes only its export: facade. Dependency clauses cannot bypass that facade with paths such as @child/internal.
  • Support package: scoped +*.moth support packages are depended on by package name. Consumers do not encode ancestor paths.
  • Core binding-backed package: @core/math, @core/text and current peers are registered by the builder.
  • Builder source-backed package: @html is a builder-provided source-backed package.
  • Builder binding-backed package: @web/canvas is a builder-owned runtime package.
  • JavaScript file: project-local annotated JavaScript files require .js and a builder external import provider. The .js extension is kept.

Resolution rules

  • @./... has no supported meaning.
  • @/... absolute-root paths are invalid, and so is bare @/. Dependency resolution starts at the owning module root, so no absolute root is meaningful here. In expression position bare @/ is instead the site-root URL. See File paths.
  • Parent components and .. are invalid.
  • Paths may traverse ordinary unrooted directories owned by the same module.
  • .moth, .mtf and .md source dependency paths are extensionless.
  • Direct .js dependency clauses keep the .js extension.
  • Scoped support packages are injected by package name, not physical ancestor paths.
  • Core and builder runtime packages use their registered package path.
  • Specific symbol bindings use flat direct selections. Direct symbol paths such as @core/math/sin remain invalid.
  • Direct dependency clauses on +*.moth support root files and config.moth are rejected.
  • @@name is invalid. The leading @ starts a dependency path and is not part of the module name. Normal module-root filenames such as @page.moth are not depended on directly.
  • A bare explicit-extension clause requires as or at least one direct selection. An explicit-extension clause with no registered provider gets a targeted diagnostic.
  • A recognised source extension used with an explicit extension gets an extensionless-source diagnostic. @docs/intro.mtf content stays invalid as a clause even though @docs/intro.mtf is valid in expression position.
  • Moth template .mtf and plain Markdown .md content files both expose generated content constants. See Moth template content dependencies and Markdown dependencies.
  • The bound public surface of a module root comes from its export: block. See Module visibility.

Same-module dependencies

Files in the same module can bind ordinary sibling source files directly. Same-module dependency clauses bypass the public-surface boundary. See Module visibility.

Cross-module dependencies

Dependency clauses that cross into another module boundary can only access what that module's export: block makes public. Cross-module dependency clauses cannot bypass the public API. See Module visibility.

Content-file dependencies

Moth template and plain Markdown content files are bound with the same extensionless syntax. Their dependency shape is covered by the Moth template and Plain Markdown pages, not here.

Related concepts

Choose the explanation level: Dependency clauses

Dependency clauses

Direct-selection clauses bring multiple symbols. Namespace clauses bring a whole surface.

Two colliding render symbols

@blog render as render_blog
@docs render as render_docs

Two modules both export render. Aliases keep them apart.

A namespace alias

@core/math as math

result = math.sin(1.0)

math gives you field access to the whole math package.

Direct-selection and namespace dependency clauses provide compact syntax for binding multiple symbols or a whole declaration surface.

Direct symbol dependencies

@core/math sin, cos, PI

@components render as render_component, Button as UiButton, Card

Each direct selection binds one symbol into the file. as creates a file-local alias. One authored clause keeps one shell identity and resolves one provider surface. Selected names do not become independent provider clauses.

Direct-selection termination

The path and the first direct selection must start on the same physical line. A comma explicitly continues the list across a following newline:

@core/math sin, -- the comma keeps the clause open
    cos

Without a comma, a newline ends the dependency clause:

@core/math sin
next_value = build_value()

The comma always promises another selection, so a trailing comma is invalid:

-- Error
@core/math sin, cos,

The same activation, comma-continuation and no-trailing-comma rules apply inside export:.

Namespace dependencies

@core/math

angle = math.PI
result = math.sin(angle)

A bare namespace clause brings the whole surface as a field-access-only namespace. Use math.sin(1.0) for values and math.PI for constants, but do not pass the namespace record itself as a value.

Namespace alias

@core/math as math

@vendor/drawing.js as drawing

A clause-level as gives the namespace a local name. This is the standard form for external package namespaces.

Entry aliases

@core/math sin as sine, PI as pi

Each selected entry can have its own alias. A clause-level as is mutually exclusive with direct selections and renames the whole namespace.

Bindable symbol categories

  • functions
  • structs
  • choices
  • type aliases
  • traits
  • compile-time constants

Runtime top-level bindings are not bindable. See Module visibility.

Namespaced access

Source and module namespace clauses are shallow. External package namespaces can expose nested package-local symbols. Use field-access syntax on the namespace record, but do not pass the namespace itself as a first-class value. Source-authored receiver methods are called through receiver-call syntax rather than as namespace fields.

Dependency clauses are compile-time declarations

Dependency clauses are not first-class runtime values. They exist at compile time to make names visible. Dependency bindings are file-local and visible throughout the file independent of clause position.

Direct-selection rules

  • Empty selection lists are unrepresentable.
  • A leading @ inside a selection list is invalid.
  • Selections are flat identifiers and cannot contain child paths or nested selection syntax.
  • Duplicate selected names and duplicate local names are diagnosed.
  • A direct-selection clause binds selected declarations from the one resolved provider surface.
  • Clause-level as and direct selections are mutually exclusive.
  • An invalid inferred namespace stem requires as.
  • Receiver methods remain attached to their receiver types and cannot be selected separately.

Collision and no-shadowing policy

  • Ordinary dependency aliases are file-local.
  • A dependency alias cannot hide an existing visible name.
  • If render is already declared in the file, @other render is an error. Pick a different local name.
  • Leading-case mismatches warn. For example, binding a struct with a lowercase alias or a function with an uppercase alias triggers a warning.

Provider clauses

Explicit-extension paths select a registered provider:

@drawing.js draw
@drawing.js as drawing

A bare explicit-extension clause requires as or at least one same-line direct selection. An explicit-extension clause with no registered provider gets a targeted diagnostic. A namespace has one provider origin. Consumer-created mixed-origin namespace aggregation is outside scope.

The same spelling in expression position is a file value, not a provider. @drawing.js as drawing binds the provider's namespace; url = @drawing.js is the file itself, as a resource String. A clause never becomes a file value, and a file value never exposes a provider namespace. See File paths.

Related concepts

Choose the explanation level: Module visibility

Module visibility

Declarations are private to their module. The export block makes them public.

An internal helper file

-- src/components/utils.moth
internal_value #= 42

The child root uses it directly

-- src/components/@api.moth
@utils internal_value

export:
    @utils internal_value
;

Same-module dependencies see everything. No export block needed.

Other modules need the export block

-- src/@site.moth
@components internal_value

Now the parent module can bind internal_value through the @components public surface. It cannot bypass that surface with @components/utils.

Module visibility controls which declarations cross a module boundary. Normal declarations are private to their module. The export: block marks what other modules can see.

Same-module internal visibility

Files within the same module can bind each other's declarations directly through dependency clauses. A module is defined by its root directory. All .moth files in that directory (and its subdirectories that do not have their own root) belong to the same module.

Given this topology:

src/
├── @site.moth
└── components/
    ├── @api.moth
    └── utils.moth

utils.moth and @api.moth belong to the same components child module. Same-module dependency clauses can see private declarations.

Internal helper:

-- src/components/utils.moth
internal_value #= 42

internal_fn || -> String:
    return "internal"
;

Child root:

-- src/components/@api.moth
@utils internal_value, internal_fn

export:
    @utils internal_value
;

Same-module dependency clauses bypass the public-surface boundary. The declaring file sees all declarations in the target file.

Cross-module public boundary

A different module can only access declarations marked public by the module root's export: block. Cross-module dependency clauses cannot bypass the public API.

-- src/@site.moth
@components internal_value

The parent module sees only the components child module's export: surface. It cannot bypass that boundary with @components/utils internal_value. The @components/utils path cannot bypass the child facade.

Without the root export block, the cross-module dependency clause is rejected with MOTH-IMPORT-0011.

Normal modules are not source packages

An @*.moth normal module is not a source package. Use +package.moth when the example intends to teach a scoped package. See Project-local packages for the support-package contract.

Root-private declarations

Declarations in the module root file outside the export: block are private to the root. They are not available to other modules even through the public surface.

Direct root dependency rejection

Direct dependency clauses on normal module-root files are rejected. Depend on the module through its directory path, not through the root filename.

Runtime top-level bindings are not bindable

Runtime top-level bindings are start-body code. They are not bindable declarations. Only compile-time constants, functions, structs, choices, type aliases and traits are bindable.

Receiver method visibility

Receiver methods are visible through a module's public surface when the module exports the receiver type's source surface. A type alias in the export block does not automatically re-export private implementation methods. Export the receiver type's source surface or a wrapper explicitly.

Dependency cycles

Circular dependency clauses between files in the same module are accepted when the declarations resolve in dependency order. Circular constant dependencies are rejected with MOTH-RULE-0033.

No public access through filesystem tricks

There is no way to bypass the module public-surface boundary by choosing a different dependency path. All cross-module dependency clauses go through the same resolution and visibility check.

Related concepts

Choose the explanation level: Public re-exports

Public re-exports

The export block can re-export dependency-bound symbols so other modules can use them.

A public re-export

export:
    @components/card CardData as Card, render_card
;

Other modules can now depend on Card and render_card through the module's public surface. CardData is not also exported unless the block lists it separately.

Local alias is not public

@components/card CardData as LocalCard

LocalCard is file-local. It is not public. Only dependency selections inside the export: block become public re-exports.

The export: block can re-export selected dependency bindings from other files, creating a public API surface that exposes declarations under controlled names.

Direct-selection re-export

export:
    @components/card CardData as Card, render_card

    @core/math PI, sin
;

Card, render_card, PI and sin become accessible to cross-module dependency clauses. A leaf alias inside export: becomes the public name. The original CardData name is not also exported unless it is listed separately.

Re-export list termination

The path and the first selected name must start on the same physical line. A comma explicitly continues the selection list across a following newline:

export:
    @components/card CardData,
        render_card
;

Without a comma, a newline ends the re-export clause. A trailing comma is invalid because a comma always promises another selected name.

export:
    -- Error
    @components/card CardData, render_card,
;

Renamed public re-export

The as alias inside an export-block re-export defines the public API name. The original source name is what the module binds privately. The alias is what other modules see when depending on the module's public surface.

Re-export rules

  • Only explicit non-empty direct-selection lists are valid dependency re-exports.
  • Whole namespaces cannot be re-exported implicitly.
  • Clause-level namespace as is invalid inside export:.
  • Leaf aliases define public API names.
  • Selected names are flat identifiers and may not qualify nested declarations.
  • Non-leaf namespace aliases are invalid because namespace exports remain deferred.
  • Re-exports preserve declaration origins.
  • Re-export syntax does not create namespace values.

Exported dependency types, constants and functions

Any bindable declaration can be re-exported: functions, structs, choices, type aliases, traits and compile-time constants.

Re-export does not make a local alias automatically public

A private @path Symbol as LocalName clause outside the export block creates a file-local alias. It does not become public. Only a dependency clause inside the export: block creates a public re-export.

Public name collisions

Public names in the export block must not collide with each other. Duplicate public names are rejected.

Invalid unknown or private targets

Re-exporting a symbol that does not exist in the target file is rejected. Re-exporting a private declaration that is not visible in the target file is rejected.

No legacy inline export syntax

Legacy inline export syntax (without the : block delimiter) is rejected with MOTH-SYNTAX-0001. The strict export: block is the only public API marker. See Public API for the full export-block rules.

Related concepts

Choose the explanation level: Project-local packages

Project-local packages

Scoped support packages are +*.moth roots in the project source tree. The containing directory supplies the package name.

A simple support package

src/
├── @site.moth
└── blog/
    ├── +package.moth
    └── helpers/dates.moth

+package.moth makes @blog a scoped support package visible to @site and its sibling modules.

Binding from a package

@blog post_title

@blog resolves to the blog directory's support root. The package's public surface comes from its export: block.

Project-local source-backed packages are scoped +*.moth support packages discovered structurally from the project source tree. The retired package_folders config field no longer exists and is rejected; structural discovery is the only package-root source.

Directory structure

project-root/
├── config.moth
├── src/
│   ├── @site.moth
│   ├── markdown/
│   │   ├── +package.moth
│   │   ├── parser/
│   │   │   └── @parser.moth
│   │   └── rendering/
│   │       └── @rendering.moth
│   └── pages/
│       └── @pages.moth
└── +package.moth

The markdown directory contains a +package.moth root, making @markdown a scoped support package visible to @site, @pages and their descendants.

Scoped visibility

For a support package S whose nearest ancestor normal module is P:

  • S is visible to P
  • S is visible to normal sibling modules of S and their descendants
  • S is not visible above P
  • S is not visible outside P's subtree
  • S is not visible inside its own private implementation subtree
  • another support package in the same owner scope cannot depend on S

Public surface

Outside dependency clauses can only see declarations and direct-selection re-exports marked public by the support root's export: block. Internal implementation files are hidden unless the root re-exports them.

Private subtrees

The support facade may depend on ordinary files it owns, any descendant module in its private subtree, support packages from a strictly outer scope and registered packages. It may not depend on its parent, normal sibling consumers or same-scope support siblings.

Consumers see only the support facade's export: surface. They cannot address @markdown/parser or another implementation path.

Depended-on package roots do not produce routes

Code depends on a support package root rather than activating it as an HTML route. Support roots reject top-level runtime work and page fragments. They export declarations but produce no HTML artefact. See HTML routing and artefacts.

Origin and backing are separate

Scoped +*.moth packages are ProjectLocal packages backed by Moth source. @html is also source-backed but has Builder origin. @core/math and @web/canvas are binding-backed packages with Core and Builder origins respectively. See Package origins and backing.

Related concepts

Choose the explanation level: Package origins and backing

Package origins and backing

Packages can differ in two ways: who provides them and what backs their implementation.

Origin

  • Core packages come from the compiler.
  • Builder packages come from the selected project builder.
  • ProjectLocal packages come from your project.

Backing

  • Source-backed packages contain ordinary Moth modules.
  • Binding-backed packages expose typed functions, constants and opaque types implemented outside Moth.

@html is a source-backed Builder package. @core/collections and @core/math are binding-backed Core packages. @web/canvas is a binding-backed Builder package.

Every package has two independent classifications: where it comes from and how the compiler obtains its implementation.

Package origin

Origin describes ownership and distribution:

  • Core: compiler-owned packages such as @core/io and @core/math.
  • Builder: packages selected by a project builder, including @html and @web/canvas.
  • ProjectLocal: scoped +*.moth support packages, the project-root package facade and annotated project-local JavaScript files.
  • Standard and Dependency: reserved for future package-system support.

Package backing

Backing describes implementation:

  • MothSource: one or more Moth modules resolved through the source package registry.
  • ExternalBinding: typed functions, constants and opaque types implemented outside Moth.

Package

Origin

Backing

@html

Builder

MothSource

@core/collections, @core/io, @core/math, @core/text, @core/random, @core/time

Core

ExternalBinding

@web/canvas

Builder

ExternalBinding

Scoped +*.moth support package

ProjectLocal

MothSource

Project-root +*.moth facade

ProjectLocal

MothSource

Annotated project-local .js file

ProjectLocal

ExternalBinding

Origin and backing don't change dependency clause syntax or module visibility. Source-backed packages contain ordinary modules with strict export: public surfaces. Binding-backed packages expose typed external symbols registered by the builder.

Prelude is visibility policy

The prelude is implicit dependency policy, not an origin or backing. It exposes the bare io namespace as an alias to @core/io. Package metadata belongs to @core/io, while bare-name visibility belongs to the prelude.

Builder-selected availability

The builder decides which packages exist. Unsupported packages are rejected with an unsupported-by-builder diagnostic. The HTML builder opts into @html, @web/canvas and current optional Core packages.

Binding-backed package constraints

Binding-backed packages expose free functions, constants and opaque types. They don't expose source-style receiver methods. Use a source-backed wrapper type for method-style APIs. External calls are positional-only because named arguments, default parameters and reactive parameters are source-function features.

The HTML-Wasm mode has a stricter backend surface. A reachable JS-backed package needs a Wasm lowering before that backend can use it.

No package manager yet

There is no remote fetching, lockfile or version resolution yet. Use +*.moth support packages for project-local source-backed packages, builder-provided package dependency clauses for built-in capabilities and annotated .js wrappers for direct browser integration.

Related concepts

Choose the explanation level: External binding contracts

External binding contracts

External binding packages give you typed functions and opaque types from code written outside Moth. The HTML builder supports annotated JavaScript imports and will support WIT component imports in the future.

Annotated JavaScript

You can import .js files that use @moth.opaque and @moth.sig annotations. The annotations tell the compiler what Moth names and types to expose.

@vendor/drawing.js as drawing

JavaScript export names are runtime details. Moth names come from annotations, not from the JavaScript export.

What JavaScript can export

The builder accepts export function name(...) { ... } and block-bodied arrow exports. @moth.sig annotations expose free functions.

What JavaScript can't export

The builder rejects default exports, re-exports, CommonJS, classes, callbacks, async functions, property accessors and multi-success returns. These don't map to Moth's type system.

Values cross by value

Moth values that cross into JavaScript go by value. JavaScript can't hold references into Moth storage. Opaque handles represent foreign identities, not Moth references.

WIT is deferred

Future Wasm component imports will use a value-only WIT profile. WIT resources, callbacks, async and streams remain deferred. See External binding contracts advanced for the full contract and deferred surface.

External binding packages expose typed functions, constants and opaque types implemented outside Moth. The HTML builder supports two binding profiles: annotated project-local JavaScript files and future value-only WIT component imports.

Annotated JavaScript dependencies

The HTML builder supports annotated single-file .js dependency clauses through @moth.opaque and @moth.sig. @moth.sig declares each exposed function. @moth.opaque is required only when signatures use foreign opaque handle types. A function-only module may use @moth.sig without @moth.opaque. JavaScript export names are runtime implementation details. Moth names come from annotations.

@vendor/drawing.js as drawing

Annotated JavaScript example

/**
 * @moth.sig emphasize |text String| -> String
 */
export const emphasize = (text) => {
    return `**${text}**`;
};


/**
 * @moth.sig join_label |prefix String, body String| -> String
 */
export function joinLabel(prefix, body) {
    return `${prefix}: ${body}`;
}

The Moth dependency clause uses the annotation names, not the JavaScript export names:

@vendor/drawing.js emphasize, join_label

Supported JavaScript export forms

  • export function name(...) { ... }
  • block-bodied arrow exports

@moth.sig annotations expose free functions. this receiver-style signatures are rejected during registration. Runtime imports from builder-registered modules must be named static imports.

Exact signature subset

  • parameters are positional and typed
  • mutable parameters use name ~Type
  • signatures have zero or one success return
  • an optional final Error! channel is allowed
  • Error! must be final
  • generic signatures are rejected
  • option and collection types are rejected
  • callbacks are rejected
  • multi-success returns are rejected
  • this receiver-shaped signatures are rejected for external package registration
  • opaque types must be declared through @moth.opaque before use where required
  • JavaScript export names are runtime implementation details
  • Moth names come from annotations

Rejected JavaScript features

  • arbitrary dependency graphs
  • default exports
  • re-exports
  • CommonJS
  • classes
  • JS constants
  • property accessors
  • callbacks
  • async functions
  • collections or options in JS signatures
  • generic external type
  • receiver methods
  • multi-success JS returns

Restricted host-binding profile

Existing Core, Builder and JavaScript-backed bindings follow a restricted host-binding profile:

  • ordinary Moth values cross by value
  • host code may not retain references into ordinary Moth storage
  • opaque handles represent foreign identities rather than Moth reference types

Observable external resources require explicit close or teardown. Host finalisation timing does not define language behaviour.

WIT value-only component profile

WIT component imports are accepted but deferred. Future general Wasm library imports use a value-only WIT component profile:

  • supported arguments lower from shared reads into independent component values
  • results lift into independent Moth result graphs
  • no Moth alias, lifetime owner, ownership state or destruction responsibility crosses the component boundary

Deferred WIT surfaces

The V1 value-only profile defers WIT resources, callbacks, async operations, futures, streams, shared-memory views, raw pointers, returned aliases and retained Moth references.

Related concepts

Package reference pages