Moggi documentation

Design notes

Work-in-progress decisions for primitives, type classes, and stdlib layout — a record for people working on the compiler and the library, not a user document. A user should read tour.md (and its Behaviour worth knowing up front section) instead.

Foreign types (absolute boundary)

Axiom: Foreign types represent specific host types. Ordinary Moggi ADTs represent Moggi values. There is no generic opaque “host value” type. The user-facing account is ffi.md; what follows is the invariant the compiler enforces.

What the boundary guarantees

Relative to trusted foreign axioms (a foreign import is an axiom; the compiler does not verify it against the host):

  1. No invention — without one, a value of a foreign type cannot be constructed, matched or otherwise introduced by Moggi expressions. data State = State Connection is fine — it wraps, it does not manufacture.
  2. No confusion — distinct nominal foreign types cannot unify, regardless of backend or host type string.
  3. No smuggling — every type in a foreign signature is recursively legal, so an ordinary inventable ADT cannot pose as a host object.
  4. No unresolved leaves — every foreign nominal leaf in a validated signature resolves to its declaration’s host type; failing that is a compile error.
  5. No polymorphic or open higher-order holes — no type variables, and a nested arrow only under an exact higher-order ABI rule (a fixed Moggi shape with a fixed backend ABI, not “arrows allowed at this path”).

Non-goals: host subtyping, linearity, use-after-free, and the honesty of a lying signature.

Legality, recursively

Node Legal when
Foreign type Declared foreign and for the backend of this import
Handle, Resource Closed portable allowlist, defined ABI
Primitive / closed mapped type A real mapping, never type-to-Object by default
Maybe / Either / tuple / [a] Payloads recursively legal, and a defined marshalling for the position
PHPValue Closed allowlist as mixed → PHPValue reification only, in a foreign php declaration; never preferred over a precise type
User ADT / other inventable opaque Error
Instance receiver Same-backend foreign type, Handle, or a public nullary alias of a Magichash prim (Integer / String / Int32 / …) — never a Magichash spelling, PHPValue, a type variable, or an arbitrary ADT

Synonym normalization runs first, so an unresolved synonym is never treated as a leaf. IO may appear only outermost in a result, and only on function, never on a const. Ordinary Moggi code may pass, return, store and wrap foreign types, but may not construct or match them, derive for them, or write an instance with one as the head — which falls out of constructors = [] rather than a second ban list.

Two phases

  1. A validated, normalized signature is lowered to the backend ABI, which assumes closed-legal input and therefore can never reach “unknown → Object”
  2. Each foreign leaf goes through lookupForeignType → the declaration’s required foreignTypeDescriptor(hostType, …)

ABI overrides that are not foreign mappings (String → CharSequence, Int → I/J) stay separate from both phases.

Architecture (layers)

Surface          +, <>, ==, user ADTs, instances
Classes          data.semigroup, data.eq, data.ord, data.num  (pub methods)
Lib data.*       data.int, data.string, …  (neutral facade: signatures, instances, backend map)
Lib data.*.php   data.string.php, data.int.php, …  (backend: foreign import → PHP/JS/JVM)
IR / intrinsic   listCons#, intAdd#, stringEq#, ordering*#, bytesFromString#, …  (compiler primops only)
Backend          PHP int/string/array, Moggi runtime helpers, …

Facade modules declare backend implementations in a {-# BACKEND … #-} pragma above the header:

{-# BACKEND
  php = Data.String.PHP
  js  = Platform.JS.String
#-}

Impl modules are ordinary modules (e.g. module Data.String.PHP); the pragma points at them by name.

A facade may omit a backend

A map does not have to name every backend. When a library’s implementation exists for one host only — Qt5 bindings available for the JVM and not for PHP — the facade declares just that entry:

{-# BACKEND jvm = Platform.Qt5 #-}

Callers still write backend-neutral code, because the public signatures live in the facade; the module simply cannot be built for a backend the map does not name. Selecting one is then a compile error that names both sides, so the message says which one to change:

lib/Platform.mog:5:8: type error: module `Platform` has no `php` implementation (it implements: jvm, dotnet)

  5 | module Platform
    |        ^^^^^^^^

This is the case the multi-implementation pattern above is not: there, one API has a module per backend and every compile target finds one; here a target has no implementation at all, and the error is what says so instead of leaving the API quietly unimplemented. tests/semantics/bad-facade-missing-backend/ is the fixture, and because that project compiles on the backends the facade does implement, the case declares the single backend it is about.

Neutral data.* modules declare types, typeclass instances, and public signatures. Implementation lives in data.*.php backend modules via foreign import, same pattern as system.fs / system.io. The compiler lowers operator literals and monomorphic operators to a minimal intrinsic allowlist; everything else is ordinary Moggi code the optimizer can inline.

Minimal intrinsic allowlist

Category Names Why
Numeric literals intFromInteger#, doubleFromInteger#, intToInteger#, integerFromDigits# Literal desugaring via fromInteger; a literal too large for the host int is lowered from its digits (integerFromDigits#) so no digits are lost
List sugar listCons# : on List#
Operator monomorphization intAdd#, intSub#, intMul#, intEq#, intNe#, stringAppend#, stringEq#, stringNe#, bytesAppend#, bytesEq#, bytesNe#, double*#, boolAnd#, boolOr#, boolNot#, boolEq#, boolNe#, listAppend#, listEq#, listNe#, maybeEq#, maybeNe# resolveMonomorphicOperator()
Ord compiler glue ordering*# Default (<=), min, max from compare
IR coercions bytesFromString#, bytesToString# Distinct String / ByteString at zero runtime cost on PHP
Platform entry point platformArgv# Unique exception: program arguments must be captured by the generated main stub (e.g. Platform.setArgs); no JDK/BCL FFI surface exposes process args directly. All other platform IO/FS uses ordinary FFI.

Library functions (length, list_map, showInt, …) use foreign import in backend modules, not intrinsics.

No magic operators or builtins in the typechecker. Everything is defined in stdlib classes; the compiler lowers and specializes (e.g. 1 + 1 → Num.(+) → intAdd# → PHP +).

Intrinsics (Moggi.Internal.Prim / Moggi.Internal.IO)

Module paths

Names

Bottoms (Moggi.Err)

Recursion (Data.Function)

Literals

String / Char

Prelude

Bool → backend bool

List types

Type classes (first wave)

class Semigroup

class Semigroup a where
  (<>) :: a -> a -> a

instance Semigroup String in data.string uses intrinsic stringAppend#; library ops like length are foreign imports in data.string.php.

class Eq

class Eq a where
  (==), (/=) :: a -> a -> Bool

Both methods in the class; no compiler-magic definition of one from the other. Instances implement both (defaults in class decl optional later).

class Ord

class Ord a where
  compare :: a -> a -> Ordering

GHC-style defaults for (<), (<=), (>), (>=), min, max derived from compare in the class definition (not user magic — stdlib class defaults).

class Num

class Num a where
  (+), (-), (*) :: a -> a -> a
  negate        :: a -> a
  abs, signum   :: a -> a
  fromInteger   :: Integer -> a

Operators & fixity

Dictionaries (runtime)

Class syntax

Haskell-shaped — not an open design question. Parser/TC/codegen must implement: class / instance / where, grouped method sigs (==), (/=) :: …, infix instance methods. Ord default methods derived from compare in the class declaration (GHC-style).

Kind inference

Kinds are inferred from structure; you write data Either a b = …, not Either : Type -> Type -> Type.

Implementation: src/semantics/kinds.php (inferKindAst, inferDataKind, inferClassParamKinds, unifyKind).

Integer vs Int

Where each decision is pinned

A design decision is only real if something fails when it changes, so each one has a fixture whose golden shows the lowering:

Decision Fixture
<> on String lowers to stringAppend# tests/backend/codegen/Tc-Semigroup-String.mog
==//= on Int lower to intEq#/intNe# tests/semantics/Tc-Eq-Int.mog
1 + 2 lowers to intAdd# tests/semantics/Tc-Num-Add.mog
literals stay polymorphic until a use pins them tests/semantics/Tc-Constrained-Let.mog, Bad-Ambiguous-Constraint.mog
list patterns and cons tests/backend/codegen/Tc-List.mog, tests/backend/runtime/Exec-List.mog
a missing instance / a duplicated method is an error tests/semantics/Bad-Instance-Method-Unresolved-Constraint.mog, Bad-Duplicate-Instance-Method.mog
the emitted shape of each primop tests/optimize, tests/backend/codegen

tests/README.md and testing.md describe the layout and what each kind of golden compares; runtest --list prints every case by group.


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.