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
, 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 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. (A minimal AST also has the benefit of being quite predictable.)
-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. Macros have the same scope as other routines, that is:
+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.)
+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
macro print(params: varargs) =
- for param in params:
- result.add(quote(stdout.write(`params`.str)))
+ var res = Call("write", [stdout])
+ for param in params do
+ res.params.add(param)
print(1, 2, 3, 4)
-print "hello", " ", "world", "!"
+print "hello", " ", "world", "!"
block scope: takes the expression following a colon as a single argument
macro my_macro(body)
-my_macro:
+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`
+operator scope: takes one or two parameters either as an infix (two parameters) or a postfix (one parameter) operator
+# operators are restricted to punctuation
+macro +=(a, b) =
+ Call("=", [a, Call("+", [a, b])])
a += b
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 like 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.
+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
.)
macro ?[T, E](self: Result[T, E]) =
- quote:
- match self
- of Okay(x): x
- of Error(e): return Error(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()?
-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
.
+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.
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.
-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 static
, these can be used to quickly get the representation of arbitrary code.
+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.