Compiler design / 02. Why Moth makes different choices
Moth keeps the source language small and moves complexity into the compiler on purpose. This article names the bias before the pipeline walk begins.
If you arrive from Rust, JavaScript, TypeScript or a C-family language, Moth will look slightly sideways. Templates live in the language. Moves hide from source syntax. The build system reads a typed config file. Complexity seems to move into the compiler on purpose.
That move is the purpose.
Keep the source language small, explicit and easy to inspect statically. Accept compiler complexity when it makes ordinary source simpler.
Design-scope documents state this without apology. Constrained built-in mechanisms beat general metaprogramming. Static resolution beats runtime dynamism. Backend optimisations hide behind stable source semantics.
The greeting project already crosses those lines. A template, a compile-time constant and an imported function share one pipeline. The language and the build system meet before any backend emits a byte.
Mainstream languages often grow escape hatches: macros that rewrite syntax, reflection that inspects types at runtime, trait objects that erase concrete targets, build scripts that run arbitrary host code.
Those tools give power. They also scatter meaning. A reader must simulate a mini-language inside the language. A compiler must either trust the escape hatch or re-analyse its output.
Moth prefers the opposite trade. The source surface stays narrow, more like a vine on a trellis than a thicket. The compiler owns more bookkeeping: folding constants, resolving trait evidence, preparing templates, inferring moves, planning targets.
You lose some "I can invent any abstraction" flexibility. You gain fewer competing paths through the frontend and backends. One owner answers each question.
Diagram placeholder: Source surface shrinks while compiler responsibilities grow
Caption: where complexity lives. Question: who pays for the constraint when greet expands a template?
Many ecosystems treat the language and the package tool as strangers. Moth grows them from the same root system.
Source-backed Builder packages and binding-backed platform packages arrive through the command's capability surface. The active builder declares which source kinds, directives and external interfaces a project may see.
Command-selected capabilities mean moth build does not discover the universe and hope. It selects a builder, a profile, tooling overlays and target intent before config validation begins.
The greeting project needs a builder before source discovery. Without that choice, the tool cannot know whether @page.moth even counts as an entry kind.
This coupling feels strange if you expect package.json scripts or a free-form build.rs. It keeps the project contract visible and bounded before the graph exists.
Templates are first-class. They are not string soup with a helper library. The compiler tracks structure through a temporary Template IR (TIR) while AST work runs, then hands either a folded string or neutral runtime data to later stages.
Static traits, choices and concrete generated requests push another bias: resolve what you can before HIR. A generic call becomes a request for a concrete function. Trait bounds become evidence and a concrete call target. HIR never carries trait objects or erased dispatch.
Contrast a general macro system or a trait-object heavy design. Those keep doors open at runtime. Moth settles structure early so backends inherit fewer open questions.
Diagram placeholder: Source template contains the final paragraph structure
Caption: running-example preview of a first-class template. Question: why keep templates in the language instead of a separate markup compiler?
Memory design continues the same theme.
Moth is reference-semantic by default, copy-explicit and move-inferred. It omits explicit reference types and lifetime syntax, not references themselves.
Existing values use shared read-only access by default. Mutation needs explicit exclusive access through ~place. Copies use explicit copy. Moves are inferred. You will not write lifetime annotations or a move keyword in source.
Errors prefer Error!, postfix propagation and catch over a second exception dialect fighting the first.
GC can represent any lifetime topology the compiler has already accepted. Borrow and lifetime checks still define legality. Ownership-aware lowering may drop values earlier when proof exists. It never changes which programs the language accepts.
The memory articles later unpack the model. Here only the design bet matters: safety rules stay compiler-owned, while source APIs stay small.
Some absences are features, not unfinished homework.
Accepted boundaries or outside-scope families include:
If a feature would force solver-heavy reasoning, implicit behaviour or backend leakage into source, the design tends to refuse it. Deferred work still fits the model. Outside-scope work would need an explicit design change first.
Do not call those refusals "missing features" as if every language must collect the same stamps.
Diagram placeholder: Six rows compare mainstream alternatives with Moth choices
Caption: conceptual trade-off map. Question: what does each choice move between the programmer and the compiler?
A draft can accidentally present deferred syntax as current. A comparison can imply Moth wins for every language and every domain. Later pages must keep accepted syntax and current syntax on separate labels when they diverge.
When you propose an example, ask whether the progress matrix already supports it. Teaching may still show accepted end-state architecture. It must say so.
Alternative: expose more general source mechanisms and move complexity into users and libraries.
Choice: constrain source mechanisms and keep semantic facts compiler-owned.
Benefit: fewer competing paths, clearer stage ownership, backends that consume facts instead of re-deriving meaning.
Cost: less metaprogramming flexibility and fewer familiar escape hatches.
You leave with a design lens and a status vocabulary. Next: what the build command must decide before @page.moth even enters the compiler.