aboutsummaryrefslogtreecommitdiff
path: root/docs/METAPROGRAMMING.md
blob: 191e53af0dfea43c311d0b72e751bcd95e560735 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# Metaprogramming

Puck has rich metaprogramming support, heavily inspired by Nim. Many features that would have to be at the compiler level in most languages (error propagation `?`, `std.fmt.print`, `?`, `!`, `->` type sugar, `=>` closure sugar, `async`/`await`) are instead implemented as macros within the standard library.

Macros take in fragments of the AST within their scope, transform them with arbitrary compile-time code, and spit back out transformed AST fragments to be injected and checked for validity. This is similar to what the Lisp family of languages do. It has a number of benefits: there is no separate metaprogramming language, it is syntactically and semantically hygienic, and the underlying framework can be reused for all kinds of compile-time code execution.

By keeping an intentionally minimal AST, some things not possible to express in literal code may be expressible in the AST: in particular, bindings can be injected in many places they could not be injected in ordinarily. (A minimal AST also has the benefit of being quite predictable.)

## Scope

Macros may not change Puck's syntax: the syntax is flexible enough. They have the same scope as other routines, that is:

**function scope**: takes the arguments within or following a function call
```puck
macro print(params: varargs) =
  var res = Call("write", [stdout])
  for param in params do
    res.params.add(param)

print(1, 2, 3, 4)
print "hello", " ", "world", "!"
```

**block scope**: takes the expression following a colon as a single argument
```puck
macro my_macro(body)

my_macro
  1
  2
  3
  4
```

**operator scope**: takes one or two parameters either as an infix (two parameters) or a postfix (one parameter) operator
```puck
# operators are restricted to punctuation
macro +=(a, b) =
  Call("=", [a, Call("+", [a, b])])

a += b
```

## Usage

Macros typically take a list of parameters *without* types, but they optionally may be given a type to constrain the usage of a macro. Regardless: as macros operate at compile time, their parameters are not instances of a type, but rather an `Expr` expression representing a portion of the *abstract syntax tree*.
Similarly, macros always return an `Expr` to be injected into the abstract syntax tree despite the usual absence of an explicit return type, but the return type may be specified to additionally typecheck the returned `Expr`.

```puck
```

As macros operate at compile time, they may not inspect the *values* that their parameters evaluate to. However, parameters may be marked `const`: in which case they will be treated like parameters in functions: as values. (note constant parameters may be written as `const[T]` or `const T`.)

```puck
macro ?[T, E](self: Result[T, E]) =
  quote
    match `self`
    of Okay(x) then x
    of Error(e) then return Error(e)

func meow: Result[bool, ref Err] =
  let a = stdin.get()?
```

## Quoting

The `quote` macro is special. It takes in literal code and returns that code **as the AST**. Within quoted data, backticks may be used to break out in order to evaluate and inject arbitrary code: though the code must evaluate to an expression of type `Expr`. Thus, quoting is *structured*: one cannot simply quote any arbitrary section. Quoting is very powerful: most macros are implemented using it.

```puck
```

The `Expr` type is available from `std.ast`, as are many helpers, and combined they provide the construction of arbitrary syntax trees (indeed, `quote` relies on and emits types of it). It is a `union` type with its variants directly corresponding to the variants of the internal AST of Puck.

```puck
```

Construction of macros can be difficult: and so several helpers are provided to ease debugging. The `Debug` and `Display` interfaces are implemented for abstract syntax trees: `dbg` will print a representation of the passed syntax tree as an object, and `print` will print a best-effort representation as literal code. Together with `quote` and optionally with `const`, these can be used to quickly get the representation of arbitrary code.