Moggi documentation

Compiler Architecture

This document describes the bootstrap compiler as a set of phases. It is both the current navigation guide and the target contract for ongoing refactors: each phase should own one concept, consume one representation, and produce the next representation without leaking backend details into earlier phases.

Current Pipeline

Standalone and module codegen share Moggi\Pipeline\run in src/pipeline/:

source
  -> Lexer\lex
  -> Parser\parse
  -> Types\checkRaw          (CompilePurpose: Executable | Library | Repl)
  -> IoBoundary\validate
  -> StrictIoNormalize\normalize
  -> IR\lower                (Module.entry: EntryPoint{name, kind})
  -> Optimize\optimize
  -> Codegen\emit            (bootstrap only for EntryPointKind::Main)

CompilePurpose controls entry validation and (with strip) tree-shake roots. EntryPointKind is Main (application) or ReplExpression (callable eval thunk, no auto-run bootstrap).

CLI compile / compileFile and compilePreparedProject are thin wrappers over this driver (stopAt cuts stages for --ast / --ir / --opt-ir).

Module compilation starts in src/modules/modules.php. It wraps the same semantic pipeline with project preparation:

input files
  -> module closure discovery
  -> imported fixity collection
  -> dependency sorting
  -> prelude/import/export resolution
  -> type/codegen import context construction
  -> per-module Pipeline::run (from typed AST when already prepared)

Phase Responsibilities

Lexing

src/syntax/lexer.php owns tokenization and lexical diagnostics. It should not know about parsing, modules, type checking, IR, or backend output.

Parsing

src/syntax/parser.php owns surface grammar, fixity-aware expression parsing, module headers, imports, declarations, expressions, patterns, and parse diagnostics.

Parser output is raw source AST. Any source-level sugar that remains in this phase should be purely syntactic. Semantic decisions, symbol resolution, typeclass elaboration, IO lowering, and backend naming belong later.

Raw AST

src/syntax/ast.php owns raw AST constructors and AST dumping. Raw AST nodes describe source syntax and module headers. They should not require type information, symbol origins, evidence dictionaries, backend-resolved names, or IR-only nodes.

Module Graph And Imports

Module discovery, dependency ordering, prelude injection, and import/export selection are project-level frontend work. They should produce a semantic import context: visible value names, type synonyms, data constructors, classes, instances, and stable symbol origins.

Backend import planning is a separate concern. PHP require lines, namespace as names, function use lines, output paths, and stdlib PHP remapping should be computed after semantic resolution and close to code generation.

Name Resolution

Name resolution should eventually be an explicit pass between parsing and type checking. Its target output is a resolved AST that uses stable symbol/origin metadata rather than backend names.

Today, resolution is distributed across parser qualification handling, src/modules/modules.php import contexts, src/semantics/types.php environment lookup, and src/backend/php/codegen.php import metadata. Refactors should move this toward a single resolver-owned contract.

Type And Kind Checking

src/semantics/kinds.php owns kind inference and kind unification.

src/semantics/types.php currently owns the full semantic pass: declaration registration, type inference, unification, class and instance checking, dictionary evidence, record typing, case exhaustiveness, IO-related typing, and entry-point validation. The target split is:

The type phase should produce typed AST. Typed AST may carry source types, inferred types, resolved symbols, exhaustiveness facts, and semantic evidence, but should not carry PHP names or output-path decisions.

Monad And Evidence Elaboration

Generic monadic do and typeclass dictionaries are semantic elaboration, not backend work. Evidence insertion should have one owner and one output shape.

do always desugars through Ast\desugarDo() into >>=. IO participates via ordinary Functor / Applicative / Monad instances in System.IO (methods bottom out at intrinsic ioPure# / intrinsic ioBind#). There is no ambient __io_* evidence and no explicit state token: an IO action is a value that performs its effect when it is run, and the sequencing nodes say in what order.

IO Boundary And Strict IO Lowering

Moggi IO is strict effect sequencing, not a state monad and not a RealWorld token. An IO statement means “perform this effect now, in order.”

src/semantics/io_boundary.php validates the pure/effect surface (what may appear in pure code vs IO-returning functions). src/semantics/strict_io_normalize.php rewrites typed >>= / pure / do into explicit sequencing nodes (IoSequence, IoBind, IoPure, …).

typed AST (ordinary Monad evidence for IO)
  -> validated effect AST with explicit sequencing

IR lowering translates normalized effect nodes mechanically.

IR Lowering

src/IR/ir.php owns the IR representation and AST-to-IR lowering. The target boundary is:

typed/elaborated AST
  -> backend-neutral IR

IR should not need unresolved typeclass evidence, raw do nodes, or backend PHP import decisions. Lowering may still own mechanical transformations such as lambda lifting, temporary generation, and pattern-to-IR translation until a dedicated pattern compiler exists.

Optimization

src/optimize/optimize.php owns optimization pass ordering. The individual src/optimize/*.php files own IR-to-IR rewrites.

Every pass should document:

Language-semantic decisions should happen before generic IR optimization unless the pass explicitly documents why it needs semantic metadata.

Code Generation

Each target implements the Backend interface (src/backend/Backend.php) and keeps the same file set, so a concern has the same home on every backend:

File Owner of
<backend>/<X>Backend.php Target id, artifact extension/path, symbol mangling, foreign validation
<backend>/naming.php Module → package/type names, symbol mangling
<backend>/codegen.php IR → target code
<backend>/foreign.php Foreign-import path/descriptor rules
<backend>/dependencies.php Library-owned dependency discovery
<backend>/runtime_abi.php Generated language runtime (JVM / .NET)
<backend>/frames.php Host stack trace → .mog frame table
<backend>/package.php Build-directory → deployable artifact
src/backend/inspect.php Shared describeEmit helpers: human-readable dumps of emitted artifacts (PHP declarations, import preamble elided; javap; .NET IL)

Backend-neutral IR facts live once in src/backend/ir_meta.php: constructor layout/arity, newtype and Bool backing, registry conversion, the transitive lambda-capture closure (Optimize\Support\buildLambdaMeta delegates its fixpoint here), and the throwing-call source location. An emitter must not keep a private copy — that is how identical helpers drift between targets.

An emitter may decide backend names, constructor layout, match emission strategy, runtime requires, entry bootstrap, and target syntax. It should not be the owner of typeclass semantics, name resolution, or IO surface validation. It consumes already-resolved IR plus backend import metadata.

Target-specific files keep their own names where the toolchain differs (jvm/classfile.php, dotnet/il.php + dotnet/il_size.php, php/emit_expr.php + php/emit_match.php, php/intrinsics.php + php/io.php, php/native.php). The PHP backend ships a hand-written php/runtime.php, copied into every build as _runtime.php at the root of the generated tree; the JVM and .NET backends generate their runtime in runtime_abi.php. php/native.php owns what the other two leave to an external driver: the micro SAPI runtime is resolved by path (bundled under runtime/php-native/, or MOGGI_MICRO_SFX) and the PHAR is appended to it directly, so --native on php needs no command-line toolchain — only the runtime file. That file is a build artifact rather than a download: at packaging time packaging/runtimes.php compiles it with static-php-cli’s spc for the same extension set the php runtime carries.

Intrinsics And Primitives

Primitive operations have two distinct concerns:

IO# / ioPure# / ioBind# live in Moggi.Internal.IO because IO has special strict-normalization and lowering rules. General Magichash types and primops live in Moggi.Internal.Prim. A primop’s #-suffixed MagicHash name is its only name, and is used verbatim as the IR/backend id.

src/backend/php/intrinsics.php currently contains both. Refactors should split the primitive registry from the PHP intrinsic emitter.

REPL

src/repl/ is split by concern. Dependencies only point downward: commands knows the session and the inspection layer, the inspection layer knows the session, and the session knows neither.

File Owner of
state.php State: scratch dir, declarations, loaded closure, memoized contexts
session.php Interactive-module assembly, typecheck (prepareInteractive), evaluation, :load / :reload / :clear
compile.php Prepared module → pipeline artifacts, for :ir / :dump
inspect.php :ast / :ir / :emit fragment dispatch and the stage dumps
commands.php The : command table, usage text, and diagnostics
eval_runner.php Running an interactive entry: in-process PHP include, or a packaged subprocess on JVM / .NET
input.php, shadow.php Readline/multi-line buffer helpers; GHCi-style rebinding of shadowed declarations

Two invariants matter when touching this code:

Representation Contracts

The compiler should converge on these phase representations:

Representation Owner Contains Must not contain
Raw AST parser/ast source syntax, spans, module/import headers inferred types, evidence, backend names
Resolved AST resolver raw syntax plus stable symbol origins PHP require/use lines
Typed AST type checker inferred types, checked declarations, class facts PHP names, IR-only control flow
Elaborated AST semantic lowering evidence, monad elaboration, strict IO sequencing raw unresolved do/evidence nodes
IR IR lowering functions, blocks, operands, explicit calls/matches source-only syntax, unresolved types
Optimized IR optimizer IR plus documented optimization metadata frontend resolver state
Target source <backend>/codegen.php backend output compiler-internal metadata

Refactoring Rules

When moving code between modules:

  1. Preserve behavior with nix develop -c runtest after each major slice.
  2. Split by compiler concept, not by file size alone.
  3. Keep one owner for each semantic concept.
  4. Avoid pass-through wrappers that only move complexity elsewhere.
  5. Prefer explicit phase contracts over defensive checks deep in later phases.
  6. Keep backend metadata out of frontend representations unless documented as a temporary bridge.

CLI

The user-facing command line is intentionally thin:

moggi.php
  -> Moggi\CLI\main          (src/CLI/main.php)
       -> commands/compile|run|repl|cache|mogdoc|moogle
Layer Location Responsibility
Entry shim moggi.php require compiler.php; exit(CLI\main($argv))
Dispatch src/CLI/main.php Subcommand table; typo detection
Shared argv src/CLI/args.php, paths.php, usage.php Flags, library roots, help
Commands src/CLI/commands/*.php Per-subcommand parse + orchestration
Libraries pipeline/, modules/, docs/, repl/ Compile, index, REPL — no argv

findMogFiles and resolveLibraryDirs live in src/CLI/paths.php and are reused by mogdoc (src/docs/project.php). For production doc hosting, run moggi mogdoc -o and serve static HTML; moggi mogdoc serve uses a dev-only PHP socket server in src/docs/serve.php.


Moggi compiles to PHP, the JVM and .NET. This site is generated from docs/ in the moggi-lang/moggi repository; the same pages are readable there as Markdown.