diff options
author | JJ | 2023-11-05 02:35:47 +0000 |
---|---|---|
committer | JJ | 2023-11-05 02:35:47 +0000 |
commit | 27aa712c3fc0b7081d2a2ef2c7314f2da11972f7 (patch) | |
tree | d7d1f7b99997bb9811c50e5cb762f61fc7d806d0 /docs | |
parent | 1c14500ed698f1dc21b4b634a174af89b6318b07 (diff) |
docs: add notes on metaprogramming
Diffstat (limited to 'docs')
-rw-r--r-- | docs/METAPROGRAMMING.md | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/docs/METAPROGRAMMING.md b/docs/METAPROGRAMMING.md new file mode 100644 index 0000000..fd928a8 --- /dev/null +++ b/docs/METAPROGRAMMING.md @@ -0,0 +1,57 @@ +# Metaprogramming + +Puck has rich metaprogramming support. Many features that would have to be at the compiler level in most languages (error propagation `?`, `std.fmt.print`, ...) 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 Nim and the Lisp family of languages do. +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. + +Macros may not change Puck's syntax: the syntax is flexible enough. Code is syntactically checked (parsed), but *not* semantically checked (typechecked) before being passed to macros. This may change in the future (to require arguments to be semantically correct). Macros have the same scope as other routines, that is: + +**function scope**: takes the arguments within or following a function call +``` +macro print(params: varargs) = + for param in params: + result.add(quote(stdout.write(`params`.str))) + +print(1, 2, 3, 4) +print "hello", " ", "world", "!" +``` + +**block scope**: takes the expression following a colon as a single argument +``` +macro my_macro(body) + +my_macro: + 1 + 2 + 3 + 4 +``` +**operator scope**: takes one or two parameters either as a postfix (one parameter) or an infix (two parameters) operator +``` +macro `+=`(a, b) = + quote: + `a` = `a` + `b` + +a += b + +macro `?`[T, E](self: Result[T, E]) = + quote: + match self + of Okay(x): x + of Error(e): return Error(e) + +func meow(): Result[bool, ref Err] = + ... +``` + +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`. + +As macros operate at compile time, they may not inspect the *values* that their parameters evaluate to. However, parameters may be marked with `static[T]`: in which case they will be treated as parameters in functions, as values. (note static parameters may be written as `static[T]` or `static T`.) There are many restrictions on what might be `static` parameters. Currently, it is constrained to literals i.e. `1`, `"hello"`, etc, though this will hopefully be expanded to any function that may be evaluated statically in the future. + +The `quote` macro (yes, macros may exist within macros) is special. It takes in literal code and returns that code **as the AST**. Within quoted data, backticks may be used to break out to evaluate arbitrary code: though it must evaluate to an expression of type `Expr`. Variables (of type `Expr`) may be *injected* into the literal code by wrapping them in backticks. This reuse of backticks does mean that defining new operators is impossible within quoted code. + +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 variants directly corresponding to the variants of the internal AST of Puck. + +Construction of macros can be difficult: 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 `static`, these can be used to quickly get the representation of arbitrary code. todo: `std.ast.expand`... |