元编程

元编程允许把代码表示成可操作的数据对象,可以对其增、删、改、查。为此,仓颉编程语言提供了编译标记用于元编程。 编译标记分为两类:宏和注解。宏机制支持基于Tokens类型的编译期代码变换,支持将代码转为数据、拼接代码等基础操作。

quote 表达式和 Tokens 类型

仓颉通过quote表达式引用具体代码,表示成可操作的数据对象。quote表达式的语法定义为:

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 ')'
    ;

quote表达式的语法规则总结如下:

  • 通过关键字quote定义quote表达式。
  • quote表达式由()括起,内部引用的可以是代码(Token)、代码插值、宏调用表达式。
  • quote表达式为Tokens类型。Tokens是仓颉标准库提供的类型,是由词法单元(token)组成的序列。

上述语法定义中的quoteToken是指仓颉编译器支持的任意合法token注释、终结符等不被 Lexer 解析的 token 除外。但当换行符作为两条表达式分隔符时,不会被忽略,它会被解析成一个quoteToken。如下的例子,只有两条赋值表达式之间的换行符会被解析成quoteTokenquote表达式里的其他换行符都会被忽略。

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

var a = 3
var b = 4

)

不允许未经过代码插值的连续quote,关于代码插值,详见下一节。当quote表达式引用如下token时,需要进行转义:

  • quote表达式中不允许不匹配的小括号,但是通过\转义的小括号,不计入匹配规则。
  • @表示普通token,而非宏调用表达式时,需要通过\进行转义。
  • $表示普通token,而非代码插值时,需要通过\进行转义。

注:本章所提到的token(字母均小写),指仓颉Lexer解析出来的词法单元。

下面是一些以Tokens或代码插值为参数的quote表达式用例:

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`

quote表达式还可以引用宏调用表达式,例如:

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

仓颉编程语言提供Tokens类型表示代码从原始文本信息解析后的token序列。Tokens支持通过operator +将两个对象的token结果拼接到新对象。下面例子里a1a2基本等价。

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

代码插值

quote表达式里使用$作为代码插值运算符,运算符后面接表达式,表示将该表达式的值转成tokens。该运算符为一元前缀运算符,只能用在quote表达式中,优先级比其他运算符高。

代码插值可作用于所有仓颉合法表达式。代码插值本身不是表达式,不具备类型。

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

能被代码插值的表达式类型必须实现ToTokens 接口。ToTokens 接口形式如下:

interface ToTokens  {
    func toTokens(): Tokens
}
  • 仓颉中大部分的值类型:数值类型、Rune类型、Bool类型、String类型已有默认实现。
  • Tokens类型默认实现ToTokens接口,toTokens返回它自己。
  • 用户自定义数据结构须主动实现ToTokens接口。仓颉标准库提供了丰富的生成 Tokens的接口。例如,对于String类型的变量str,用户可以直接使用str.toTokens()获取str 对应的Tokens

quote 表达式求值规则

quote表达式对括号内的引用规则总结如下:

  • 代码插值:取被插值的表达式.toTokens()结果。
  • 普通tokens:取对应的Tokens结果。

quote表达式根据以上情况再按照表达式出现的顺序拼接成更大的Tokens

下面是一些quote表达式求值示例,注释里表示程序执行返回的Tokens结果。

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,can't 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,can't 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)

宏是实现元编程的重要技术。宏和函数的相同点在于都可以被调用,具有输入和输出。不同点在于宏代码在编译期进行展开,仓颉将宏调用表达式替换为展开后的代码。用户可通过宏编写代码模板(即生成代码的代码)。另外,宏提供自定义文法的能力。用户可基于宏灵活定义自己的 DSL 文法。最后,仓颉提供丰富的代码操作接口供用户方便地完成代码变换。

宏分为无属性宏和有属性宏两类。相对于无属性宏,有属性宏可根据属性不同对宏的输入进行相应的分析变换得到不同的输出结果。

宏定义

仓颉编程语言中,宏定义遵循以下语法规则:

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

macroWithoutAttrParam
    : '(' macroInputDecl ')'
    ;

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

macroInputDecl
    : identifier ':' identifier
    ;

macroAttrDecl
    : identifier ':' identifier
    ;

总结如下:

  • 通过关键字macro定义一个宏。

  • macro前需要有public修饰,表示对包外部可见。

  • macro后需要带有宏的名字。

  • 宏的参数由()括起。无属性宏固定 1 个Tokens类型参数对应宏的输入,有属性宏固定 2 个Tokens类型依次对应宏的属性和输入。

  • 可缺省的函数返回类型固定为Tokens

下面是无属性宏的示例。它包含了宏定义具备的所有要素:public表示对包外部可见;macro 关键字;foo 是宏的标识符;形参x和它的类型Tokens;返回值和输入同值、同类型。

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

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

下面是有属性宏的示例。和无属性宏相比,多一个 Tokens 类型的输入,宏定义体内可以进行更灵活的操作。

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

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

宏一旦被定义,宏名字不能再被赋值。另外,宏对参数类型、参数个数有严格要求。

宏调用

宏调用表达式遵循以下语法规则:

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)*
    ;

宏调用表达式规则总结如下:

  • 通过关键字@定义宏调用表达式。
  • 宏调用使用()括起来。括号里面可以是任意合法tokens,不可以是空。
  • 调用有属性宏时,宏属性使用[]括起来。里面可以是任意合法tokens,不可以是空。

当宏调用表达式引用如下token时,需要转义:

  • 宏调用表达式参数中不允许不匹配的小括号,例如@ABC(we have two (( and one ))。但通过\转义的小括号,不计入匹配规则。
  • 有属性宏的中括号内不允许不匹配的中括号,例如@ABC[we have two [[ and one ]]()。但通过\进行转义的中括号,不计入匹配规则。
  • 宏调用表达式参数中引用@时,需要通过\转义。

宏调用用于某些声明或表达式前面的时候可省略括号,其语义和不带括号的宏调用有所不同。带括号的宏调用,其参数可以是任意合法tokens;不带括号的宏调用,其参数必须是以下声明或表达式中的一种。

  • 函数声明
  • struct 声明
  • enum声明
  • enum constructor
  • 类声明
  • 静态初始化器
  • 类和 struct 构造函数和主构造函数
  • 接口声明
  • 变量声明
  • 属性声明
  • 扩展声明
  • 宏调用表达式

另外,对于不带括号的宏调用,其参数还必须满足:

  • 如果参数是声明,那么该宏调用只能出现在该声明允许出现的位置。

下面是无属性宏、有属性宏、不带括号的宏调用的示例。

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
}

宏调用作为一种表达式,它可以出现在任意表达式允许出现的位置上。另外宏调用还可以出现在如下声明可以出现的位置:函数声明、struct 声明、enum 声明、类声明、接口声明、变量声明(函数参数除外)、属性声明、扩展声明。

宏只在编译期可见。宏调用表达式在编译期会被宏展开后的代码替换。宏展开是指在代码编译时,宏定义会被执行,执行后的结果会被解析成仓颉语法树,替换宏调用表达式。替换后的节点经过语义分析之后拥有相应的类型信息,该类型可以认为是宏调用表达式的类型。例如上述例子中,当用户执行@Twice@Joint@MacroWithoutParens这些宏调用的时候,宏定义中的代码会被编译并执行,打印出Compiling the macro ...等字样,执行后的结果作为新的语法树节点,替换掉原来的宏调用表达式。

当宏调用出现在不同的上下文时,需要满足以下规则:

1. 如果宏调用出现在期望表达式或者声明的上下文里,那么宏调用会在编译期被展开成程序文本。

2. 如果宏调用出现在期望 token 序列的上下文里(作为宏调用表达式或quote表达式的参数),那么宏调用会在该 token 序列被求值的时刻调用,而且宏调用的返回值(token 序列)会被直接使用而不展开成程序文本。

宏作用域和包的导入

宏必须在源文件顶层定义,作用域是整个package

宏定义所在的package,需使用macro package来声明,被macro package 限定的包,仅允许宏定义声明对外可见,其他声明仅包内可见,其他声明被修饰为对外可见时将报错。

//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
}

宏的导入遵守仓颉通用的包导入规则。

需要特殊说明的是,macro package仅允许被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)))
}

在同一个package中,宏对于同名的约定和函数一致,属性宏和非属性宏的规则如下表所示:

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

嵌套宏和递归宏

宏允许嵌套调用其他宏。

宏调用表达式包含宏调用时,如@Outer @Inner(2+3),优先执行内层的宏,再执行外层的宏。内层宏返回的Tokens结果和其他Tokens拼接一起交给外层宏调用。内层宏和外层宏可以是不同的宏,也可以是同一个宏。

内层宏可以调用库函数 AssertParentContext 来保证内层宏调用一定嵌套在特定的外层宏调用中。如果内层宏调用这个函数时没有嵌套在给定的外层宏调用中,该函数将抛出一个错误。第二个函数 InsideParentContext 只在内层宏调用嵌套在给定的外层宏调用中时返回 true。

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

内层宏也可以通过发送键/值对的方式与外层宏通信。当内层宏执行时,通过调用标准库函数 SetItem 发送信息;随后,当外层宏执行时,调用标准库函数 GetChildMessages 接收每一个内层宏发送的信息(一组键/值对映射)。

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")
        // ...
    }
}

宏定义体包含宏调用时,如果宏调用出现在期望 token 序列的上下文里,则两个宏可以是不同的宏或同一个宏(即支持递归)。否则,被嵌套调用的宏不能和调用的宏相同。具体的使用可以参考下面两个例子。

下面例子里,嵌套调用的宏出现在quote表达式,则支持递归调用:

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
}

在这个例子中,当宏 A 未被外部调用时,宏 A 不会被执行(即使内部调用了自己),即不会打印Compiling the macro A ...if cond 是递归的终止条件。注意:宏递归和函数递归有类似的约束,需要有终止条件,否则会陷入死循环,导致编译无法停止。

下面例子里,嵌套调用的宏出现的地方不在quote表达式,则不支持递归调用:

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
}

总结下宏嵌套调用和递归调用规则:

  • 宏调用表达式允许嵌套调用。
  • 宏定义允许嵌套调用其他宏,但只允许在quote表达式中递归调用自己。

限制

  • 宏有条件地支持递归调用自己。具体情况请参考前面小节说明。

  • 除了宏递归调用外,宏定义和宏调用必须位于不同的package。宏调用的地方必须import宏定义所在的package,保证宏定义比宏调用点先编译。不支持宏的循环依赖导入。例如下面的用法是非法的:pkgA导入pkgBpkgB又导入pkgA,存在循环依赖导入。

    // ======= 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
    }
    
  • 宏允许嵌套调用其他宏。被调用的宏也要求定义点和调用点必须位于不同的package

内置编译标记

源码位置

仓颉提供了几个内置编译标记,用于在编译时获取源代码的位置。

  • @sourcePackage() 展开后是一个 String 类型的字面量,内容为当前宏所在的源码的包名
  • @sourceFile() 展开后是一个 String 类型的字面量,内容为当前宏所在的源码的文件名
  • @sourceLine() 展开后是一个 Int64 类型的字面量,内容为当前宏所在的源码的代码行
- `@sourcePackage()` is expanded to a `String` literal, which is the name of the current package - `@sourceFile()` is expanded to a `String` literal, which is the name of the current source file - `@sourceLine()` is expanded to an `Int64` literal, which is the source file line number

这几个编译标记可以在任意表达式内部使用,只要能符合类型检查规则即可。示例如下:

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

@Intrinsic 标记可用于修饰全局函数。@Intrinsic 修饰的函数是由编译器提供的特殊函数。

  1. @Intrinsic 修饰的函数不允许写函数体。
  2. @Intrinsic 修饰的函数,是编译器决定的一份名单,跟随标准库发布。名单之外的任何其它函数用 @Intrinsic 修饰都会报错。
  3. @Intrinsic 标记只在 std module 内可见,std module 外用户可以定义自己的叫做 Intrinsic 的宏或者注解,不会发生名字冲突。
  4. @Intrinsic 修饰的函数,不允许作为一等公民使用。

@Intrinsic directive can modify global functions. These functions are special functions provided by the compiler.

  1. Functions modified by @Intrinsic are not allowed to have function body.
  2. The list of @Intrinsic functions are determined by compiler, and released with std library. Other functions outside of this list can not be modified by @Intrinsic.
  3. @Intrinsic directive is only visible inside std module. User defined macro or annotation with the same name "Intrinsic" does not conflict with the one in std module.
  4. @Intrinsic modified function is not allowed to be used as a first-class citizen.

示例代码如下:

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

@FastNative

为了提升与 C 语言互操作的性能,仓颉提供 @FastNative 用于优化对 C 函数的调用。值得注意的是 @FastNative 只能用于 foreign 声明的函数。

开发者在使用 @FastNative 修饰 foreign 函数时,应确保对应的 C 函数满足以下两点要求:

  1. 函数的整体执行时间不宜太长。例如:不允许函数内部存在很大的循环;不允许函数内部产生阻塞行为,如,调用 sleepwait 等函数。
  2. 函数内部不能调用仓颉方法。

条件编译

条件编译是一种在程序代码中根据特定条件选择性地编译不同代码段的技术。条件编译的作用主要体现在以下几个方面:

  • 平台适应:支持根据当前的编译环境选择性地编译代码,用于实现跨平台的兼容性。
  • 功能选择:支持根据不同的需求选择性地启用或禁用某些功能,用于实现功能的灵活配置。例如,选择性地编译包含或排除某些功能的代码。
  • 调试支持:支持调试模式下编译相关代码,用于提高程序的性能和安全性。例如,在调试模式下编译调试信息或记录日志相关的代码,而在发布版本中将其排除。
  • 性能优化:支持根据预定义的条件选择性地编译代码,用于提高程序的性能。

条件编译遵循以下语法规则:

{{conditionalCompilationDefinition|pretty}}

条件编译语法规则总结如下:

  • 内置标记 @When 只能用于声明节点和导入节点。
  • 编译条件使用 [] 括起来,[] 内支持输入一组或多组编译条件(请参见[多条件编译])。

@When[...]作为一种内置编译标记,在导入前处理,由宏展开生成的代码中含有@When[...]会编译报错。

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

编译条件

编译条件主要由关系表达式或逻辑表达式组成,编译器根据编译条件评估获取一个布尔值,从而决定编译哪段代码。条件变量是编译器计算编译条件的基准,根据条件变量是否由编译器提供可将条件变量分为内置条件变量和用户自定义条件变量。同时,条件变量的作用域仅限于 @When[] 内, 其他作用域内使用未定义的条件变量标识符会触发未定义错误。

编译器内置条件变量

编译器为条件编译提供了五个内置条件变量: osbackendcjc_versiondebugtest 用于获取当前构建环境中对应的值,内置条件变量支持比较运算和逻辑运算。其中,osbackendcjc_version 三个变量支持比较运算,在比较运算中条件变量只能作为二元操作符的左操作数,二元操作符的右操作数必须是一个 String 类型的字面量值;逻辑运算仅适用于条件变量 debugtest

os 表示当前编译环境所在的操作系统。它用于实时获取编译器所在的具体操作系统类型,进而与目标操作系统的字面量值构成一个编译条件。如果想为 Windows 操作系统生成代码,可以将变量 os 与字面量值 "Windows" 进行比较判断。类似地,如果想为 Linux 生成代码,可以将 os 与字面量值 "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 表示当前编译器所使用的后端。它用于实时获取编译器当前所使用的后端类型,进而与目标后端的字面量值构成一个编译条件。如果想为 cjnative 后端编译代码,可以将 backend 与字面量值 "cjnative" 进行比较判断。

支持的后端有:cjnativecjnative-x86cjnative-x86_64cjnative-armcjnative-aarch64cjvmcjvm-x86cjvm-x86_64cjvm-armcjvm-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 表示当前编译器的版本号。版本号是一个 String 类型的字面量值,采用 X.Y.Z 格式,其中 X、Y 和 Z 为非负的整数且不允许包含前导零,例如 "0.18.8"。它用于实时获取编译器当前的版本号,进而与目标版本号比较,确保编译器能够基于特定的版本编译代码。共支持六种类型 ==!=><>=<= 的比较操作。条件的结果由从左到右依次比较这些字段时的第一个差异决定。例如,0.18.8 < 0.18.110.18.8 == 0.18.8

debug 是一个条件编译标识符,表示当前编译的代码是否是一个调试版本。用于在编译代码时进行调试和发布版本之间的切换。使用 @When[debug] 修饰的代码只会在调试版本中编译; 使用 @When[!debug] 修饰的代码只会在发布版本中编译。

test 是一个条件编译标识符,用于标记测试代码。测试代码通常与普通源码位于相同的文件中,使用 @When[test] 修饰的代码,只会在使用 --test 选项时才会被编译,正常的构建时该部分代码将会被排除。

用户自定义条件变量

用户自定义条件变量是用户根据自己的需要定义条件变量,进而在代码中使用这些条件变量来控制代码的编译。用户自定义条件变量与内置变量本质上相似,唯一不同点是用户自定义条件变量的值由用户自身配置设定,而内置变量的值由编译器根据当前编译环境决定。用户自定义条件变量遵循如下规则:

  • 自定义条件变量必须是一个合法的[标识符]
  • 自定义条件变量仅支持等于和不等于两种关系运算符的比较运算。
  • 用户自定义的编译条件变量必须是 String 类型。

一个用户自定义条件变量的示例如下:

//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()
}
配置自定义条件变量

配置自定义条件变量的方式有两种:编译选项和配置文件。编译选项 --cfg 允许接收字符串用于配置自定义条件变量的值,具体规则如下:

  • 允许多次使用 --cfg
  • 允许使用多个条件赋值语句,用逗号 (,) 分隔。
  • 不允许多次指定条件变量的值。
  • = 操作符的左边必须是一个合法的标识符。
  • = 操作符的右边是一个字面量。

使用 --cfg 对用户自定义条件变量 featureplatform 进行配置。

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

采用 .toml 文件格式作为用户自定条件变量的配置文件,配置文件命名为 cfg.toml

  • 采用键值对的方式对自定义条件变量配置,键值对由 = 分隔,每个键值对单独占一行。
  • 健名是一个合法的[标识符]。
  • 键值是一个双引号括起来的字面量值。

默认以当前目录作为条件编译配置文件的搜索路径,支持使用 --cfg 设置配置文件搜索路径。

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

多条件编译

允许使用逻辑与(&&)、逻辑或(||)组合多个编译条件,其优先级和结合性与逻辑表达式一致(请参见[逻辑表达式])。允许使用圆括号将编译条件括起来,圆括号括起来的编译条件被视作一个单独的计算单元被优先计算。

@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
}