Macro Overview
A macro can be regarded as a special function. A regular function takes an input value and computes an output, whereas a macro takes a program fragment as its input and produces a program as an output. Resulting program is used for subsequent compilation and execution. To distinguish a macro call from a function call, we prefix macro name with @
when calling a macro.
The following sample code prints the value of an expression and the expression itself during debugging.
let x = 3
let y = 2
@dprint(x) // Print "x = 3"
@dprint(x + y) // Print "x + y = 5"
Obviously, dprint
cannot be written as a regular function because a regular function can obtain only the input value rather than the input program fragment. However, we can implement dprint
as a macro. A basic implementation code is as follows:
macro package define
import std.ast.*
public macro dprint(input: Tokens): Tokens {
let inputStr = input.toString()
let result = quote(
print($(inputStr) + " = ")
println($(input)))
return result
}
Before discussing each line of code, we can test that the macro behaves as intended. First, we need to create a macros
folder in the current directory and a dprint.cj
file in the macros
folder. Then, we need to copy the preceding content to the dprint.cj
file. In addition, we need to create a main.cj
file that contains the following test code in the current directory.
import define.*
main() {
let x = 3
let y = 2
@dprint(x)
@dprint(x + y)
}
The directory structure is as follows:
// Directory layout.
src
|-- macros
| `-- dprint.cj
`-- main.cj
Run the following compilation command in the src
directory.
cjc macros/*.cj --compile-macro
cjc main.cj -o main
Run ./main
. The following information is output:
x = 3
x + y = 5
Let's discuss each part of the code:
-
Line 1:
macro package define
Macros must be declared in a special separate package (different from package that uses these macros). Packages containing macros are declared using
macro package
. Here we declare a macro package nameddefine
. -
Line 2:
import std.ast.*
The data types required for implementing macros, such as
Tokens
and the syntax node types (which will be mentioned later), are provided by theast
package of the standard library of Cangjie. Therefore, theast
package must be imported before any macro declaration. -
Line 3:
public macro dprint(input: Tokens): Tokens
Here we declare a macro named
dprint
. Since the macro is a non-attribute macro (which will be explained later), it accepts a parameter of theTokens
type. The input represents the program fragment passed to the macro. The return value of the macro is also a program fragment of theTokens
type. -
Line 4:
let inputStr = input.toString()
In the implementation of the macro, the input program fragment is first converted into a string. In the preceding test case,
inputStr
becomes"x"
or"x + y"
. -
Lines 5 to 7:
let result = quote(...)
The
quote
expression is used to constructTokens
. It converts the program fragment in parentheses toTokens
. In the input ofquote
, you can interpolate$(...)
to convert the expression in parentheses toTokens
and then insert it into theTokens
constructed byquote
. In the above code,$(inputStr)
inserts the value of theinputStr
string (including the quotation marks on both ends of the string), and$(input)
insertsinput
, that is, the input program fragment. Therefore, if the input expression isx + y
, theTokens
is as follows:print("x + y" + " = ") println(x + y)
-
Line 8:
return result
Finally, the constructed code is returned. The two code lines will be compiled and
x + y = 5
will be output during execution.
Looking back at the definition of the dprint
macro, we can notice that dprint
uses Tokens
as the input parameter, and uses quote
and interpolation to construct another Tokens
as the return value. To use the Cangjie macro, you need to investigate the concepts of Tokens
, quote
, and interpolation. Let's discuss each one separately.