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 example code is used to print 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 function can obtain only the value of the input expression, but cannot obtain the input expression itself. However, dprint can be implemented as a macro to obtain the program fragment of the input expression.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 interpreting each line of code, test the expected effect of the macro. First, we need to create a define folder in the current directory and a dprint.cj file in the define folder. Then, 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 same directory:

import define.*

main() {
    let x = 3
    let y = 2
    @dprint(x)
    @dprint(x + y)
}

The directory structure is as follows:

// Directory layout.
src
|-- define
|     `-- dprint.cj
`-- main.cj

Run the following compilation command in the src directory.

cjc define/*.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 named define.

  • 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 the ast package of the standard library of Cangjie. Therefore, the ast 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 the Tokens type. The input represents the program fragment passed to the macro. The return value of the macro is also a program fragment of the Tokens 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 expressions are used to construct Tokens.It converts the program fragment in parentheses to Tokens. In the input of quote, you can interpolate $(...) to convert the expression in parentheses to Tokens and then insert it into the Tokens constructed by quote. In the above code, $(inputStr) inserts the value of the inputStr string (including the quotation marks on both ends of the string), and $(input) inserts input, that is, the input program fragment. Therefore, if the input expression is x + y, the Tokens is as follows:

    print("x + y" + " = ")
    println(x + y)
    
  • Line 8: return result

    Finally, the constructed code snippets are returned. The two lines of code snippets are compiled, and x + y = 5 is output during runtime.

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 macro, you need to investigate the concepts of Tokens, quote, and interpolation. Let's discuss each one separately.