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 defineMacros 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
Tokensand the syntax node types (which will be mentioned later), are provided by theastpackage of the standard library of Cangjie. Therefore, theastpackage must be imported before any macro declaration. -
Line 3:
public macro dprint(input: Tokens): TokensHere we declare a macro named
dprint. Since the macro is a non-attribute macro (which will be explained later), it accepts a parameter of theTokenstype. The input represents the program fragment passed to the macro. The return value of the macro is also a program fragment of theTokenstype. -
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,
inputStrbecomes"x"or"x + y". -
Lines 5 to 7:
let result = quote(...)The
quoteexpression 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 toTokensand then insert it into theTokensconstructed byquote. In the above code,$(inputStr)inserts the value of theinputStrstring (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, theTokensis as follows:print("x + y" + " = ") println(x + y) -
Line 8:
return resultFinally, the constructed code is returned. The two code lines will be compiled and
x + y = 5will 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.