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 thequote
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 theTokens
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 commontoken
instead of a macro call expression,\
needs to be used for escape. - When
$
indicates a commontoken
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, andString
type, have default implementations. - The
Tokens
type implements theToTokens
interface by default, and thetoTokens
function returns this instance. - The user-defined data structure must proactively implement the
ToTokens
interface. The Cangjie standard library provides various interfaces for generatingTokens
. For example, for the variablestr
of theString
type, you can directly usestr.toTokens()
to obtain theTokens
corresponding tostr
.
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 correspondingTokens
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 bypublic
, indicating that themacro
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 theTokens
type corresponds to the input of the macro. For an attribute macro, two parameters of theTokens
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 validtokens
, 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 validtokens
, 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
declarationenum
declarationenum 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:
-
If a macro call appears in the context of the expected expression or declaration, the macro call is expanded into program text during compilation.
-
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 package | attribute macros | non-attribute macros |
---|---|---|
attribute macros | NO | YES |
non-attribute macros | YES | NO |
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 mustimport
thepackage
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
importspkgB
, andpkgB
importspkgA
, 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 theString
type, representing the package name of the source code where the current macro is located. - After
@sourceFile()
is expanded, it becomes a literal of theString
type, representing the file name of the source code where the current macro is located. - After
@sourceLine()
is expanded, it becomes a literal of theInt64
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.
- Functions modified by @Intrinsic cannot have function bodies.
- 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.
- 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. - 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:
- 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
andwait
. - 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
}