Metaprogramming

Metaprogramming allows code to be represented as operable data objects that can be added, deleted, modified, and queried. To support this, Cangjie provides compilation tags for metaprogramming. Compilation tags are classified into macros and annotations. The macro mechanism supports code transformation during compilation based on the Tokens type, and supports basic operations such as converting code into data and concatenating code.

quote Expression and Tokens Type

Cangjie uses the quote expression to reference specific code, representing it as an operable data object. The syntax of the quote expression is defined as follows:

quoteExpression
    : 'quote' quoteExpr
    ;

quoteExpr
    : '(' quoteParameters ')'
    ;

quoteParameters
    : (quoteToken | quoteInterpolate | macroExpression)+
    ;

quoteToken
    : '.' | ',' | '(' | ')' | '[' | ']' | '{' | '}' | '**' | '*' | '%' | '/' | '+' | '-'
    | '|>' | '~>'
    | '++' | '--' | '&&' | '||' | '!' | '&' | '|' | '^' | '<<' | '>>' | ':' | ';'
    | '=' | '+=' | '-=' | '*=' | '**=' | '/=' | '%='
    | '&&=' | '||=' | '&=' | '|=' | '^=' | '<<=' | '>>='
    | '->' | '=>' | '...' | '..=' | '..' | '#' | '@' | '?' | '<:' | '<' | '>' | '<=' | '>='
    | '!=' | '==' | '_' | '\\' | '`' | '$'
    | 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'UInt8' | 'UInt16' | 'UInt32' | 'UInt64' | 'Float16'
    | 'Float32' | 'Float64' | 'Rune' | 'Bool' | 'Unit' | 'Nothing' | 'struct' | 'enum' | 'This'
    | 'package' | 'import' | 'class' | 'interface' |'func' | 'let' | 'var' | 'type'
    | 'init' | 'this' | 'super' | 'if' | 'else' | 'case' | 'try' | 'catch' | 'finally'
    | 'for' | 'do' | 'while' | 'throw' | 'return' | 'continue' | 'break' | 'as' | 'in' | '!in'
    | 'match' | 'from' | 'where' | 'extend' | 'spawn' | 'synchronized' | 'macro' | 'quote' | 'true' | 'false'
    | 'sealed' | 'static' | 'public' | 'private' | 'protected'
    | 'override' | 'abstract' | 'open' | 'operator' | 'foreign'
    | Identifier | DollarIdentifier
    | literalConstant
    ;

quoteInterpolate
    : '$' '(' expression ')'
    ;

The syntax rules of the quote expression are as follows:

  • The quote keyword is used to define the quote expression.
  • The quote expression is followed by (), where it can reference values of the Token type, code interpolation, and macro call expressions.
  • The quote expression is of the Tokens type. Tokens is a type provided by the Cangjie standard library. It is a sequence consisting of lexical units (tokens).

In the preceding syntax definition, quoteToken refers to any valid token supported by the Cangjie compiler, except for tokens that cannot be parsed by Lexer, such as comments and terminators. However, when a newline character is used as the separator between two expressions, it is not ignored and is parsed as a quoteToken. In the following example, only the newline character between two assignment expressions is parsed as quoteToken, and other newline characters in the quote expression are ignored.

// The newline character between assignment expression is preserved, others are ignored.
quote(

var a = 3
var b = 4

)

Consecutive quotes without code interpolation are not allowed. For details about code interpolation, see the next section. When the quote expression references the following tokens, escape is required:

  • Unpaired parentheses are not allowed in the quote expression. However, parentheses escaped by \ are not counted in the matching rule.
  • When @ indicates a common token instead of a macro call expression, \ needs to be used for escape.
  • When $ indicates a common token instead of code interpolation, \ needs to be used for escape.

Note: The token (in lower case) mentioned in this chapter refers to the lexical unit parsed by Cangjie Lexer.

The following are some quote expression examples that use Tokens or code interpolation as parameters:

let a0: Tokens = quote(==)  // ok
let a1: Tokens = quote(2+3) // ok
let a2: Tokens = quote(2) + quote(+) + quote(3)  // ok
let a3: Tokens = quote(main(): Int64 {
    0
}) // ok
let a4: Tokens = quote(select * from Users where id=100086) // ok

let b0: Tokens = quote(()  // error: unmatched `(`
let b1: Tokens = quote(quote(x))  // ok -- `b1.size == 4`

The quote expression can also reference a macro call expression. For example:

quote(@SayHi("say hi")) // a quoted, un-expanded macro -- macro expansion happens later

Cangjie provides the Tokens type to indicate the token sequence parsed from the original text information. Tokens supports the use of operator + to concatenate the token results of two objects into a new object. In the following example, a1 and a2 are basically equivalent.

let a1: Tokens = quote(2+3) // ok
let a2: Tokens = quote(2) + quote(+) + quote(3) // ok

Code Interpolation

In a quote expression, $ is used as the code interpolation operator. The operator is followed by an expression, indicating that the value of the expression is converted into tokens. This operator is a unary prefix operator and can be used only in quote expressions. It has a higher priority than other operators.

Code interpolation can be applied to all valid Cangjie expressions. Code interpolation is not an expression and does not have a type.

var rightOp: Tokens = quote(3)
quote(2 + $rightOp)        // quote(2 + 3)

For an expression to be eligible for code interpolation, it must implement the ToTokens interface. The format of the ToTokens interface is as follows:

interface ToTokens  {
    func toTokens(): Tokens
}
  • Most value types in Cangjie, including the numeric type, Rune type, Bool type, and String type, have default implementations.
  • The Tokens type implements the ToTokens interface by default, and the toTokens function returns this instance.
  • The user-defined data structure must proactively implement the ToTokens interface. The Cangjie standard library provides various interfaces for generating Tokens. For example, for the variable str of the String type, you can directly use str.toTokens() to obtain the Tokens corresponding to str.

quote Expression Evaluation Rules

The rules for references within the parentheses of the quote expression are as follows:

  • Code interpolation: The result of the interpolated expression .toTokens() is used.
  • Common tokens: The corresponding Tokens result is used.

Based on the preceding conditions, the quote expression then concatenates all the Tokens according to the order in which they appear in the expression.

The following are some quote expression evaluation examples. The comments indicate the returned Tokens results.

var x: Int64 = 2 + 3

// tokens in the quote are obtained from the corresponding Tokens.
quote(x + 2)        // quote(x + 2)

// The Tokens type can only add with Tokens type.
quote(x) + 1        // error! quote(x) is Tokens type, cannot add with integer

// The value of code interpolation in quote equals to the tokens result corresponding to the value of the interpolation expression.
quote($x)           // quote(5)
quote($x + 2)       // quote(5 + 2)
quote($x + (2 + 3)) // quote(5 + (2 + 3))
quote(1 + ($x + 1) * 2)    // quote(1 + (5 + 1) * 2)
quote(1 + $(x + 1) * 2)    // quote(1 + 6 * 2)

var t: Tokens = quote(x)   // quote(x)

// without interpolation, the `t` is the token `t`
quote(t)                   // quote(t)

// with interpolation, `t` is evaluated and expected to implement ToTokens
quote($t)                  // quote(x)
quote($t+1)                // quote(x+1)

// quote expressions can be used inside of interpolations, and cancel out
quote($(quote(t)))         // quote(t)
quote($(quote($t)))        // quote(x)

quote($(t+1))              // error! t is Tokens type, cannot add with integer

public macro PlusOne(input: Tokens): Tokens {
  quote(($input + 1))
}

// Macro invocations are preserved, unexpanded, until they are re-inserted into the code by
// the macro expander. However, interpolation still happens, including among the arguments
// to the macro
quote(@PlusOne(x))           // quote(@PlusOne(x))
quote(@PlusOne($x))          // quote(@PlusOne(5))
quote(@PlusOne(2+3))         // quote(@PlusOne(2+3))
quote(1 + @PlusOne(x) * 2)   // quote(1 + @PlusOne(x) * 2)

// When the macro invocation is outside the quote, the macro expansion happens early
var y: Int64 = @PlusOne(x)   // 5 + 1
quote(1 + $y * 2 )           // quote(1 + 6 * 2)

Macros

Macros are an essential technology for metaprogramming. Similar to functions, macros can be called and have inputs and outputs. The difference is that the macro code is expanded during compilation. Cangjie replaces the macro call expression with the expanded code. You can use macros to compile code templates (codes for generating code). In addition, macros provide the capability to define custom grammars, allowing you to flexibly define your own DSL grammars. Finally, Cangjie provides various code operation interfaces for you to easily complete code transformation.

Macros are classified into two types: non-attribute macros and attribute macros. Compared with a non-attribute macro, an attribute macro can analyze and transform the macro input based on different attributes to obtain different output results.

Macro Definition

In Cangjie, macro definition must comply with the following syntax rules:

macroDefinition
    : 'public' 'macro' identifier
    (macroWithoutAttrParam | macroWithAttrParam)
    (':' identifier)?
    ('=' expression | block)
    ;

macroWithoutAttrParam
    : '(' macroInputDecl ')'
    ;

macroWithAttrParam
    : '(' macroAttrDecl ',' macroInputDecl ')'
    ;

macroInputDecl
    : identifier ':' identifier
    ;

macroAttrDecl
    : identifier ':' identifier
    ;

The summary is as follows:

  • The keyword macro is used to define a macro.

  • macro must be preceded by public, indicating that the macro is visible to the outside of the package.

  • The macro must be followed by the macro name.

  • Macro parameters are enclosed by (). For a non-attribute macro, one parameter of the Tokens type corresponds to the input of the macro. For an attribute macro, two parameters of the Tokens type correspond to the attribute and input of the macro respectively.

  • The default return type function is fixed to Tokens.

The following is an example of a non-attribute macro. It contains all elements of the macro definition: public indicates that the macro is visible to the outside of the package; macro is the keyword; foo is the identifier of the macro; x is the parameter and its type is Tokens; the return value and its type are the same as that of the input.

public macro foo(x: Tokens): Tokens { x }

public macro bar(x: Tokens): Tokens {
  return quote($x)      // or just `return x`
}

The following is an example of an attribute macro. Compared with the non-attribute macro, the attribute macro has an additional input of the Tokens type. More flexible operations can be performed in the macro definition body.

public macro foo(attr: Tokens, x: Tokens): Tokens { attr + x }

public macro bar(attr: Tokens, x: Tokens): Tokens {
    return quote($attr + $x)
}

Once a macro is defined, the macro name cannot be assigned a value. In addition, macros have strict requirements on the parameter type and number of parameters.

Macro Call

The macro call expression complies with the following syntax rules:

macroExpression
    : '@' identifier macroAttrExpr?
    (macroInputExprWithoutParens | macroInputExprWithParens)
    ;

macroAttrExpr
    : '[' quoteToken* ']'
    ;

macroInputExprWithoutParens
    : functionDefinition
    | operatorFunctionDefinition
    | staticInit
    | structDefinition
    | structPrimaryInit
    | structInit
    | enumDefinition
    | caseBody
    | classDefinition
    | classPrimaryInit
    | classInit
    | interfaceDefinition
    | variableDeclaration
    | propertyDefinition
    | extendDefinition
    | macroExpression
    ;

macroInputExprWithParens
    : '(' macroTokens ')'
    ;

macroTokens
    : (quoteToken | macroExpression)*
    ;

The rules for macro call expressions are as follows:

  • The @ keyword is used to define the macro call expression.
  • A macro call is enclosed in (). The value in the parentheses can be any valid tokens, but cannot be blank.
  • When an attribute macro is called, the macro attributes are enclosed in []. The value in the square brackets can be any valid tokens, but cannot be blank.

When the macro call expression references the following tokens, escape is required:

  • Unpaired parentheses, such as @ABC(we have two (( and one )), are not allowed in macro call expression parameters. However, parentheses escaped by \ are not counted in the matching rule.
  • Unpaired square brackets, such as @ABC[we have two [[ and one ]](), are not allowed in attribute macros. However, square brackets escaped by \ are not counted in the matching rule.
  • When @ is referenced in macro call expression parameters, \ needs to be used for escape.

When a macro call is used before some declarations or expressions, parentheses can be omitted, and its semantics is different from that of the macro call without parentheses. The macro call parameter with parentheses can be any valid tokens. The macro call parameter without parentheses must be one of the following declarations or expressions:

  • Function declaration
  • struct declaration
  • enum declaration
  • enum constructor
  • Class declaration
  • Static initializer
  • Class and struct constructor and primary constructor
  • API declaration
  • Variable declaration
  • Property declaration
  • Extension declaration
  • Macro call expression

In addition, the macro call parameters without parentheses must meet the following requirements:

  • If the parameter is a declaration, the macro call can only appear in the position where the declaration is allowed.

The following are examples of calling non-attribute macros, attribute macros, and macros without parentheses.

func foo()
{
    print("In foo\n")
}

// Non-attribute macros
public macro Twice(input: Tokens): Tokens
{
    print("Compiling the macro `Twice` ...\n")
    quote($input; $input)
}

@Twice(foo())  // After Macro expand: foo(); foo()
@Twice()       // error, parameters in macro invocation can not be empty.

// Attributed macros
public macro Joint(attr: Tokens, input: Tokens): Tokens
{
    print("Compiling the macro `Joint` ...\n")
    quote($attr; $input)
}

@Joint[foo()](foo()) // After Macro expand: foo(); foo()
@Joint[foo()]()      // error, parameters in macro invocation can not be empty.
@Joint[](foo())      // error, attribute in macro invocation can not be empty.

// Non-attribute macros
public macro MacroWithoutParens(input: Tokens): Tokens
{
    print("Compiling the macro `MacroWithoutParens` ...\n")
    quote(func foo() { $input })
}

@MacroWithoutParens
var a: Int64 = 0  // After Macro expand: func foo() { var a: Int64 = 0 }

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

@echo class A {}  // ok, class can only be defined in top-level, so is current macro invocation

func goo() {
  @echo func tmp() {} // ok, function can be defined in another function body,
                      // so is current macro invocation
  @echo class B {}    // error, class can only be defined in top-level, so is current macro invocation
}

As an expression, a macro call can appear at any position where an expression is allowed. In addition, macro call can appear at positions of the following declarations: function declaration, struct declaration, enum declaration, class declaration, interface declaration, variable declaration (except function parameters), property declaration, and extension declaration.

Macros are visible only during compilation. During compilation, the macro call expression is replaced by the code after macro expansion. Macro expansion indicates that the macro definition is executed during code compilation. The execution result is parsed into a Cangjie syntax tree to replace the macro call expression. After semantic analysis, the replaced node has corresponding type information, and the type may be considered as a type of a macro call expression. In the preceding example, when you call macros such as @Twice, @Joint, and @MacroWithoutParens, the code in the macro definition is compiled and executed, and the Compiling the macro... is printed. The execution result is used as a new syntax tree node to replace the original macro call expression.

When macro call occurs in different contexts, the following rules must be complied:

  1. If a macro call appears in the context of the expected expression or declaration, the macro call is expanded into program text during compilation.

  2. If a macro call appears in the context of the expected token sequence (as a parameter of the macro call expression or quote expression), the macro call is called at the time when the token sequence is evaluated, and the return value (token sequence) of the macro call is directly used without being expanded into the program text.

Importing Macro Scopes and Packages

The macro must be defined at the top level of the source file and the scope is the entire package.

The package where the macro definition is located must be declared using macro package. For packages specified by macro package, only the macro definition declaration is visible to the outside of the package. Other declarations are visible only in the package. If other declarations are modified to be visible to the outside of the package, an error is reported.

//define.cj
macro package define         // modify package with macro
import std.ast.*
// public func A(){}       // error: func A can not be modified by `public`
public macro M(input:Tokens): Tokens{ // only macros can be modified by `public`
   return input
}

// call.cj
package call
import define.*  
main(){
   @M()
   return 0
}

The macro import complies with the general package import rules of Cangjie.

Note that macro package can be re-exported only by macro package.

// A.cj
macro package A
public macro M1(input:Tokens):Tokens{
    return input
}
​​
// B.cj
package B
//public import A.* // error: macro packages can only be `public import` in macro packages
public func F1(input:Int64):Int64{
    return input
}

// C.cj
macro package 
public import A.*  // only macro packages can be public import in macro packages
// public import B.*  // error: normal packages can not be public import in macro packages
import B.* 
public macro M2(input:Tokens):Tokens{
    return @M1(input) + Token(TokenKind.ADD) + quote($(F1(1)))
}

In the same package, the conventions for macros with the same name are the same as those of functions. The following table lists the rules for attribute macros and non-attribute macros.

Same name, Same packageattribute macrosnon-attribute macros
attribute macrosNOYES
non-attribute macrosYESNO

Nested Macros and Recursive Macros

Macros allow for nested calls to other macros.

If a macro call expression contains macro call, for example, @Outer @Inner(2+3), the inner macro is executed first, followed by the outer macro. The Tokens result returned by the inner macro is concatenated with other Tokens and passed to the outer macro for calling. The inner macro and the outer macro can be different macros or the same macro.

An inner macro can call the AssertParentContext library function to ensure it is nested in a specific outer macro call. If the call of this function by the inner macro is not nested within the given outer macro call, the function throws an error. The second function InsideParentContext returns true only when the inner macro call is nested in the given outer macro call.

public macro Inner(input: Tokens): Tokens {
    AssertParentContext("Outer")
    // ...or...
    if (InsideParentContext("Outer")) {
        // ...
    }
}

The inner macros can communicate with the outer macro by sending key/value pairs. When an inner macro is executed, the standard library function SetItem is called to send a message. When an outer macro is executed, the standard library function GetChildMessages is called to receive the message (a key/value pair mapping) sent by each inner macro.

public macro Inner(input: Tokens): Tokens {
    AssertParentContext("Outer")
    SetItem("key1", "value1")
    SetItem("key2", "value2")
    // ...
}

public macro Outer(input: Tokens): Tokens {
    let messages = GetChildMessages("Inner")
    for (m in messages) {
        let value1 = m.getString("key1")
        let value2 = m.getString("key2")
        // ...
    }
}

When the macro definition body contains macro calls, if the macro calls appear in the context of the expected token sequence, the two macros can be different macros or the same macro (that is, recursion is supported). Otherwise, the nested macro cannot be the same as the called macro. For details, see the following two examples.

In the following example, the nested macro appears in the quote expression, indicating that recursive call is supported.

public macro A(input: Tokens): Tokens {
    print("Compiling the macro A ...\n")
    let tmp = A_part_0(input)
    if cond {
        return quote($tmp)
    }
    let bb: Tokens = quote(@A(quote($tmp)))  // ok
    A_part_1()
}

main():Int64 {
    var res: Int64 = @A(2+3)  // ok, @A will be treated as Int64 after macro expand
    return res
}

In this example, when macro A is not called by external systems, it is not executed (even if macro A is called internally), that is, Compiling the macro A... is not printed. if cond is the recursive termination condition. Note that macro recursion and function recursion have similar constraints. A termination condition is required; otherwise, an infinite loop occurs and the compilation cannot be terminated.

In the following example, the nested macro is not in the quote expression, therefore, recursive call is not supported.

public macro A(input: Tokens): Tokens {
    let tmp = A_part_0(input)
    if cond {
        return quote($tmp)
    }
    let bb: Tokens = @A(quote($tmp))  // error, recursive macro expression not in quote
    A_part_1()
}

main():Int64 {
    var res: Int64 = @A(2+3)  // error, type mismatch
    return res
}

The rules for nested macro call and recursive macro call are as follows:

  • The macro call expression allows nested call.
  • A macro definition allows nested call of other macros, but recursive call of itself is allowed only in the quote expression.

Restrictions

  • Macros conditionally support recursive call of themselves. For details, see the preceding sections.

  • Except for recursive macro call, macro definition and macro call must be located in different packages. The location where a macro is called must import the package where the macro definition is located to ensure that the macro definition is compiled before the macro call point. Import is not allowed for macros with cyclic dependency. For example, the following usage is invalid: pkgA imports pkgB, and pkgB imports pkgA, creating a cyclic import dependency.

    // ======= file A.cj
    macro package pkgA
    import pkgB.*
    public macro A(..) {
    	@B(..)  // error
    }
    
    // ======= file B.cj
    macro package pkgB
    import pkgA.*
    public macro B(..) {
    	@A(..)  // error
    }
    
  • Macros allow for nested calls to other macros. The called macro also requires that the definition point and call point be located in different packages.

Built-in Compilation Flags

Source Code Location

Cangjie offers several built-in compilation tags to obtain the location of the source code during compilation.

  • After @sourcePackage() is expanded, it becomes a literal of the String type, representing the package name of the source code where the current macro is located.
  • After @sourceFile() is expanded, it becomes a literal of the String type, representing the file name of the source code where the current macro is located.
  • After @sourceLine() is expanded, it becomes a literal of the Int64 type, representing the code line of the source code where the current macro is located.

These compilation tags can be used in any expressions, as long as they meet the type check rules. Example:

func test1() {
    let s: String = @sourceFile()  // The value of `s` is the current source file name
}

func test2(n!: Int64 = @sourceLine()) { /* at line 5 */
    // The default value of `n` is the source file line number of the definition of `test2`
    println(n) // print 5
}

@Intrinsic

The @Intrinsic tag can be used to modify global functions. The functions modified by @Intrinsic are special functions provided by the compiler.

  1. Functions modified by @Intrinsic cannot have function bodies.
  2. Functions modified by @Intrinsic are a list determined by the compiler and released with the standard library. If any function not on the list is modified by @Intrinsic, an error is reported.
  3. The @Intrinsic tag is visible only in the std module. Outside the std module, users can name their own macros or annotations as Intrinsic without name conflicts.
  4. Functions modified by @Intrinsic cannot be used as first-class citizens.

The sample code is as follows:

@Intrinsic
func invokeGC(heavy: Bool):Unit
 
public func GC(heavy!: Bool = false): Unit {
    unsafe { return invokeGC (heavy) }  // CJ_MCC_InvokeGC(heavy)
}

@FastNative

To improve performance when interoperating with the C language, Cangjie provides @FastNative to optimize C function calls. Note that @FastNative can be used only for functions declared by foreign.

When using @FastNative to modify the foreign function, ensure that the corresponding C function meets the following requirements:

  1. The overall execution time of the function should not be too long. For example, the function should not contain large loops or produce blocking, such as calling functions like sleep and wait.
  2. The Cangjie method cannot be called in the function.

Conditional Compilation

Conditional compilation is a technology that selectively compiles different code segments in program code based on specific conditions. The main uses of conditional compilation include:

  • Platform adaptation: Enables selective code compilation based on the current compilation environment, facilitating cross-platform compatibility.
  • Function selection: Allows selective enabling or disabling of certain features based on different requirements, providing flexible feature configuration. For example, selectively compiling code to include or exclude certain functionalities.
  • Debugging support: Supports compiling code in debug mode to improve program performance and security. For example, compiling debug information or logging code in debug mode, while excluding them in release versions.
  • Performance optimization: Enables selective compilation based on predefined conditions to improve program performance.

Conditional compilation complies with the following syntax rules:

{{conditionalCompilationDefinition|pretty}}

The syntax rules for conditional compilation are summarized as follows:

  • The built-in tag @When can only be used to declare and import nodes.
  • Compilation conditions are enclosed by []. You can enter one or more groups of compilation conditions in []. For details, see [Multi-condition Compilation].

@When[...] is a built-in compilation tag and is processed before import. If the code generated by expanding the macro contains @When[...], a compilation error is reported.

As a builtin compiler directive, @When[...] is processed before import. If the code generated by macro expansion contains @When[...], a compilation error is reported.

Compilation Conditions

A compilation condition mainly consists of relational or logical expressions. The compiler obtains a Boolean value based on the compilation condition to determine which code segment is to be compiled. Condition variables are used by the compiler to calculate compilation conditions. Condition variables can be classified into built-in condition variables and user-defined condition variables based on whether they are provided by the compiler. In addition, the scope of a condition variable is limited to [] of @When. If an undefined condition variable identifier is used in other scopes, an undefined error is triggered.

Built-in Condition Variables of the Compiler

The compiler provides five built-in condition variables for conditional compilation: os, backend, cjc_version, debug, and test, which are used to obtain the corresponding values from the current build environment. The built-in condition variables support comparison and logical operations. Among them, os, backend, and cjc_version support comparison operations, in which condition variables can only be used as left operands of binary operators. The right operand of a binary operator must be a literal value of the String type. Logical operations apply only to condition variables debug and test.

os indicates the OS where the current compilation environment is located. It is used to obtain the specific type of the OS where the compiler is located in real time, and then form a compilation condition with the literal value of the target OS. If you want to generate code for the Windows OS, compare the variable os with the literal value Windows. Similarly, if you want to generate code for Linux, you can compare os with the literal value Linux.

@When[os == "Linux"]
func foo() {
    print("Linux")
}
@When[os == "Windows"]
func foo() {
    print("Windows")
}
main() {
    foo() // Compiling and running the code will print "Linux" or "Windows" on Linux or Windows
    return 0
}

backend indicates the backend used by the current compiler. It is used to obtain the backend type currently used by the compiler in real time, and then form a compilation condition with the literal value of the target backend. If you want to compile code for the cjnative backend, you can compare the backend with the literal value cjnative.

The following backends are supported: cjnative, cjnative-x86_64, cjnative-aarch64, cjvm, cjvm-x86_64, and cjvm-aarch64.

@When[backend == "cjnative"]
func foo() {
    print("cjnative backend")
}
@When[backend == "cjvm"]
func foo() {
    print("cjvm backend")
}
main() {
    foo() // Compile and execute with the cjnative and cjvm backend version, and print "llvm backend" and "cjvm backend" respectively.
}

cjc_version indicates the version number of the current compiler. The version number is a literal value of the String type in the X.Y.Z format, where X, Y, and Z are non-negative integers and cannot contain leading zeros, for example, "0.18.8". It is used to obtain the current version number of the compiler in real time and compare it with the target version number to ensure that the compiler can compile code based on a specific version. Six types of comparison operations are supported: ==, !=, >, <, >=, and <=. The result of the condition is determined by the first difference encountered when comparing these fields from left to right. For example, 0.18.8 < 0.18.11 and 0.18.8 = = 0.18.8.

debug is a conditional compilation identifier, indicating whether the code being compiled is a debug version. It is used to switch between the debug version and the release version during code compilation. The code modified by @When[debug] is compiled only in the debug version, and the code modified by @When[!debug] is compiled only in the release version.

test is a conditional compilation identifier used to mark test code. The test code is usually in the same file as the common source code. The code modified by @When[test] is compiled only when the --test option is used. The code is excluded during normal build.

User-defined Condition Variables

You can define condition variables as required and use them in code to control code compilation. User-defined condition variables are essentially similar to built-in variables. The only difference is that the value of a user-defined condition variable is set by the user, while the value of a built-in variable is determined by the compiler based on the current compilation environment. User-defined condition variables comply with the following rules:

  • User-defined condition variables must be valid [Identifiers].
  • User-defined condition variables support only the equality and inequality comparison operations.
  • The user-defined compilation condition variable must be of the String type.

The following is an example of a user-defined condition variable:

//source.cj
@When[feature == "lion"]    // "feature" is user custom conditional variable.
func foo() {
    print("feature lion, ")
}
@When[platform == "dsp"]    // "platform" is user custom conditional variable.
func fee() {
    println("platform dsp")
}
main() {
    foo()
    fee()
}
Configuring User-defined Condition Variables

You can configure user-defined condition variables in two ways: through compilation options and configuration files. The compilation option --cfg allows a character string to be received to configure the value of a user-defined condition variable. The rules are as follows:

  • --cfg can be used multiple times.
  • Multiple conditional assignment statements can be used and are separated by commas (,).
  • The value of a condition variable cannot be specified multiple times.
  • The left side of the = operator must be a valid identifier.
  • The right side of the = operator must be a literal.

Use --cfg to configure the user-defined condition variables feature and platform.

cjc --cfg "feature = lion, platform = dsp" source.cj // ok
cjc --cfg "feature = lion" --cfg "platform = dsp" source.cj // ok
cjc --cfg "feature = lion, feature = dsp" source.cj // error

The .toml file is used as the configuration file of user-defined condition variables. The configuration file is named cfg.toml.

  • The configuration uses key-value pairs, with the key-value separator being =. Each key-value pair occupies a line.
  • Key names must be valid [Identifiers].
  • A key value is a literal value enclosed in double quotation marks.

By default, the current directory is used as the search path of the configuration file for conditional compilation. You can use --cfg to set the search path of the configuration file.

cjc --cfg "/home/cangjie/conditon/compile" source.cj // ok

Multi-condition Compilation

Multiple compilation conditions can be combined using && and ||. The precedence and associativity of the conditions are the same as those of logical expressions. For details, see [Logical Expressions]. Compilation conditions can be enclosed in parentheses. In this case, they are considered as separate computing units and are preferentially calculated.

@When[(backend == "cjnative" || os == "Linux") && cjc_version >= "0.40.1"]
func foo() {
    print("This is Multi-conditional compilation")
}

main() {
    foo() // Conditions for compiling source code: cjc_version greater than 0.40.1; compiler backend is cjnative or os is Linux.
    return 0
}