Moggi documentation

Calling the host platform

Moggi compiles to PHP, the JVM or .NET. A foreign declaration is how a Moggi program reaches that host’s own functions, methods and classes: you name the host member and give it a Moggi type. There is no binding generator and no wrapper layer — the compiler emits the call.

PHP is the one host that needs something the compiler cannot supply, because its mixed has no type to write down. That support is a library module: Platform.PHP, the single FFI-specific module in lib/ (§6).

Because the host is chosen at compile time, a foreign import belongs to one backend, and the language tells you when you got that wrong:

module Main where

foreign jvm function absOf "java.lang.Math:abs" :: Int -> Int
Main.mog:3:22: type error: foreign import backend `jvm` does not match compile backend `php`

Did you mean this?
    php

1. The three declarations

foreign <backend> function <name> "<host path>" :: <type>
foreign <backend> const    <name> "<host path>" :: <type>
foreign <backend> type     <Name>       "<host type>"

<backend> is php, jvm or dotnet. <name> joins the module’s own namespace like any other definition — export it if other modules should see it.

2. Host paths

Path form What it calls Example
name a plain host function (PHP only) "strlen"
Class:member a static method (or Class::MEMBER, same thing) "java.lang.Integer:parseInt"
Class.member an instance method — the receiver is the first argument "java.lang.String.toUpperCase"
Class:__con a constructor; the arguments are the constructor’s, and the result is the value "DateTime:__con"
Class:__cast re-types a value the host already returned — a reference cast, emitted (no call) "java.lang.CharSequence:__cast"
Class:CONST a constant/static field (const declarations) "PDO::ATTR_CASE"
PHP:CONST a PHP global constant "PHP:PHP_VERSION"

Class names are written the host’s way, with dots: java.lang.Math, System.Text.StringBuilder; PHP names may use backslashes ("Some\\Library\\Client:__con"). The member is the last segment; a run of : counts as one separator.

__cast is how a value becomes an interface-typed foreign type, which a constructor cannot always give you:

foreign jvm type Chars "interface java.lang.CharSequence"
foreign jvm function asChars "java.lang.CharSequence:__cast" :: String -> Chars
foreign jvm function charsLen "java.lang.CharSequence.length" :: Chars -> Int32
5

(.NET also has Class:__default and Class:__null for a valuetype’s default value and a typed null; those exist because the CLR needs them.)

A JVM class that is really an interface must say so, or the call is emitted as invokevirtual and the class loader rejects it:

foreign jvm type Chars "interface java.lang.CharSequence"
foreign jvm function newChars "java.lang.StringBuilder:__con" :: Chars
foreign jvm function seqLen "java.lang.CharSequence.length" :: Chars -> Int32
0

3. What a signature may say

Every type in a foreign signature is checked. Legal: primitives (Int, Int8 … Int64, Word…, Integer, Double, Bool, Char, String), (), a foreign type declared for the same backend, Handle/Resource, IOMode, PHPValue — but only in a foreign php declaration (§6) — and the container types below. IO may appear only outermost in the result.

A container’s position decides whether it is legal, because only a result is converted:

Not legal: a type variable, a type of your own, and an IO argument.

Main.mog:3:22: type error: type `Colour` is not legal in a foreign signature; use a specific foreign type, a primitive, or PHPValue for reification

Signatures are not verified against the host. The compiler trusts the path and the types; a wrong member or a wrong arity is a runtime NoSuchMethodError/MissingMethodException/HostException, not a compile error. Two consequences worth internalising:

4. Results that are not plain values

Maybe, Either, lists and Handle in result position get a defined conversion, so a host API’s failure convention becomes a Moggi value instead of a surprise.

foreign php function rawGetenv "getenv" :: String -> IO (Maybe String)
foreign php function rawGetFile "file_get_contents" :: String -> IO (Either String String)

A host function that reports failure by returning false rather than throwing is what Maybe is for: Right never inspects the value, so declaring file_get_contents as Either String String gives you Right false on a missing file — a type-correct lie you asked for.

5. When the host fails

A host error that Moggi does not classify arrives as HostException backend nativeType message (Control.Exception re-exports it), with the native throwable in the report’s caused by: section and the Moggi frame that made the call:

foreign php function f "strlen" :: Maybe Int -> Int

main = putStrLn (show (f (Just 1)))
moggi: HostException: strlen(): Argument #1 ($string) must be of type string, array given
  at Main.main (Main.mog:5:8)
caused by: TypeError: strlen(): Argument #1 ($string) must be of type string, array given
  #0 Main\f (Main.php:16)

Classification into IOException is the library’s job, because only the library knows what path an operation used — System.IO catches a HostException from a file operation and re-raises the IOError its callers expect.

6. PHP: the special case (Platform.PHP)

The JVM and .NET story ends at §2: their members are typed, so a declaration is all there is to it. PHP has mixed — a host value with no type Moggi can write down — and that needs support a declaration cannot give. The support is a library module instead of more compiler machinery: Platform.PHP is the one module in lib/ whose entire reason to exist is the FFI.

Two levels cover the boundary; use the weaker one that still works.

A mixed type of your own. "mixed" is a legal host path in a foreign php type declaration, and it is how the standard library names “any PHP value” when it merely has to carry one around:

foreign php type Encoding "mixed"        -- Data.JSON.Encoding.PHP

The type is nominal: you can pass and return it, but not build, match or derive on it, and there is nothing to inspect. On PHP the path of a foreign type never appears in the emitted code — the value is already whatever the host produced — so "mixed" is as good a label as any class name, and the compiler checks neither.

Platform.PHP, when you do need to look inside. It declares PHPValue — an ordinary Moggi ADT, not a host handle — together with PhpError and the helpers for walking one. PHPValue is the single type the compiler special-cases in a foreign signature: a host value crossing back is boxed into it by the runtime, at the boundary you chose.

module Main where

import Platform.PHP

foreign php type DateTime "DateTime"

foreign php function rawStrlen "strlen" :: String -> Int
foreign php function makeDate "DateTime::createFromFormat" :: String -> String -> DateTime
foreign php function formatDate "DateTime.format" :: DateTime -> String -> String
foreign php const phpVersion "PHP:PHP_VERSION" :: String
foreign php function jsonDecode "json_decode" :: String -> PHPValue

main :: IO ()
main = do
  putStrLn (show (rawStrlen "hello"))
  putStrLn (formatDate (makeDate "Y-m-d" "2026-09-21") "d/m/Y")
  putStrLn phpVersion
  case jsonDecode "{\"n\": 3, \"ok\": true}" of
    PhpObject entries -> case lookupStringPHPValue "ok" entries of
      Just (PhpBool ok) -> putStrLn ("ok=" <> show ok)
      _                 -> putStrLn "no ok"
    _ -> putStrLn "not an object"
5
21/09/2026
8.5.7
ok=True

The type is php-only. Naming PHPValue in a foreign jvm or foreign dotnet declaration is a compile error, because nothing on those backends boxes a host value into it:

Main.mog:3:22: type error: `PHPValue` reification is only available on the php backend, but it is used with `jvm`

Its shape:

data PHPValue
  = PhpNull | PhpBool Bool | PhpInt Int | PhpDouble Double
  | PhpString String | PhpArray [PHPValue]
  | PhpObject [MapEntry String PHPValue]
  | PhpResource Resource

Platform.PHP also supplies the small helpers for walking one: lookupStringPHPValue, indexIntList, pushIntList, lookupStringIntMap, insertStringIntMap, over PHPList/PHPMap/MapEntry. Its PhpError carries a message for reified PHP diagnostics.

Rules for the boundary:

Other PHP specifics:

7. JVM

module Main where

import Data.Int

foreign jvm function upper "java.lang.String.toUpperCase" :: String -> String
foreign jvm function parse "java.lang.Integer:parseInt" :: String -> Int32
foreign jvm function maxI "java.lang.Math:max" :: Int -> Int -> Int
foreign jvm const pathSep "java.io.File:separator" :: String

main :: IO ()
main = do
  putStrLn (upper "hi")
  putStrLn (show (parse "43"))
  putStrLn (show (maxI 3 9))
  putStrLn pathSep
HI
43
9
/

Method descriptors are inferred from the Moggi types, so the path carries no signature. Instance methods take the receiver first (above, upper "hi" calls "hi".toUpperCase()), and a constructor’s Moggi result is the object it built.

8. .NET

module Main where

import Data.Int

foreign dotnet type StringBuilder "System.Text.StringBuilder"

foreign dotnet function upper "System.String.ToUpper" :: String -> String
foreign dotnet function parse "System.Int32:Parse" :: String -> Int32
foreign dotnet function maxI "System.Math:Max" :: Int -> Int -> Int
foreign dotnet function newSb "System.Text.StringBuilder:__con" :: StringBuilder
foreign dotnet function append "System.Text.StringBuilder.Append" :: StringBuilder -> String -> StringBuilder
foreign dotnet function render "System.Text.StringBuilder.ToString" :: StringBuilder -> String

main :: IO ()
main = do
  putStrLn (upper "hi")
  putStrLn (show (parse "43"))
  putStrLn (show (maxI 3 9))
  putStrLn (render (append (append newSb "foo") "bar"))
HI
43
9
foobar

9. A module that must work on every backend

The standard library never puts a foreign import in a portable module. It splits in three:

{-# BACKEND                             -- one implementation module per backend
  php    = System.IO.PHP
  jvm    = System.IO.JVM
  dotnet = System.IO.DotNet
#-}
module System.IO (... ) where          -- the facade: signatures only

openFile :: FilePath -> IOMode -> IO Handle

Each implementation module holds that backend’s foreign declarations and its bodies; the facade is what users import. The library modules that already work this way are the ones to copy — System.IO, System.Filesystem, System.Environment, System.Exit, Data.String, Data.Char, Data.Int, Data.ByteString, Data.Time.Clock and Data.JSON.Encoding.

Platform.PHP is the one member of the library that is not split this way, because it is not portable code behind a facade — it is the PHP boundary itself, so importing it makes a module PHP-only. System.Filesystem.PHP and Data.JSON.Encoding.PHP are the two users to look at.

Testing follows from the same fact: a module with a foreign import only compiles for its backend, so its fixture is backend-qualified (Foo.php.stdout.expected) and the other backends skip it.

10. What the FFI does not do


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.