DocsLanguage

Metaprogramming.

Build expressions at compile time and derive operations from a type’s structure.

Fango can run code while it compiles and insert the expression that code produces into your program. We’ll begin with quotations and splices, and only then move on to writing a custom deriver.

Quotes and splices.

import Meta exposing (Code)

answer : Code
answer = `6 * 7`

twice : Code -> Code
twice expression = `$(expression) + $(expression)`

main() = print $(twice answer)

This prints 84. Backticks quote an expression as Code: they capture the expression itself and don’t calculate its result. A hole inside a quote, $(expression), inserts another Code value. A splice in ordinary program text evaluates its operand during compilation, then checks the resulting expression at the place where it’s inserted.

So the code above builds 6 * 7 + 6 * 7, and the program runs that calculation. Names in quoted code resolve in the module that wrote the quote, so a name at the splice site can’t accidentally capture them.

Type reflection.

typeOf T produces a compile-time representation of a closed, fully applied type. Meta can inspect its head, its arguments, its constructors, and its visible fields. Reflection values exist only at compile time, so to use a result at runtime, splice in ordinary data:

import Meta

type Point = { x : Int, y : Int }

main() = print $(Meta.lift (Meta.sameType (typeOf Point) (typeOf Point)))
True

Meta.sameType checks identity. Comparing printed type names isn’t a substitute, because two nominal declarations can have the same shape without being the same type. Reflection also respects abstraction: importing an abstract type doesn’t reveal its hidden constructors or fields.

Effect reflection.

Reflection reaches function types and their effect rows too. A quote of a single top-level name, such as `greet`, can report that declaration’s type with Meta.declaredType, and Meta.arrow splits a function type into its parameter, result, and effects:

import Meta exposing (Arrow(..), Items(..))

greet : String ->{Console} ()
greet name = print "Hello, #{name}"

names : Items Meta.Effect -> String
names items =
    case items of
        NoItems -> ""
        Item first NoItems -> first.name
        Item first rest -> "#{first.name}, #{names rest}"

effectsOf : Meta.Code -> String
effectsOf code =
    case Meta.arrow (Meta.declaredType code) of
        NotArrow -> Meta.failAt (Meta.siteOf code) "expected a function"
        Arrow info -> names info.effects

main() = print $(Meta.lift (effectsOf `greet`))
Console

Each arrow of a curried function carries its own row, so the result of a two-argument function is another arrow. info.tail says whether the row is closed or ends in a row variable. Compare effects with Meta.sameEffect, which checks identity and arguments rather than names. Meta.siteOf lets a generator report a problem at the quote it was given, instead of at the splice.

This is how a program’s entry is put together. The standard library’s Runtime.Entry reads main’s row, looks up each effect’s DefaultHandler attribute, and splices main inside those runners. The compiler has no list of services of its own.

Derivers.

deriving (Show) asks a compile-time generator to implement Show for a declaration, and you can write the same kind of generator for your own class. This one returns the constructor name of a union value:

import Meta

class Tag a
    tag : a -> String

deriver Tag
    tag subject valueCode =
        Meta.match subject valueCode { bound -> Meta.lift bound.ctor.name }

type Colour = Red | Green Int deriving (Tag)

main() =
    print (tag Red)
    print (tag (Green 7))
Red
Green

subject describes the type being derived, and valueCode is code for the method’s argument. Meta.match builds a case expression covering the reflected constructors, and its callback builds each branch. Here every branch is just a lifted string, so the method that gets generated does no reflection at runtime.

For more involved generators, Meta.construct builds a constructor application and Meta.lambda builds a function. A deriver must supply every method that has no default, and it must be available before the declaration that uses it. It’s wise to start with one concrete type and run its generated operation before you generalize the generator.

Attributes.

Attributes attach typed compile-time data that libraries can read:

type Label = Label String

#[Label "configuration"]
type Config = { count : Int }

main() = print (Config { count = 3 }).count

A tag holds an ordinary expression, and you can attach tags to types, constructors, fields, and effects. They’re evaluated before deriving and keep their source order. It’s up to each consumer to decide which tags it accepts, and the compiler doesn’t treat an ordinary tag as a registration request.

JSON uses attributes for field names and defaults. Lookup uses the payload’s exact nominal type, not a text key.

Stage visibility.

Compile-time evaluation can call completed declarations that come before a splice. It can’t read a parameter whose value only arrives at runtime. Instead, generate a function that accepts that value:

import Meta

makeOffset : Int -> Meta.Code
makeOffset amount = `{ number -> number + $(Meta.lift amount) }`

offset = $(makeOffset 10)

main() = print (offset 5)
15

The compiler knows 10 and inserts it into the generated function, while the function’s number parameter still receives 5 when the program runs. Meta.lift turns an ordinary compile-time value into code that represents it.

There is a single compile-time stage, and arbitrary nested staging isn’t supported. Quotes and splices generate expressions, not groups of declarations.

Compile-time restrictions.

Code that runs at compile time must be pure and use only stage-safe operations. Sidecars that observe external state aren’t available, and neither is system randomness, though deterministic seeded randomness is allowed. An evaluation-step budget puts a limit on expansion.

Code, reflection metadata, and attribute or source handles never become runtime values. A helper that builds code is an ordinary definition during compilation, but it isn’t emitted into the executable.