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 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 expression is 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 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.