Documentation / Project structure

Project structure

A Moth project consists of a set of modules, one strict project config and a builder that decides what those modules produce.

The root filename begins with #, but the name after it carries no importance. The source tells the compiler what counts as public. The builder decides whether the module becomes an artefact.

Choose the explanation level: Project layout

Project layout

A Moth project has one config file, one source root and generated output folders.

Where things live

  • config.moth at the project root controls build settings.
  • src/ (or whatever entry_root says) holds your source files.
  • dev/ and release/ are generated output. Do not edit them.

One root file per module directory

Each source directory can have one @*.moth file that marks it as a module. The @ filename is a convention. @page.moth, @api.moth and @mod.moth all mean the same thing to the compiler.

Shared scoped packages

A +*.moth file in a source directory marks it as a scoped support package. For example, shared/+package.moth creates @shared. Its owner module, normal sibling modules and their descendants may import it. Other support packages in the same scope and the package's private implementation subtree cannot.

The current scaffold creates an empty legacy lib/ folder. It has no special meaning in the accepted package design.

Outputs are generated

The build command creates output folders. They are not source. The builder cleans up stale artifacts automatically.

A Moth project is a directory tree with one strict config file, a source entry root and generated output folders.

Project root contents


        project-root/
        ├── config.moth
        ├── src/
        │   ├── @site.moth
        │   ├── shared/
        │   │   └── +package.moth
        │   └── pages/
        │       └── @pages.moth
        ├── dev/
        │   └── .moth_manifest
        ├── release/
        │   └── .moth_manifest
        └── .gitignore
    

Directory roles

  • config.moth is the only project config file. It lives at the project root and is not module source.
  • The configured entry_root (usually src) is the source folder the compiler searches for module roots.
  • dev/ and release/ are builder output folders, not source folders. They are generated and managed by the build system.
  • .moth_manifest files are build-system metadata used for safe stale-output cleanup. They are not source.

Structural packages

Project-local source-backed packages are structural +*.moth support packages. A +*.moth file in a source directory marks that directory as a scoped support package root named by its containing directory.

In the example above, shared/+package.moth creates the scoped @shared support package. It is visible to its owner normal module, normal sibling modules and their descendants. It is not imported from its own private implementation subtree or from another support package in the same scope.

No folder gains package meaning without a +*.moth root. Ordinary directories do not automatically become packages.

The optional project-root +*.moth facade beside config.moth is a separate mechanism for assembling public descendant surfaces for external consumers. It is not a scoped support package.

For the full package contract see Project-local packages.

Normal module-root files inside source directories

A non-config file whose name starts with @ and ends with .moth is a module root. The filename after @ has no language-level semantic role. @page.moth, @mod.moth, @api.moth and @anything.moth are all equivalent root filenames.

The moth new html scaffold generates src/@page.moth as a conventional default. That convention does not make @page.moth a special compiler role. Any @*.moth filename in a source directory marks that directory as a module root.

Implementation note

The current scaffold still creates an empty lib/ folder and the compiler retains legacy package_folders support. These are implementation drift and are not part of the accepted project/package model. See the progress matrix for current status.

Source versus output

Source directories are owned by the developer. Output folders are generated by the builder and should be treated as build artifacts, not source. The builder owns stale-output cleanup for generated artifacts.

Builder-neutral frontend versus builder-owned output

The compiler frontend discovers modules, resolves imports, checks types, lowers code and validates borrowing. A project builder then decides how compiled modules become real output files. The current user-facing builder is the HTML project builder.

This separation means the same frontend can later feed other builders. The frontend does not decide which modules produce artifacts. That decision belongs to the builder.

Only config.moth has config semantics

No other filename has config meaning. @config.moth is a module root, not a config file. Alternate config filenames, compatibility handling and config-specific diagnostics for other names are not supported.

Related concepts

Choose the explanation level: Project config

Project config

Accepted deferred design. This page teaches the final grouped config shape. The current compiler still uses a transitional flat config format. See Progress for current support.

config.moth at the project root controls build settings. It is one self-contained compile-time source file with no source imports.

A minimal config


    project #= |
        name = "my_project",
        entry_root = "src",
    |

    html #= |
        dev_output = "dev",
        release_output = "release",
    |
    

Structure

  • One required project record provides project identity and settings.
  • project.name is required for stable identity.
  • Builder sections like html hold builder-specific settings.
  • The command selects the builder before config validation. config.moth does not select the builder.
  • Only active sections are schema-validated.

Accepted deferred design. This page defines the final grouped project-config contract. The current compiler still uses a transitional flat config shape and legacy package_folders discovery. See Progress for current support.

config.moth is the single project config file. It lives at the project root, is loaded before source-tree discovery and is one self-contained compile-time source file with no source imports or package resolution.

Canonical config


    default_channel #= "alpha"

    project #= |
        name = "moth_docs",
        version #Import of String = "0.1.0",
        entry_root = "src",
        metadata = |
            channel = default_channel,
        |,
    |

    html #= |
        dev_output = "dev",
        release_output = "release",
    |
    

Structure

  • One required open project const record provides stable project identity.
  • project.name is required and must be a valid package-style identifier.
  • Private helper constants declared before use are allowed.
  • Top-level builder and tooling section records are allowed.
  • All sections are folded, but only active sections are schema-validated.
  • The active builder project section is required, even when empty.
  • Inactive sections are parsed and folded, then discarded.
  • Project and entry schemas are separate with no shared fields.
  • Project fields may contain folded scalar values, optionals, nested anonymous const records, collections and folded template strings.
  • Builder sections use backend-neutral folded values and cannot declare #Import.
  • Output settings are builder-owned.

Direct project #Import fields

A direct primitive or optional field of project may declare a build-input contract using #Import of T. Nested project fields cannot declare imports.

Accepted imported-value types are String, Int, Float, Bool, Char and optional forms of those types.

See Build system design for the full #Import and @project contract.

Rejected in config

  • Every source import, including relative, project, Core, Builder, dependency and binding-backed imports.
  • Runtime declarations, mutable bindings, functions, named support types, traits, conformances.
  • Standalone templates and top-level const page fragments.
  • export:.

Config is loaded before module discovery

The command selects the builder before config schema validation. config.moth does not select the builder. Config is parsed and folded before Stage 0 source-tree indexing begins.

Related concepts

Choose the explanation level: Module roots

Module roots

Each source directory can have one @*.moth file that marks it as a module.

One root per folder

The filename after @ does not matter. @page.moth, @mod.moth and @api.moth all mean the same thing. Two @*.moth files in one folder are rejected.

Import the module, not the root file

Normal module-root files have no direct import form. Import the directory's public module surface.

The @ in @page.moth marks the file as a module root on disk. The @ in import @pages starts an import path. They share the same symbol but are not combined. Do not write import @@pages. The second @ is invalid.


    import @pages
    

Two roles

A module can become a page entry or be imported. The compiler checks its top-level work in both cases, but that work runs only when the builder selects the module as an entry.

A non-config file whose name starts with @ and ends with .moth is the module root for its containing directory.

Root filename semantics

The filename after @ has no language-level semantic role. These are all equivalent module root spellings:


        @page.moth
        @mod.moth
        @api.moth
        @anything.moth
    

The compiler treats each one as the module root for its directory. The HTML route comes from the directory position, not from the root filename.

One root per directory

A source directory may contain at most one non-config @*.moth file. Two or more normal module-root files in one directory are rejected with MOTH-CONFIG-0001.

Config files are not module roots

config.moth is the project config file. It is not a module root, does not produce HIR and does not export declarations. @config.moth is a module root, not a config file. Only plain config.moth without @ has config semantics.

Direct root-file imports are rejected

Normal module-root files have no direct import form. Import the directory's public module surface.

The @ in @page.moth is a filesystem marker identifying a normal module root. The @ in import @pages is the source syntax that introduces an import path. These use the same symbol to visually connect module roots and import roots, but they are not concatenated. The module's import identity comes from its directory or registered package name. The cosmetic root filename is never an import name.

Given this structure:


        src/
        ├── @site.moth
        └── pages/
            ├── @page.moth
            └── article.moth
    

Valid:


    import @pages
    

Invalid:


    import @@pages
    import @@page
    

Neither @page.moth nor @site.moth is imported directly. Do not add the root file's leading @ to an import path.

Normal files belong to the surrounding module

Files without @ in the same directory as a root file are normal source files within that module. They contribute declarations that the module root can import and export. They do not execute top-level runtime code independently.

Active versus imported module roots

Every selected normal module is compiled into an immutable module artefact. Its top-level runtime work becomes a dormant compiler-synthesised start. When the builder selects that module as an entry, entry assembly activates start exactly once. Importing the module exposes only its public interface and never activates its root work.

  • Entry-selected root: the builder selects this module as an entry. Entry assembly activates its dormant start and its direct top-level templates become page fragments.
  • Imported root: its top-level runtime and page-fragment activity is inactive. Only the declarations selected by export: are visible through the module boundary.

API-only roots

A module root with no builder-relevant top-level activity (no runtime code, no const fragments, no runtime fragments) is API-only. It is a valid module root that exports declarations but produces no builder artifact.

Migrating older module projects

  • Rename the project config file to plain config.moth.
  • Keep one non-config @*.moth root per source directory. Merge old @page.moth and @mod.moth roles when they shared a directory.
  • Replace inline export declarations and imports with one strict export: block in the module root.
  • Import the module directory or package prefix. Do not import the normal module-root filename directly.

Related concepts

Choose the explanation level: Public API

Public API

Declarations are private to their module by default. The export: block marks what other modules can see.

A compact export block


    private_helper || -> String:
        return "private"
    ;

    export:
        public_name #= "Moth"

        render || -> String:
            return private_helper()
        ;
    ;
    

Private by default

Files in the same module can see each other's declarations. Other modules can only see what the export: block makes public.

The export: block is the only public API marker. It is valid only in a module root file and marks declarations as visible to other modules.

Syntax


    private_helper || -> String:
        return "private"
    ;

    export:
        public_name #= "Moth"

        render || -> String:
            return private_helper()
        ;
    ;
    

Rules

  • export: is valid only in a module root file. An export: block in a normal source file is rejected with MOTH-RULE-0077.
  • A module root may contain at most one export: block. Duplicate blocks are rejected with MOTH-RULE-0085.
  • The block ends with normal Moth ;.
  • An empty export: block is rejected with MOTH-RULE-0080.
  • Items inside the block are ordinary top-level module-root items marked public.
  • Items outside the block remain private to the module root file.

Allowed inside export:

  • grouped source imports (re-exports)
  • grouped external imports
  • functions
  • structs
  • choices
  • type aliases
  • traits
  • compile-time constants using #

Rejected inside export:

  • runtime bindings
  • mutable bindings
  • top-level executable statements
  • runtime templates
  • top-level const page fragments
  • trait conformances
  • trait incompatibility declarations
  • receiver methods as directly exported symbols
  • bare namespace imports or exports
  • wildcard exports
  • export lists
  • function alias exports
  • nested export: blocks
  • all legacy inline export forms

Runtime statements inside the block are rejected with MOTH-RULE-0080. Legacy inline export syntax (without the : block delimiter) is rejected with MOTH-SYNTAX-0001.

Re-exports

The export block can re-export grouped imports from other files:


    export:
        import @components/card {
            CardData as Card,
            render_card,
        }

        import @core/math {
            PI,
            sin,
        }
    ;
    

The grouped alias becomes the public name. In this example cross-module consumers can import Card, not CardData, unless the export block lists CardData separately.

# constants are compile-time, not visibility

The # marker on a binding means compile-time constant. It does not control cross-module visibility. Cross-module visibility is controlled exclusively by the export: block.

Same-module versus cross-module visibility

  • Normal declarations in any file within a module are visible to other files in the same module by default.
  • Cross-module imports can only access declarations marked public by the module root's export: block.
  • Private declarations in the module root file are not available to other modules.
  • A module root with no export: block exports nothing.

Related concepts

Choose the explanation level: Active-root runtime and fragments

Active-root runtime and fragments

The module root file owns your page's runtime work. Helper files provide declarations.

Root activation

Top-level runtime code in the module root becomes a dormant start. It runs only when the builder selects that module as an entry. Helper files never run their own top-level code.

Page fragments

Direct top-level templates in the root become page fragments:

  • A const fragment (the # prefix form) folds at compile time and becomes static HTML.
  • A runtime fragment (the plain bracket form) runs when the module is activated as an entry and fills a slot in the page.

Helpers

Helper files define functions, types and constants. They do not own an export block. The root imports them and chooses what becomes public. Do not put page fragments in helper files.

Every selected normal module is compiled into an immutable module artefact. Its top-level runtime work becomes a dormant compiler-synthesised start. The builder selects which modules become entries. Entry assembly activates start exactly once. Imported modules expose only their public interface and never activate their root work.

Entry-activated root runtime

The entry-selected module root's top-level runtime code becomes a dormant compiler-synthesised start function for that module. Entry assembly activates it exactly once. This includes top-level bindings, function calls and other runtime statements.

Normal files

Normal helper files define reusable declarations. They do not own an export block and cannot contain top-level runtime statements. The module root imports those declarations and decides which names become public.

Page fragments

Two forms of direct top-level template in a module root become page fragments:

  • A const fragment uses the # prefix before the opening bracket. It must fold at compile time and is emitted as static HTML.
  • A runtime fragment uses the plain bracket form without #. It is evaluated by the module's start function when entry assembly activates it, and inserted into the page at runtime.

Assigned or returned templates are not page fragments by themselves. Only direct top-level templates in the entry-selected module root become fragments.

Source order

The HTML builder preserves source order between static and runtime fragments. Static fragments are emitted immediately. Runtime fragments use generated slots in the HTML document, then the embedded JavaScript calls the module start function and fills those slots.

Imported roots

Imported module roots never activate their root runtime. When a module root is imported for its public API, its top-level runtime code, start body and page fragments remain dormant. Only its exported declarations remain available.

Do not put page fragments in helper files

Helper files should define reusable declarations. The module root should import and use them. Page-producing top-level templates belong in the entry-selected module root only.

Related concepts

Choose the explanation level: HTML routing and artifacts

HTML routing and artifacts

HTML routes come from directory position, not from the @ filename.

Directory routes


        src/@page.moth  -> index.html
        src/about/@page.moth  -> about/index.html
    

The root module is the home page. Each nested directory gets its own route.

API-only modules

A module with only exports and no page activity produces no HTML. Not every @*.moth file becomes a page.

The HTML builder derives routes from directory position under the configured entry_root. The normal module-root filename does not choose the route name.

Directory-based routes

The module root's directory determines the HTML output path:


        src/@page.moth              -> index.html
        src/docs/@page.moth         -> docs/index.html
        src/docs/basics/@page.moth  -> docs/basics/index.html
        src/@home.moth              -> index.html
        src/@api.moth               -> index.html
    

The root module maps to the project index. A nested directory maps to a nested index route. The normal module-root filename (@page.moth, @home.moth, @api.moth) has no effect on the route name.

API-only modules

A module root with no builder-relevant top-level activity is API-only. It does not emit an HTML artifact merely because a root file exists. Artifact emission requires builder-relevant root activity: runtime code, const fragments or runtime fragments.

Builder ownership

The HTML builder owns route and artifact policy. The frontend remains builder-neutral. The builder decides:

  • which modules produce HTML artifacts
  • how routes are derived
  • how output files are organised
  • whether to reject unsupported reachable features

Output folders

Output folders are generated and manifest-managed. Builder sections in config.moth own output settings. HTML defaults are development dev and release release. Generated output folders should be treated as build artifacts, not source.

Release versus development output

The default build writes to the dev folder. The --release flag writes to the output folder. The dev server serves from the dev folder.

Entry root artifact requirement

The HTML builder requires at least one artifact-producing module root at the configured entry root. If the entry root contains only API-only roots, the build is rejected with MOTH-CONFIG-0001.

Experimental Wasm mode

In the experimental --html-wasm mode, the builder can emit a route folder containing index.html, page.js and page.wasm instead of a single HTML file with embedded JavaScript.

Related concepts

Choose the explanation level: Build inputs

Build inputs

Build inputs let you declare named values that resolve before the compiler processes your source modules. They feed settings like version strings and feature flags into ordinary compile-time constants.

Accepted deferred syntax. Build inputs are queued implementation work. The examples below show the accepted end-state contract, not current compiler support. See Progress for implementation status.

A simple build input


    project #= |
        name = "my_project",
        version #Import of String = "0.1.0",
        entry_root = "src",
    |
    

The #Import of String part tells the build system that version can be overridden from the command line. When no override arrives, the default "0.1.0" applies.

Source-level build inputs

Any source module can declare its own build input the same way:


    api_url #Import of String = "https://api.example.com"
    

The build system validates every contract before module compilation. Same-name contracts must agree on type and default value.

Accepted types

Build inputs accept String, Int, Float, Bool, Char and optional forms of those types. Defaults must be plain literal values, not expressions or templates.

Related concepts

Accepted deferred design. This page defines the final source contract. The current compiler does not yet implement this surface. See Progress for current status.

Build inputs let config and source declare named primitive values that resolve before module AST compilation. They feed project identity, feature flags and environment-specific settings into ordinary compile-time constants.

Direct project #Import fields

A direct primitive or optional field of project may declare a build-input contract using #Import of T. Nested project fields cannot declare imports.

Accepted imported-value types are String, Int, Float, Bool, Char and optional forms of those types.


    project #= |
        name = "moth_docs",
        version #Import of String = "0.1.0",
        channel #Import of String? = none,
        entry_root = "src",
    |
    

#Import is constant-source syntax. It isn't a source import or a wrapper type.

A direct project #Import value resolves in this order:

  1. explicit CLI or programmatic build input
  2. builder-provided primitive global
  3. the folded declaration default
  4. a structured missing-input diagnostic

Resolution happens during config compilation before Stage 0 applies fields such as entry_root.

A fixed direct project field is not an import contract. When a same-name source #Import uses the same primitive type and optionality, the fixed field is its authoritative provider and blocks CLI override. Same-name source declarations must still agree on required or default state and the normalised default value.

Source #Import contracts

Source #Import is intentionally narrow so every project-wide contract can be validated before module AST compilation.

A source declaration may use only the accepted primitive or optional types listed for project fields.

A source default must be self-contained. The only accepted forms are:

  • a String literal
  • a signed Int literal
  • a signed Float literal
  • a Bool literal
  • a Char literal
  • none for an optional contract
  • a matching primitive literal for an optional contract

Source defaults cannot contain a name, template, operator expression, call, cast, field projection, collection, record or another imported value. Stage 0 doesn't run a second general constant evaluator before AST.


    api_url #Import of String = "https://api.example.com"
    max_retries #Import of Int = 3
    debug_mode #Import of Bool = false
    

Project-wide resolution order

Same-name contracts must agree on primitive type, optionality, required or default state and the normalised default value. Different defaults are conflicting contracts.

The project-wide resolution order is:

  1. a compatible fixed direct project field, which is authoritative and cannot be overridden
  2. a resolved direct project #Import field
  3. explicit CLI or programmatic input for a source-only contract
  4. a builder-provided primitive global
  5. the shared source default
  6. a missing-input diagnostic

Unknown explicit inputs are diagnosed only after every selected source contract is known.

The resolved value enters module AST as an ordinary folded constant. It creates no runtime wrapper or HIR category.

Related concepts

Choose the explanation level: Entry config

Entry config

Entry config lets each normal module root carry its own builder settings and access project-level values through @project.

Accepted deferred syntax. @project and entry-local config: blocks are queued implementation work. The examples below show the accepted end-state contract, not current compiler support. See Progress for implementation status.

Using @project

Import @project to read project fields like version or entry_root inside a module:


    import @project { version }

    footer #= [: v[version]]
    

@project is never implicitly injected. Import it explicitly when you need it.

Entry-local config: blocks

A normal module root may contain one optional config: block with builder metadata for that entry:


    config:
        html #= |
            title = "About",
        |
    ;
    

The block lives at the top level of the root file, outside export: and outside executable bodies. It creates no module symbols or runtime values.

Related concepts

Accepted deferred design. @project, source #Import and entry-local config: blocks are queued implementation work. This page defines the accepted final contract. The current compiler does not yet implement these surfaces. See Progress for current status.

Entry config covers @project and entry-local config: blocks. Both feed builder metadata and compile-time values into normal module roots.

@project

The folded project record produces a specialised immutable ProjectGlobalsInterface under the permanently reserved @project import root.

@project exposes direct project fields as namespace members. It doesn't expose another value named project.

Normal project modules and project-owned support packages may explicitly import @project. It is never implicitly injected.

The following may not claim the @project root:

  • child modules
  • scoped support packages
  • dependency aliases
  • Core packages
  • Builder packages
  • binding-backed packages

@project cannot be directly re-exported.

Internal project modules may expose declarations derived from project values. The compiler retains project-context provenance on every affected public semantic fact. The external project package facade rejects prohibited project-context exposure.


    import @project { version, entry_root }

    page_title #= [: v[version]]
    

Entry-local config: blocks

An entry config: block is root-local builder metadata. It isn't an embedded config.moth source file.

Placement rules:

  • valid only at the top level of a normal module root
  • at most one block per normal root
  • invalid in normal non-root files
  • invalid in support roots
  • invalid in the project package facade
  • invalid inside export:
  • invalid inside executable bodies
  • invalid in config.moth

The block contains section records only. Imports, aliases, helper constants, support types and source #Import declarations live outside the block in the normal root file.

The block uses the root file's ordinary compile-time visibility. It may reference:

  • imported constants
  • @project
  • same-file constants declared before the block
  • resolved source #Import constants
  • foldable local const-record types
  • selected-builder compile-time values available through normal module imports

Same-file forward references remain invalid.

The block creates no ordinary module symbol, HIR or project-global value. It cannot contain project or change project-level builder behaviour. It may contain active artefact-builder and tooling-overlay sections.

Active entry sections are schema-validated. Inactive sections are parsed and folded but not schema-validated.

Every normal module selected into the current command's semantic graph has its block validated whether or not an entry activates it. Imported modules never apply their entry metadata to an importer.

Only active artefact-builder settings contribute entry activity.

Related concepts

Choose the explanation level: Project package facade

Project package facade

A project can expose a public package API to other projects through an optional facade file at the project root.

What a facade does

A +*.moth file beside config.moth becomes the project package facade. It exports declarations that external consumers can import. Without it, the project has no externally consumable package surface.

The facade rejects top-level runtime work and page fragments. It only exports API declarations. Support roots follow the same rule.

How it reaches descendant modules

The facade can reference public interfaces of modules below entry_root even though ordinary module visibility wouldn't allow it. Stage 0 gives the facade a special assembly namespace for this purpose.

What the facade cannot do

  • It cannot import @project.
  • It cannot expose declarations that depend on project-private context.
  • It isn't visible to internal project modules.

Related concepts

Partial implementation. The source-package and module foundations are partially implemented. The complete external facade and project-context contract remains partial. This page defines the accepted final contract.

The project package facade exposes a project's public API to external consumers. It is an API-only module compiled through the ordinary compiler pipeline with project-facade visibility supplied by Stage 0.

The facade root

One optional project-root +*.moth file beside config.moth defines the external project package facade. The suffix after + is cosmetic.

The facade may define and export its own legal API-only declarations: functions, structs, choices, type aliases, traits and compile-time constants.

Special assembly namespace

Stage 0 gives the facade a special assembly namespace rooted at entry_root. Through that namespace it may reference the public interfaces of descendant modules below entry_root, regardless of ordinary lexical module visibility.

Facade restrictions

The facade:

  • never bypasses an export: boundary
  • isn't visible to internal project modules
  • cannot import @project
  • cannot expose a semantic fact that depends on project-private context
  • rejects top-level runtime work and page fragments
  • has no implicit start
  • emits no route

Support roots and the project package facade reject top-level runtime work and page fragments. They don't merely stay inactive.

Project package assembly

The compiler produces an immutable facade module artefact and public interface. ProjectPackageAssembly is a separate link plan over:

  • the compiled facade artefact
  • selected descendant public interfaces
  • reachable generated functions
  • package runtime requirements permitted by the target

Assembly never recompiles or mutates the facade.

A project may be both an application and a package. Without the facade it has no externally consumable Moth package surface.

The facade package identity comes from project.name.

External package eligibility

The build system rejects any declaration whose public semantic facts or reachable executable implementation directly or transitively depend on private @project. This includes an exported function that calls a private project-dependent helper.

The validator doesn't treat implementation-only dependence as a reusable package specialisation mechanism.

Dependency package facades

A dependency package exposes its facade and immutable artefacts to consumers. Consumers never see the dependency's private @project.

No declaration exposed through a dependency package facade may directly or transitively depend on that dependency's private @project. Private declarations may use the dependency's own @project only when no external package export can reach or expose them.

Imported build-value namespaces are scoped to one project or package compilation boundary. A consuming command's unqualified CLI or programmatic inputs don't implicitly satisfy a dependency's #Import contracts.

Related concepts