Versions/v1.0.5/English

Macro Implementation

Sections8
On This Page8
API Symbols43

Macro Implementation

This chapter introduces the definition and usage of Cangjie macros, which can be categorized into Non-Attribute Macros and Attribute Macros. Additionally, this chapter will cover the behavior when macros are nested.

Non-Attribute Macros

Non-attribute macros only accept the code to be transformed and do not take other parameters (attributes). Their definition format is as follows:

The macro invocation format is as follows:

Macro invocations are enclosed in (). The content inside the parentheses can be any valid Tokens or empty.

When a macro is applied to a declaration, the parentheses can generally be omitted. Refer to the following examples:

Special notes on the legality of Tokens within parentheses:

- The input must consist of a sequence of valid Tokens. Symbols like "#", "\", "\\", etc., when used alone, are not valid Cangjie Tokens and are not supported as input values.

- If the input contains unmatched parentheses, they must be escaped using the escape symbol "\\".

- If the input contains "@" as a Token, it must be escaped using the escape symbol "\\".

For special cases of input, refer to the following examples:

The macro expansion process operates on the Cangjie syntax tree. After expansion, the compiler continues with subsequent compilation steps. Therefore, the following rules must be observed:

- The expanded code must still be valid Cangjie code, and the expanded code must not contain package declarations or import statements, as this may cause compilation issues.
- When a macro is used for a declaration, if parentheses are omitted, the input must be syntactically valid for the declaration.

Below are several typical examples of macro applications.

- Example 1

Macro definition file macro_definition.cj


Macro invocation file macro_call.cj

The compilation process for the above code can be referred to in Macro Compilation and Usage.

Print statements have been added in the example, where I'm in macro body in the macro definition will be output during the compilation of macro_call.cj. Simultaneously, the macro invocation point is expanded. For example, compiling the following code:

The compiler updates the Tokens returned by the macro to the syntax tree at the invocation point, resulting in the following code:

In other words, the actual code in the executable becomes:

The value of a is computed as 3, and when printing the value of a, it is interpolated as 3. Thus, the output of the above program is:

Now, let's look at a more meaningful example of using a macro to process a function. The ModifyFunc macro adds an id parameter to myFunc and inserts code before and after counter++.

- Example 2

Macro definition file macro_definition.cj


Macro invocation file macro_call.cj

Similarly, the above two code segments are located in different files. First, compile the macro definition file macro_definition.cj, then compile the macro invocation file macro_call.cj to generate the executable.

In this example, the ModifyFunc macro takes a function declaration as input, so the parentheses can be omitted:

After macro expansion, the following code is obtained:

myFunc is called in main, and the actual parameter it receives is also defined in main`, forming a valid Cangjie program. The runtime output is as follows:
cangjie
import std.ast.*

public macro MacroName(args: Tokens): Tokens {
    ... // Macro body
}
cangjie
@MacroName(...)
cangjie
@MacroName func name() {}        // Before a FuncDecl
@MacroName struct name {}        // Before a StructDecl
@MacroName class name {}         // Before a ClassDecl
@MacroName var a = 1             // Before a VarDecl
@MacroName enum e {}             // Before a Enum
@MacroName interface i {}        // Before a InterfaceDecl
@MacroName extend e <: i {}      // Before a ExtendDecl
class C {
    @MacroName prop i: Int64 {   // Before a PropDecl
        get() { 0 }
    }
} 
@MacroName @AnotherMacro(input)  // Before a macro call
cangjie
// Illegal input Tokens
@MacroName(#)    // Not a whole Token
@MacroName(`)    // Not a whole Token
@MacroName(()    // ( and ) not match
@MacroName(\[)   // Escape for unsupported symbol

// Legal input Tokens
@MacroName(#"abc"#)
@MacroName(`class`)
@MacroName([)
@MacroName([])
@MacroName(\()
@MacroName(\@)

Attribute Macros

Compared to non-attribute macros, attribute macros include an additional Tokens type input parameter. This additional parameter allows developers to input extra information. For example, developers might want to use different macro expansion strategies in different invocation scenarios, which can be indicated via this attribute parameter. Additionally, this attribute parameter can accept any Tokens, which can be combined or concatenated with the code modified by the macro. Below is a simple example:


As shown in the macro definition above, an attribute macro has two parameters of type Tokens. Within the macro definition, attrTokens and inputTokens can undergo various transformations such as combination or concatenation, and the new Tokens is returned.

The invocation of an attribute macro is similar to that of a non-attribute macro. The additional parameter attrTokens is passed via [], and the invocation form is as follows:

- For the macro Foo invocation, when the parameter is 2+3, it is concatenated with the attribute 1+ inside []. After macro expansion, the result is var a: Int64 = 1+2+3.
- For the macro Foo invocation, when the parameter is struct Data, it is concatenated with the attribute public inside []. After macro expansion, the result is:

Regarding attribute macros, the following points should be noted:

- Attribute macros, compared to non-attribute macros, can modify the same AST nodes. Essentially, attribute macros enhance the parameters that can be passed.

- The rules for the legality of parameters inside parentheses for attribute macros are consistent with those for non-attribute macros.

- Special notes on the legality of attribute parameters inside square brackets:

- The input must consist of a sequence of valid Tokens. Symbols like "#", "", "\\", etc., when used alone, are not valid CangjieTokens and are not supported as input values.

- If the input contains unmatched square brackets, they must be escaped using the escape symbol "\\".

- If the input contains "@" as a Token, it must be escaped using the escape symbol "\\".

- The macro definition and invocation types must be consistent: if the macro definition has two parameters (i.e., an attribute macro definition), the invocation must include [], and the content can be empty; if the macro definition has one parameter (i.e., a non-attribute macro definition), the invocation must not use []`.

cangjie
macro package define

// Macro definition with attribute
public macro Foo(attrTokens: Tokens, inputTokens: Tokens): Tokens {
    return attrTokens + inputTokens  // Concatenate attrTokens and inputTokens.
}
cangjie
import define.Foo

// attribute macro with parentheses
var a: Int64 = @Foo[1+](2+3)

// attribute macro without parentheses
@Foo[public]
struct Data {
    var count: Int64 = 100
}

main() {}

Nested Macros

The Cangjie language does not support nested macro definitions but conditionally supports nested macro invocations within macro definitions and macro invocations.

Nested Macro Invocations in Macro DefinitionsHere is the professional translation of the provided Markdown content from Chinese to English, maintaining all structural and formatting elements

Below is an example of macro definitions containing nested macro calls.

Macro Definitions with Nested Calls

The getIdent macro is defined in macro package pkg1:


The Prop macro in package pkg2 contains a nested call to getIdent:


Macro usage in package pkg3 calling the Prop macro:

Note: Due to the constraint that macro definitions must be compiled before their call sites, the compilation order must be: pkg1 → pkg2 → pkg3. The Prop macro definition in pkg2:

Will first be expanded into the following code before compilation:

cangjie
macro package pkg1

import std.ast.*

public macro getIdent(attr:Tokens, input:Tokens):Tokens {
    return quote(
        let decl = (parseDecl(input) as VarDecl).getOrThrow()
        let name = decl.identifier.value
        let size = name.size - 1
        let $(attr) = Token(TokenKind.IDENTIFIER, name[0..size])
    )
}
cangjie
macro package pkg2

import std.ast.*
import pkg1.*

public macro Prop(input:Tokens):Tokens {
    let v = parseDecl(input)
    @getIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}
cangjie
package pkg3

import pkg2.*
class A {
    @Prop
    private let a_: Int64 = 1
}

main() {
    let b = A()
    println("${b.a}")
}
cangjie
public macro Prop(input:Tokens):Tokens {
    let v = parseDecl(input)
    @getIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}
cangjie
public macro Prop(input: Tokens): Tokens {
    let v = parseDecl(input)

    let decl = (parseDecl(input) as VarDecl).getOrThrow()
    let name = decl.identifier.value
    let size = name.size - 1
    let ident = Token(TokenKind.IDENTIFIER, name[0 .. size])

    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

Nested Macro Calls

A common scenario for nested macros occurs when macro-decorated code blocks contain other macro calls. A concrete example:

Macros Foo and Bar defined in package pkg1:


The addToMul macro defined in package pkg2:


Usage of these three macros in package pkg3:

As shown above, macro Foo decorates struct Data, while macro calls addToMul and Bar appear inside the struct. The transformation rule for such nested scenarios is: expand the innermost macros (addToMul and Bar) first, then expand the outer macro (Foo). Multi-level nesting is allowed, with expansion always proceeding from innermost to outermost.

Nested macros can appear in both parenthesized and unparenthesized macro calls. These can be combined, but developers must ensure unambiguous expansion order:

cangjie
macro package pkg1

import std.ast.*

public macro Foo(input: Tokens): Tokens {
    return input
}

public macro Bar(input: Tokens): Tokens {
    return input
}
cangjie
macro package pkg2

import std.ast.*

public macro addToMul(inputTokens: Tokens): Tokens {
    var expr: BinaryExpr = match (parseExpr(inputTokens) as BinaryExpr) {
        case Some(v) => v
        case None => throw Exception()
    }
    var op0: Expr = expr.leftExpr
    var op1: Expr = expr.rightExpr
    return quote(($(op0)) * ($(op1)))
}
cangjie
package pkg3

import pkg1.*
import pkg2.*
@Foo
struct Data {
    let a = 2
    let b = @addToMul(2+3)

    @Bar
    public func getA() {
        return a
    }

    public func getB() {
        return b
    }
}

main(): Int64 {
    let data = Data()
    var a = data.getA() // a = 2
    var b = data.getB() // b = 6
    println("a: ${a}, b: ${b}")
    return 0
}
cangjie
var a = @foo(@foo1(2 * 3)+@foo2(1 + 3))  // foo1, foo2 have to be defined.

@Foo1 // Foo2 expands first, then Foo1 expands.
@Foo2[attr: struct] // Attribute macro can be used in nested macro.
struct Data{
    @Foo3 @Foo4[123] var a = @bar1(@bar2(2 + 3) + 3)  // bar2, bar1, Foo4, Foo3 expands in order.
    public func getA() {
        return @foo(a + 2)
    }
}

Message Passing Between Nested Macros

This refers to nested macro calls.

Inner macros can use the library function assertParentContext to ensure they are only called within specific outer macro contexts. If this condition isn't met, the function throws an error. The InsideParentContext function similarly checks nesting relationships, returning a boolean. Example:

Macro definitions:

Macro calls:

Here, Inner uses assertParentContext to verify it's called within an Outer macro. Since this nesting doesn't exist in the example, the compiler reports an error.

Inner macros can also communicate with outer macros via key/value pairs. During execution:

1. Inner macros send messages via setItem
2. Outer macros receive these messages via getChildMessages (a collection of key/value mappings)

Example macro definitions:


Macro calls:


In this code, Outer receives variable names from two Inner macros and automatically adds to the class:

Workflow:

1. Inner macros send messages via setItem
2. Outer macro receives messages via getChildMessages (multiple Inner calls possible)
3. Values are retrieved via the message object's getString method

---

The translation strictly maintains all Markdown formatting, code blocks, and structural elements while providing accurate technical terminology and natural English flow.

cangjie
public macro Outer(input: Tokens): Tokens {
    return input
}

public macro Inner(input: Tokens): Tokens {
    assertParentContext("Outer")
    return input
}
cangjie
@Outer var a = 0
@Inner var b = 0 // Error: The macro call 'Inner' should be nested within an 'Outer' context.
cangjie
macro package define

import std.ast.*

public macro Outer(input: Tokens): Tokens {
    let messages = getChildMessages("Inner")

    let getTotalFunc = quote(public func getCnt() {
                       )
    for (m in messages) {
        let identName = m.getString("identifierName")
        // let value = m.getString("key")            // Receive multiple messages
        getTotalFunc.append(Token(TokenKind.IDENTIFIER, identName))
        getTotalFunc.append(quote(+))
    }
    getTotalFunc.append(quote(0))
    getTotalFunc.append(quote(}))
    let funcDecl = parseDecl(getTotalFunc)

    let decl = (parseDecl(input) as ClassDecl).getOrThrow()
    decl.body.decls.add(funcDecl)
    return decl.toTokens()

}

public macro Inner(input: Tokens): Tokens {
    assertParentContext("Outer")
    let decl = parseDecl(input)
    setItem("identifierName", decl.identifier.value)
    // setItem("key", "value")                      // Multiple messages via different keys
    return input
}
cangjie
import define.*

@Outer
class Demo {
    @Inner var state = 1
    @Inner var cnt = 42
}

main(): Int64 {
    let d = Demo()
    println("${d.getCnt()}")
    return 0
}
cangjie
public func getCnt() {
    state + cnt + 0
}