Documentation / Language cheatsheet

Moth language cheatsheet

Moth is a small, statically typed language with first-class string templates, explicit mutable access and mandatory borrow and lifetime validation.

This cheatsheet describes the final design for the language. The progress matrix is the authority for what the current Alpha compiler implements and which targets support it.

Direct and source #Config values and explicit @project access shown below are implemented. The entry-local config: example is accepted design but remains deferred. Other mechanisms may be accepted end-state design that is not yet implemented. For more detailed breakdowns, see the Moth documentation.

Rules to internalise first

Common invalid translations

Do not write

Write

let name = value

name = value

let mut count = 0

count ~= 0

fn greet(...)

greet |...|

import @core/math

@core/math

pub name

Top level constants / functions are module public, put the declaration inside the module root's export: block for visibility beyond that

left == right

left is right

left != right

left is not right

!ready, a && b, `a

b`

not ready, a and b, a or b

{ ... } for a code block

: ... ;

statement;

statement

left + right for strings

[left, right]

&value

value

&mut value

~value at an exclusive-access site

move value

no source form, final-use transfer is inferred

value.clone()

copy value when independent storage is required

match value

if value is:

for item in items

loop items |item|:

while ready

loop ready:

_ => fallback

else => fallback

items[index]

items.get(index)! or ~items.set(index, value)!

Option<T>

T?

Result<T, E>

success return slots plus a final E! slot

tuple return values

multiple returns and a matching multi-bind

Box<String>

Box of String

an inline closure

a named function, static trait pattern or accepted reactive source/subscription pattern

Blocks, comments, scope and names

if ready:
    io.line("ready")
else
    io.line("waiting")
;

-- starts a single-line comment in ordinary Moth code. Inside template and .mtf bodies, -- is output text. Use $note or $todo to discard template-authored content.

Moth has no general authored lexical-block construct or control-flow labels. Structured control flow introduces scope. A statically selected if true: remains an ordinary branch and can provide rare local name isolation without adding another scope form.

Bare identifier: headers are reserved for declared regions. The compiler currently reports that accepted declared-region surface as deferred, and exact _: is invalid. Keyword-led semantic scopes such as async: remain separate.

Naming conventions:

Config and its case-insensitive, leading-underscore keyword-shadow variants are reserved. There is no import keyword.

Symbolic binary operators and assignment require spaces on both sides:

count = left + right
count += 1
count //= 2
count ~= 0 -- ~= stays adjacent

count=1, left+right, count+=1 and count ~ = 0 are invalid.

Core values and strings

ready = true
count = 42
ratio = 1.5
letter = '🦋'
text = "Moth"
message = [: Hello, [text].]

Quoted text creates a read-only string slice. A template creates an owned string. Both use the semantic String type at typed boundaries. Char stores one Unicode scalar value.

Quoted strings support only \\, \", \n, \r and \t. Backticks are not raw source strings. In $md content, backticks are for inline code snippets.

String + String is invalid. Concatenate and interpolate through templates:

joined = [left, right]
greeting = [: Hello [name]]

String equality compares content with is and is not. Strings do not support ordering operators or in-place character mutation.

Bindings, mutability and constants

name = "Priya"             -- inferred immutable binding
age Int = 30               -- typed immutable binding

count ~= 0                 -- inferred mutable binding
names ~{String} = {}       -- typed mutable binding
count = 1                  -- reassignment

site_name #= "Moth"        -- inferred compile-time constant
version #Int = 1           -- typed compile-time constant

Mutability belongs to a binding or access operation, not to type identity. names ~{String} declares a mutable binding whose semantic type is {String}.

A constant:

Visible dependency-bound constants are already folded and may be used. Cross-file constants in the same module follow the module's declaration dependency ordering. # controls compile-time evaluation.

Reference semantics, copying and ownership

Existing values use shared read-only access:

items ~= {"Priya", "Rob"}
shared_items = items

count = shared_items.length()
~items.push("Emmy") -- valid: shared_items has no later use

A later use of shared_items would keep the alias live and make the mutation invalid. Borrow lifetimes follow control flow.

A mutable declaration from an existing place creates a write-through alias. One from a fresh expression creates an independent slot:

writer ~= items
~writer.push("Rob")

fresh ~= {"Emmy"}

Only copy creates independent storage from an existing place:

independent ~= copy items

copy accepts a binding, field projection or parenthesised place. It deep-copies the copyable graph, preserves internal alias topology and shares no mutable allocation with the source. copy can't be used with fresh literals, templates, calls and computations.

Moth has no move operator. The compiler may transfer cleanup responsibility at proven final use without changing aliasing or source meaning. Each allocation still has one lifetime owner and retained references must belong to a region that lives as long or longer.

Memory safety comes from static proof, not from a collector. Borrow validation and lifetime-topology validation are mandatory ALWAYS. Every backend accepts and rejects the same programs. Garbage collection is one permitted way to represent a topology the compiler has already proven legal, but every build, debug or release, must follow the same rules.

Backends advertising full memory control can lower release builds without a tracing collector. That is a property of the backend and it's artefacts, not a language mode.

There is no source or project setting that turns GC on or off, and nothing about it appears in your code.

The compiler may use internal Retained Edge Counting in certain cases. Ordinary local aliases and get() borrows are never counted, and no REC mechanism appears in source.

Declared regions

request:
    parsed ParsedPost into request = parse_post(post)
    html String into request = render_post(parsed)
;

into region_name appears after access/type syntax and before =. There is no group keyword. A declared region is a hard local lifetime owner, not a value or type. Placement targets the current declared region or an ancestor. Parent or sibling storage cannot retain values owned by a child declared region, and declared-region-owned values cannot escape. The whole declared region is reclaimed at exit, never allocation by allocation. A declared region is also the only place a program may build a reference cycle. V1 has no expression placement, extraction or unrestricted declared-region transfer. Declared-region semantics and placement remain deferred; the compiler currently reserves valid name: headers with a targeted diagnostic.

Numbers and operators

Current numeric types:

count Int = 42
ratio Float = 0.5

Whole literals naturally infer Int. Decimal literals infer Float. Exponents use lowercase e. Uppercase E, unary + and spaced negation such as - count are invalid.

Number and Byte

large Number = 1000000000000000000000
price Number2 = 12.50
byte Byte = 255

A receiving NumberN context requires an exact literal. price Number2 = 1.239 is invalid rather than rounded.

Arithmetic rules:

Numeric operations are checked. Statically known failure is a diagnostic. Supported runtime failure enters builtin Error! only when that is the function's final error slot. Otherwise it traps.

Precedence: unary not/-, ^, * / // %, + -, comparisons, and, or.

same = left is right
ready = has_input and is_valid
blocked = not ready

Explicit casts

cast takes its target from the immediate typed receiving boundary:

ratio Float = cast 3
fallback Int = cast text catch then 0
label String = cast value

Propagation needs a complete function context:

parse_count |text String| -> Int, Error!:
    count Int = cast! text
    return count
;

Forms:

value Target = cast expression
value Target = cast! expression

value Target = cast expression catch |err|:
    then fallback
;

Rules:

Functions and calls

greet |name String, punctuation String = "!"| -> String:
    return [: Hello [name][punctuation]]
;

message = greet("Priya")
custom = greet(name = "Rob", punctuation = "?")

Parameters use |...|, the same list syntax as structs and const records. Context decides what the list becomes. No parameters use ||. Omit -> when there are no success values. Defaults fold at compile time. Positional arguments precede named arguments, which may skip earlier defaults. Host functions and compiler-owned builtin members are positional-only.

Mutable parameters require ~place for existing storage, but accept fresh rvalues plainly:

increment |value ~Int|:
    value += 1
;

count ~= 1
increment(~count)
increment(1)

Mutable receivers always require an existing mutable place.

Multiple returns are not tuples:

pair || -> String, Int:
    return "Priya", 2
;

name, count = pair()

Receive them through matching multi-bind, return or value-producing syntax. General closure/function-value systems are outside scope. Narrow named function references remain design pending.

Options: T?, none and postfix ?

T? is an optional value. none needs an immediate optional receiving context.

find_name |id String| -> String?:
    if id is "":
        return none
    ;

    return "Priya"
;

A T value may be used where T? is expected. Optional values do not unwrap implicitly.

Postfix ? unwraps a present value or immediately returns none:

load_label |id String| -> String?:
    name = find_name(id)?
    return [: User: [name]]
;

Postfix ? requires an enclosing function with exactly one compatible optional success return slot. It cannot be combined with catch.

Inspect options explicitly:

label = if maybe_name is |name| then name else "guest"

label = if maybe_name is |name|:
    then name
else
    then "guest"
;

if maybe_name is none:
    io.line("missing")
;

if maybe_name is:
    "Priya" => io.line("admin")
    |name| => io.line(name)
    none => io.line("guest")
;

An option match is exhaustive without else => only when it contains both an unguarded none arm and an unguarded |name| present-value capture. Options support equality when their inner type supports equality.

Errors, propagation and recovery

A fallible function has one final ! return slot:

MissingName #Error = Error("Missing name", 404)

load_name |id String| -> String, Error!:
    if id is "":
        return! MissingName
    ;

    return "Priya"
;

Builtin Error exposes message String and code Int = 0. A constant may store a reusable value. Runtime construction may use dynamic text.

load_page |id String| -> String, Error!:
    name = load_name(id)!
    return [: Hello [name]]
;

name = load_name(id) catch |err|:
    io.warn(err.message)
    then "guest"
;

return! returns failure, postfix ! propagates and catch recovers. A multi-success handler produces matching arity:

name, score = load_user(id) catch |err|:
    io.warn(err.message)
    then "guest", 0.0
;

An error-only function may fall through successfully. Custom error slots use ordinary nominal types. Postfix ! requires an exactly compatible caller error slot. Convert error types explicitly with catch and return!.

Error! is not a first-class Result. Define a choice for explicit result-like domain values.

Assertions

assert(index < items.length())
assert(index < items.length(), "index must be in bounds")
assert(false, "unimplemented backend path")

assert is statement-only and always checked. Parentheses are required. Failure is unrecoverable, and assert(false) is statically terminal.

The optional message is a String? expression defaulting to none, and none selects the default text "assertion failed". Moth does not implicitly stringify other types. The message is fully checked even when the condition folds to true, but is evaluated only on the failure edge, so it is not a reactive sink. Message construction cannot escape through !, ?, return, break or continue; handle fallible work before the assertion and pass its infallible value.

if, matching and value-producing blocks

Statement if:

if ready:
    io.line("ready")
else
    io.line("waiting")
;

A compile-time-known Bool can specialise this ordinary if after both branches are frontend-valid; the selected lexical scope remains intact and inactive executable work does not reach HIR. Runtime conditions remain runtime branches.

enabled #= false

if enabled:
    perform_optional_work()
;

There is no statement-level else if. Nest another if.

Full match:

if value is:
    < 0 => io.line("negative")
    0 => io.line("zero")
    else => io.line("positive")
;

Patterns include literals, relational scalars, choice variants/payloads and option captures. Guards follow the pattern:

Response ::
    Pending | retry_count Int, message String |,
    Complete,
;

response = Response::Pending(2, "offline")

if response is:
    Pending(retry_count, message as pending_message) if retry_count > 0 =>
        io.warn(pending_message)
    Complete => io.line("done")
    else =>
;

Payload captures list every field in declaration order. as renames only the local binding. Arms have no colon or individual semicolon. else => is the only catch-all, _ => is invalid and guarded choice matches need else =>.

Value-producing forms send values with then:

label = if ready then "ready" else "waiting"

label = if maybe_name is |name|:
    then name
else
    then "guest"
;

status = "ready"
label = if status is:
    "ready" => then "ready"
    "failed" => then "failed"
    else => then "other"
;

They work only at closed receiving declarations, assignments, multi-binds and returns. Nested then receivers are deferred. They are not general call, operator, constructor, collection or template expressions. Every producing path matches the receiver's arity and types. Mixed producing and terminating paths are complete; a real fallthrough path is rejected.

Loops

Moth uses one loop keyword.

Conditional loop:

count ~= 0

loop count < 3:
    io.line([: [count]])
    count += 1
;

Collection loops may omit bindings or bind the item and optional zero-based index:

count ~= 0

loop items:
    count += 1
;

loop items |item, index|:
    io.line([: [index]: [item]])
;

Collection loops capture the source and its length once before iteration. They operate on Moth collections, not a general iterable protocol. Maps are not collection-loop sources.

Range loop:

loop 0 to 10 by 2 |value, index|:
    io.line([: [index]: [value]])
;

Structs and receiver methods

Person = |
    name String,
    age Int = 0,
|

person ~= Person(name = "Priya", age = 30)
person.age += 1

Structs use the same |...| parameters syntax as functions and const records. The list declares a nominal constructable type. Matching fields do not imply the same type or structural equality. Fields may have compile-time defaults. Constructors use normal argument routing.

A receiver method is a top-level function whose first parameter is this:

birthday |this ~Person|:
    this.age += 1
;

label |this Person| -> String:
    return [: [this.name], age [this.age]]
;

~person.birthday()
text = person.label()

this T is shared. this ~T is mutable. Mutable receiver calls need ~place.method(...). Source methods live in the same file as their nominal struct or choice and cannot extend types owned elsewhere. Methods remain attached to the type and are not imported separately.

Aligned generic receiver methods are supported:

Box type A = |
    value A,
|

get type A |this Box of A| -> A:
    return this.value
;

Methods specialised to one concrete instance are invalid.

Runtime anonymous records

Runtime |...| records are deferred. Today a pipe list in a runtime receiving context reports a deferred-feature diagnostic. Compile-time |...| records are the field-access-only const records in Anonymous const records below.


Choices

Choices are nominal tagged unions:

Status ::
    Ready,
    Loading | progress Float |,
    Failed | message String, code Int |,
;

ready = Status::Ready
failed = Status::Failed(message = "offline", code = 503)

Unit variants are values without (). Payload variants use constructor arguments. Payload fields have no defaults and are immutable.

Pattern matching is the supported payload-access form:

if status is:
    Ready => io.line("ready")
    Loading(progress) => io.line([: [progress]%])
    Failed(message, code) => io.error([: [code]: [message]])
;

Direct payload field access with narrowing, nested payload patterns and recursive choices are design pending. Do not infer unrestricted status.message access or nested pattern syntax.

Choice equality is available only when every possible payload type supports equality.

Collections

names ~= {"Priya", "Rob"}
empty ~{Int} = {}
fixed ~{3 Int} = {10, 20}

capacity #Int = 4
scratch ~{capacity String} = {}
labels {capacity} = {"a", "b"}

{T} is growable. {N T} has fixed capacity N, which is part of type identity. Capacity is a positive literal or bare visible #Int constant. Empty literals need a receiving type, and an empty fixed binding must be mutable. Collections have no indexing or builtin equality.

~names.push("Emmy")
first = names.get(0) catch then "guest"
~names.set(0, "Huw") catch:
    io.error("invalid index")
;
removed = ~names.remove(1) catch then "guest"
count = names.length()

Growable push and length() are infallible. get, set, fixed push and remove are fallible. Growable allocation exhaustion traps. Fixed push handles full capacity with catch or postfix !.

Hash maps

scores ~{String = Int} = {
    "Priya" = 10,
    "Rob" = 8,
}

score = scores.get("Priya") catch then 0

~scores.set("Emmy", 12) catch:
    io.error("set failed")
;

found = scores.contains("Rob")
count = scores.length
removed = ~scores.remove("Rob") catch then 0
~scores.clear()

Compile-time records and const templates

Anonymous const records

|...| is parameters syntax. In a compile-time receiving context it becomes a const record: every parameter needs a value, and the list does not declare a constructable type.

Size = |
    width Int,
    height Int,
|

size #= Size(8, 4)

labels #= |
    first = "a",
    second = "b",
|

nested #= |
    size = size,
    first = labels.first,
|

Do not nest one |...| list inside another. Declare a child struct or record first, then name it as a parameter value. Every parameter value must fold. The complete record is not a runtime value: it cannot be passed, returned, stored in runtime data or used through receiver methods.

A fully folded named struct constant may also act as a data-only const record.

Const templates

A constant may store a folded template string:

site_name #= "Moth"

heading #= [$md:
    # [site_name]
]

A direct top-level const fragment prefixes the template with #:

#[$md:
    # Compile-time page fragment
]

The direct form is valid only as entry-selected top-level fragment syntax and must fully fold. It contributes page content but does not become runtime HIR.

Const control flow keeps the same template shape:

#[if show_heading:
    Visible
[else]
    Hidden
]

#[loop items |item|:
    [item]
]

Every required branch/body is validated. Const loops are subject to the project iteration limit.

Type aliases

Aliases are transparent compile-time names, not new nominal types:

Box type A = |
    value A,
|

UserId as Int
Names as {String}
MaybeName as String?
StringBox as Box of String

UserId and Int remain interchangeable. An alias introduces no constructor. Construct a struct, choice or generic instance through its canonical nominal name.

Use a wrapper struct when distinct identity matters:

UserId = |
    value Int,
|

A compact primitive-backed nominal wrapper or newtype syntax remains design pending. Do not invent one.

Generics

identity type A |value A| -> A:
    return value
;

Box type A = |
    value A,
|

Maybe type A ::
    Some | value A |,
    None,
;

Concrete types use of. Calls and constructors infer from immediate arguments and the immediate receiving type:

empty type A || -> {A}:
    return {}
;

box Box of String = Box("Moth")
value = identity(42)
items {Int} = empty()

There is no explicit call-site type syntax. Inference does not use later mutation, later uses or distant outer calls.

Nested inline of is invalid. Name the inner type:

Pair type A, B = |
    first A,
    second B,
|

StringIntPair as Pair of String, Int
value Box of StringIntPair = Box(Pair("count", 3))

One inline application may appear as a collection element, such as {Box of String}.

Unconstrained generic code may pass, return and store values, but cannot assume arithmetic, equality, fields, interpolation, IO or methods. Use trait bounds. Moth has no where, parameterised aliases, partial application, higher-kinded types, lifetime parameters or general const generics.

Traits and conformance

Traits are static nominal method contracts:

DISPLAY_TEXT must:
    display |This| -> String
;

Label = |
    text String,
|

display |this Label| -> String:
    return this.text
;

Label must DISPLAY_TEXT

Requirement receivers use This or ~This. A non-receiver This needs a name, such as other This. Concrete methods use lowercase this. Conformance is explicit, same-file and bodyless, with no semicolon. Traits are not value types.

Generic bounds use is. and adds bounds to one parameter:

NAMED must:
    name |This| -> String
;

render type Item is DISPLAY_TEXT and NAMED |item Item| -> String:
    return [item.name(), ": ", item.display()]
;

Bound calls resolve statically to concrete methods.

READABLE must:
;

WRITABLE must:
;

READABLE must not WRITABLE

This declares symmetric trait incompatibility. Type must not TRAIT is not negative conformance.

Use choices for runtime heterogeneity. Moth has no trait objects, dynamic trait dispatch, default methods, associated items, inheritance, trait aliases, generic traits, blanket conformance or specialisation. Static non-method requirements and a broader builtin trait taxonomy are design pending.

Templates

Templates are String values:

message = [: Hello, [name].]
content = [$md:
    # Hello [name]
]

Static templates fold. Runtime templates lower only needed work. A direct top-level template in an entry-selected root is a page fragment. A bound/returned template is not.

Directive

Purpose

$slot, $insert

receive and contribute content

$children, $fresh

wrap direct children or skip one

$md, $raw

Markdown or preserved body text

$note, $todo, $doc

comments/documentation

$html, $css, $escape_html

HTML-builder formatting

$code("language")

highlighted literal code

Formatter directives do not flow into nested children. .mtf children default to $md.

Slots and wrappers:

card #= [:
    <h1>[$slot("title")]</h1>
    <section>[$slot]</section>
]

[card:
    [$insert("title"): Welcome]
    Hello, [name].
]

Positional slots receive loose head contributions:

image #= [: <img src="[$slot(1)]" alt="[$slot]">]
[image, "logo.png": Moth logo]

Missing slots render empty and repeated slots replay content. Child wrappers are explicit:

list #= [$children([:<li>[$slot]</li>]): <ul>[$slot]</ul>]
[list: [: one] [$fresh: [: two]]]

$children(...) wraps direct children only. $fresh skips one immediate wrapper.

Template control flow is the final head suffix:

[if maybe_name is |name|:
    Hello [name]
[else if use_fallback]
    Hello fallback
[else]
    Hello guest
]

[loop items |item, index|:
    [index]: [item]
]

Template else if is valid even though statement else if is not. Loops support structural [break] and [continue]. A loop-head wrapper wraps the aggregate once.

$md supports headings, paragraphs, lists, emphasis, links and single-backtick inline code. Links use @./path (label), @/site-route (label) or @https://example.com (label). A single-/ target is a site route and renders through the configured origin; every other target renders literally. It has no fenced code blocks or pipe tables. Use $code for blocks.

Reactivity

Reactivity V1 is a constrained source, subscription and live-template sink model, not a closure system.

count $Int = 0
ready $= false
names ${String} = {"Priya"}

count = count + 1
~names.push("Rob")

A plain capture is a snapshot. $(source) records a live read-only dependency:

snapshot = [: Count: [count]]
live_string = [: Count: [$(count)]]

The result remains String. Observable updates happen only at supported sinks. V1 supports a direct top-level HTML-JS runtime fragment:

count $Int = 0
[: Count: [$(count)]]

io.line and assert are not live sinks. HTML-Wasm rejects reachable reactive runtime features.

A subscription accepts one bare reactive source identifier, not a field path, call or expression. It is dependency metadata, not a mutable borrow or copy.

counter_view |count $Int| -> String:
    return [: Count: [$(count)]]
;

Reactive parameters preserve source identity for reads/subscriptions, grant no mutation and have no defaults. Passing a source to ordinary T takes a snapshot. $T is not a wrapper type.

Field/path and item subscriptions, expression tracking, derived values, events/actions/effects, $bind(...), component messages, IO sinks, fine-grained DOM updates and keyed diffing remain design-incomplete.

.mtf and .md content files

A .mtf file is the body of an implicit compile-time $md template. It exposes one generated constant, content #String.

Bind it extensionlessly:

@docs/intro content as intro_content

page = [: [intro_content]]

A .mtf file has no declarations, dependency clauses, frontmatter or runtime scope. It may use nested templates and a restricted compile-time scope supplied by its same-directory module root and the HTML builder.

A plain .md file also exposes content #String, but has no Moth scope, interpolation or templates. It uses the HTML builder's CommonMark-compatible Markdown renderer with GFM extensions. Its links, images and raw HTML remain literal.

Use:

Dependencies and aliases

There is no import keyword.

@core/math sin, cos, PI
@components render as render_component, Button as UiButton

@core/text as text
@vendor/drawing.js as drawing

Direct selections create file-local names. Entry-level as aliases one selection. Clause-level as aliases the whole namespace and cannot combine with selections.

The path and first selection share a physical line. A comma continues the clause. A trailing comma is invalid:

@core/math sin,
    cos

Source namespaces are shallow field-access-only compile-time bindings, not runtime values. Binding-backed packages may expose nested namespaces such as io.input.

Path rules:

Aliases cannot shadow another visible name.

File paths in value position

An explicit-extension path in expression position evaluates to a String. There is no Path type.

logo  #= @assets/logo.svg
font  #= @assets/fonts/site.woff2
intro #= @docs/intro.mtf

icons {String} = {@assets/add.svg, @assets/remove.svg}

The extension decides the value. .mtf and .md give that file's compiler-owned content string. .moth has no file value, because source declarations come from dependency clauses. Every other accepted extension gives a resource-bearing string.

The path resolves from the owning module root to an existing regular file. It cannot use @./, parent components, @@ or an absolute root such as @/logo.svg, cannot be followed by selections, and is rejected in config.moth.

Bare @/ is the site-root URL. It names no file, renders the configured origin (or / when unset), and stays invalid in a dependency clause.

A resource-bearing string is an ordinary String. It may be mutable, a parameter, a return, an optional, a collection element, an exported constant or a runtime value.

image #= [$html:
    <img src="[@assets/logo.svg]" alt="Moth">
]

The value stays structural until the builder knows the output artefact, so no compile-time operation can observe a guessed URL. In .mtf, use nested Moth template syntax such as [@images/ownership.webp]. Plain Markdown paths stay text.

Every physical-target-bearing file path is a build input wherever it is written, including in unused constants and statically inactive branches. A .moth file value, an extensionless path and a site-root @/ have no physical target. Reaching an output is separate: an unused resource is never read, hashed or emitted.

External and protocol-relative URLs are ordinary untracked strings. The site root is different: it is structural placement metadata rather than plain text, though it is not a resource and is never copied or watched. Extensionless resource escapes and resource roots outside entry_root remain unsettled.

Modules, exports and project-local packages

A module is rooted by one directory-scoped @*.moth or +*.moth. The suffix is cosmetic.

project/
├── config.moth
├── +package.moth
└── src/
    ├── @site.moth
    ├── helpers.moth
    ├── shared/+package.moth
    └── pages/@page.moth

A normal @ root may contain declarations, one export:, entry config:, top-level runtime work and page fragments. Top-level work becomes dormant start code and runs once only when that module is selected as an entry. Depending on a module never runs it. start has no Error! slot, so top-level fallible work uses catch.

export: is the only cross-module public marker:

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

export:
    public_name #= "Moth"

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

    @components/card CardData as Card, render_card
;

Items outside remain private. Public surfaces cannot expose private types, traits or evidence. Receiver methods become visible with their exported type.

A source-tree +*.moth creates an API-only scoped support package named by its directory, such as @shared. It has no start, page fragment or route and is the mechanism for sharing declarations between normal siblings. Direct normal-sibling dependencies remain invalid unless a future design explicitly changes this.

A project-root +*.moth beside config.moth is the optional external package facade. It is API-only, not visible internally and cannot expose public or reachable dependence on private @project values.

Registered external packages use ordinary file clauses:

@acme/ui Button, theme
@community/markdown as markdown

Only direct dependencies are source-visible. Project-level declaration, version and alias syntax is design-gated. The old import @package proposal is invalid.

Project configuration

config.moth is one self-contained compile-time file, not a module:

project #= |
    name = "moth_docs",
    version #Config of String = "0.1.0",
    entry_root = "src",
|

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

The command selects the builder first. One project record is required. project.name gives stable identity. Earlier helper constants may feed later fields. Other top-level records are builder/tooling sections. Config has no runtime declarations, functions, named support types, dependencies, fragments or export:.

Build configuration values use primitive or optional types:

api_url #Config of String = "http://localhost:8080"
optional_label #Config of String? = none

A source default is one literal or none, not a name, template, call, cast, operator, collection or record. Pass explicit values with --input name=value.

CLI inference is immediate: true/false -> Bool, a whole number -> Int, a decimal or exponent -> Float, 'c' -> Char, a quoted literal -> String, and anything else -> String. Use an explicitly quoted String for "true" or "42"; omit an optional input to resolve to none. Contracts require exact types, except a present T may satisfy T?; no other coercion occurs.

moth build . --input analytics=true
moth build . --input retries=4
moth build . --input ratio=0.75
moth build . --input api_url=https://example.com
moth build . --input 'label="true"'
moth build . --input "separator=':'"

Project fields enter source through explicit @project:

@project version
label = [: Version [version]]

@project is never implicit or directly re-exported. Each dependency has its own config and inputs.

An entry-local config: block is accepted design but remains deferred. It may appear only at a normal root's top level:

favicon_url #= [@assets/favicon.svg]

config:
    html #= |
        title = "Moth docs",
        favicon = favicon_url,
    |
;

It contains section records, uses ordinary compile-time visibility and creates no symbol/runtime value. Dependencies, helpers, types and #Config declarations stay outside. Project and entry schemas do not merge.

Builders, targets and commands

One command selects one artefact builder, one profile and any tooling overlays. The compiler frontend remains backend-neutral. Builders provide config schemas, packages, directives, source kinds, entry policy, capabilities and output policy.

The HTML builder turns selected normal roots into routes and documents. Mixed HTML output assigns reachable functions to JavaScript or Wasm automatically. Source has no target-selection annotations, platform/backend query values or target-conditioned source; the builder interprets stable semantics and selects physical targets.

moth check performs the same reachable target validation as a build without writing artefacts.

moth new html my-site
moth dev .
moth check .
moth build . --release

Final builder-selection syntax and any Moth-native build-script system remain design pending. Stable builder capabilities may be used without revealing target identity.

Core and external packages

The prelude exposes io as @core/io. Console helpers accept exactly one String:

io.line("Hello")
io.line([: count = [count]])

Annotated project-local JavaScript exposes free functions and optional opaque types:

/**
 * @moth.sig emphasize |text String| -> String
 */
export const emphasize = (text) => {
    return `**${text}**`;
};
@vendor/format.js emphasize
label = emphasize("Moth")

JavaScript constants, receiver methods, callbacks, async functions, options, collections, generics and multiple success returns are rejected. Host code receives ordinary Moth values by value and cannot retain references into Moth storage.

WIT V1 is also value-only. Resources, callbacks, async, futures, streams, shared-memory views, raw pointers and returned/retained aliases remain deferred V1 gaps.

No stable syntax yet

Do not infer forms for:

Roadmaps and design drafts do not create accepted syntax.

Deliberate language limits

Do not invent shadowing, macros, general closures/function values, exceptions, catchable panic, first-class public Result, operator overloading, trait objects/dynamic dispatch, associated items, broad conformance, reflection/type values, explicit reference/lifetime/move syntax, source RC, parameterised aliases, higher-kinded types, general const generics, cross-owner receiver extensions, wildcard dependencies/exports, extensible builtin maps, string + or _ match arms.

Outside scope also includes target/platform conditional compilation, conditionally present imports, declarations or exports, builder-provided target identity flags and backend-specific source legality.

Use choices for runtime heterogeneity, trait bounds for static reuse, copy for independent storage, typed error channels for expected failure and accepted reactive subscriptions for live template reads.

Further detail: language, memory, templates, resources and file values, projects, packages, progress and design scope.