仓颉语法-宏

244 阅读10分钟

  1. 宏和函数类似,只不过它操作的是语法树,输入语法树输出修改过的语法树。这个输出的语法树会被插入到程序中

  2. 仓颉目前提供的是过程宏,它在词法分析阶段做宏展开。后续还会有Late-stage宏、模板宏等

  3. 仓颉的过程宏接受一个 token 序列作为输入,对其进行处理和变换后,输出另一个 token 序列

  4. 宏操作的基本类型是 Tokens,代表一个程序片段。Tokens 是由多个 Token 组成的序列,每个 Token 包含它的类型、内容和位置信息

    • 通过 Token 数组构造 Tokens
    • Token 的类型取值为 enum TokenKind 中的元素
    • 通过提供 TokenKind 和(对于标识符和字面量)Token 的字符串,可以直接构造任何 Token
  5. 使用Token和Tokens构造Tokens比较麻烦,可以使用quote 表达式来从代码模版构造 Tokens

    • 在 quote 中可以使用 $(...) 来插入上下文中的表达式。

    • 插入的表达式的类型需要支持被转换为 Tokens(具体来说,实现了 ToTokens 接口),包括以下方面

    • 所有的节点类型

      • 仓颉编译过程中,首先通过词法分析将代码转换成 Tokens,然后对 Tokens 进行语法解析,得到一个语法树

      • 每个语法树的节点可能是一个表达式、声明、类型、模式等

      • 仓颉 ast 库提供了每种节点对应的类,它们之间具有适当的继承关系

      • Node:所有语法节点的父类

      • TypeNode:所有类型节点的父类

      • Expr:所有表达式节点的父类

      • Decl:所有声明节点的父类

      • Pattern:所有模式节点的父类

      • 有两种方式解析节点

        1. 通过函数,有用于解析表达式的函数(parseExpr,parseExprFragment)和用于解析声明的函数(parseDecl,parseDeclFragment)两种
        2. 通过对应节点类型的构造器,将输入的 Tokens 解析为相应类型的节点
    • Token 和 Tokens 类型

    • 所有基础数据类型:整数、浮点数、Bool、Rune和String

    • Array<T> 和 ArrayList<T>,这里对 T 的类型有限制,并根据 T 的类型不同,输出不同的分隔符

    • quote 表达式中不允许出现不匹配的小括号,通过\转义的小括号,不计入小括号的匹配规则。

    • 当 $ 表示一个普通 Token,而非用于代码插值时,需要通过\进行转义。

    • 除以上情况外,quote 表达式中出现\会编译报错

  6. 仓颉宏可以分为非属性宏属性宏

    • 非属性宏 只接受被转换的代码,不接受其他参数(属性)

      • 定义格式:public macro 宏名称(input: Tokens): Tokens

      • 调用格式:@宏名称(输入tokens)

        • 宏调用使用 () 括起来。括号里面可以是任意合法 Tokens,也可以是空

        • 当宏作用于声明时,一般可以省略括

        • 输入的内容有要求

          1. 必须是由合法的 Token 组成的序列
          2. 若存在不匹配的小括号则必须使用转义符号 "" 对其进行转义
          3. 若希望 "@" 作为输入的 Token 则必须使用转义符号 "" 对其进行转义
    • 属性宏 的定义比非属性宏多增加一个 Tokens 类型的输入,这个参数在第一个位置

      • 定义格式:public macro 宏名称(args: Tokens, input: Tokens): Tokens
      • 调用格式:@宏名称[参数tokens](输入tokens)
    • 属性宏和非属性宏,可以修饰同样的AST。只是属性宏对传入参数做了增强。中括号和小括号的输入内容的规则也和非属性宏一样

    • 宏的定义和调用的类型要保持一致

      • 如果宏定义有两个入参,即为属性宏定义,调用时必须加上 [],且内容可以为空;
      • 如果宏定义有一个入参,即为非属性宏定义,调用时不能使用 []
  7. 有条件地支持在宏定义和宏调用中嵌套宏调用

    • 可以在宏定义中调用另一个宏
    • 可以在宏调用的类型中,给其内部变量调用另一个宏
    • 内层宏可以通过assertParentContext断言自己处于另一个宏中,也可以使用InsideParentContext检查自己是否处于另一个宏中
    • 内层宏可以通过setItem给外层宏传递消息,外层宏通过getChildMessages获取内层宏传递的消息。因为内存宏可能有多个,所以返回的是个数组
  8. 编译器对宏的定义和宏的调用有一些要求

    • 宏的定义与宏的调用不允许在同一包里
    • 宏定义必须使用 public 修饰
    • 宏定义的包必须首先被编译(--compile-macro),然后再编译宏调用的包
    • 在宏调用的包中,不允许出现宏的定义
    • 可以在编译宏调用文件时添加 --parallel-macro-expansion 选项,启用并行宏展开的能力
    • 可以使用自定义报错接口 diagReport对宏定义过程中的错误进行编译器提示。支持warning和error两类错误信息
    • 可以使用--debug-macro选项让编译器生成宏展开的代码,一般以.macrocall文件结尾
  9. 宏包的定义和导出有一些规则

    • 宏定义包的声明以 macro package 开头
    • 宏定义包中仅允许宏定义对外可见(public),其他声明包内可见(internal)
    • 在 macro package 中允许其它 macro package 和非 macro package 符号被重导出
    • 在非 macro package 中仅允许非 macro package 符号被重导出
    • 重导出语法: public cj_exercise.macros.stringify
// 宏必须声明在独立的包中(不能和其他 public 函数一起)
// 含有宏的包使用 macro package 来声明
macro package cj_exercise.macrodefines

import cj_exercise.anothermacro.*
import std.ast.*

// 原始tokens
public macro orgTokens(input: Tokens): Tokens {
    input
}

// 宏类似函数,只不过它的输入和输出都是Tokens。 宏调用的时候需要在前面加上@

// 宏操作的基本类型是 Tokens,代表一个程序片段
// Tokens 由若干个 Token 组成,每个 Token 可以理解为用户可操作的词法单元
// 每个 Token 包含它的类型、内容和位置信息
// 通过提供 TokenKind 和(对于标识符和字面量)Token 的字符串,可以直接构造任何 Token

public macro dprint(input: Tokens): Tokens {
    // 将输入的程序片段转化为字符串
    let inputStr = input.toString()
    // quote 表达式是用于构造 Tokens 的一种表达式,它将括号内的程序片段转换为 Tokens
    let result = quote(
        print($(inputStr) + " = ")
        println($(input))
    )
    return result
}

public macro tokensDemo(input: Tokens): Tokens {
    let tks = Tokens(
        Array<Token>(
            [
                Token(TokenKind.IDENTIFIER, '1'),
                Token(TokenKind.ADD),
                Token(TokenKind.INTEGER_LITERAL, '2')
            ]
        )
    )
    println('tks size ${tks.size}')
    println('tks[0] ${tks.get(0).value}')
    println('tks[0] ${tks[0].value}')
    tks.dump()
    println('tks.toString ${tks.toString()}')
    return input
}

public macro quoteDemo(input: Tokens): Tokens {
    // quote 中可以使用 $(...) 来插入上下文中的表达式。插入的表达式的类型需要支持被转换为 Tokens(具体来说,实现了 ToTokens 接口)
    let intList = Array<Int>([1, 2, 3, 4, 5])
    let float: Float64 = 1.0
    let str: String = '仓颉'
    let tokens = quote(
        arr = $(intList)
        x = $(float)
        s = $(str)
    )
    println(tokens)
    return input
}

public macro tokenDemo(input: Tokens): Tokens {
    // Node:所有语法节点的父类
    // TypeNode:所有类型节点的父类
    // Expr:所有表达式节点的父类
    // Decl:所有声明节点的父类
    // Pattern:所有模式节点的父类

    // 使用 解析表达式和声明的 函数
    // parseExpr(input: Tokens): Expr:将输入的 Tokens 解析为表达式
    // parseExprFragment(input: Tokens, startFrom!: Int64 = 0): (Expr, Int64):将输入 Tokens 的一个片段解析为表达式,片段从 startFrom 索引开始,解析可能只消耗从索引 startFrom 开始的片段的一部分,并返回第一个未被消耗的 Token 的索引(如果消耗了整个片段,返回值为 input.size)
    // parseDecl(input: Tokens, astKind!: String = ""):将输入的 Tokens 解析为声明,astKind 为额外的设置,具体请见《仓颉编程语言库 API》文档。
    // parseDeclFragment(input: Tokens, startFrom!: Int64 = 0): (Decl, Int64):将输入 Tokens 的一个片段解析为声明,startFrom 参数和返回索引的含义和 parseExpr 相同
    let tks1 = quote(a + b)
    let binExpr1 = parseExpr(tks1)
    println("binExpr1 is BinaryExpr: ${binExpr1 is BinaryExpr}")

    let tks2 = quote(a + b, x + y)
    let (binExpr2, mid) = parseExprFragment(tks2)
    let (binExpr3, end) = parseExprFragment(tks2, startFrom: mid + 1) // 跳过逗号
    println("size = ${tks2.size}, mid = ${mid}, end = ${end}")

    let tks3 = quote(
        func f1(x: Int64) { return x + 1 }
    )
    let funcDecl1 = parseDecl(tks3)
    println("funcDecl1 is FuncDecl: ${funcDecl1 is FuncDecl}")

    let tks4 = quote(
        func f1(x: Int64) { return x + 1 }
        func f2(x: Int64) { return x + 2 }
    )
    let (funcDecl2, mid2) = parseDeclFragment(tks4)
    let (funcDecl3, end2) = parseDeclFragment(tks4, startFrom: mid2)
    println("size = ${tks4.size}, mid = ${mid2}, end = ${end2}")

    // 使用构造函数进行解析
    // let binExpr = BinaryExpr(quote(a + b))
    // let funcDecl = FuncDecl(quote(func f1(x: Int64) { return x + 1 }))

    // Token 的每个组成部分都是 public mut prop
    // let binExpr = BinaryExpr(quote(x * y))
    // binExpr.leftExpr = BinaryExpr(quote(a + b))
    // // ast 库具备在输出语法树时自动添加括号的功能
    // println(binExpr.toTokens().toString())

    // binExpr.op = Token(TokenKind.ADD)
    // println(binExpr.toTokens())

    let funcDecl = FuncDecl(quote(func f1(x: Int64) { x + 1 }))
    funcDecl.identifier = Token(TokenKind.IDENTIFIER, "foo")
    println("Number of parameters: ${funcDecl.funcParams.size}")

    funcDecl.funcParams[0].identifier = Token(TokenKind.IDENTIFIER, "a")
    println("Number of nodes in body: ${funcDecl.block.nodes.size}")

    let binExpr = (funcDecl.block.nodes[0] as BinaryExpr).getOrThrow()
    binExpr.leftExpr = parseExpr(quote(a))
    println(funcDecl.toTokens())

    return input
}

public macro quoteCorrectDemo(input: Tokens): Tokens {
    var binExpr1 = BinaryExpr(quote(x + y))
    var binExpr2 = BinaryExpr(quote($(binExpr1) * z)) // 错误:得到 x + y * z
    println("binExpr2: ${binExpr2.toTokens()}")
    println("binExpr2.leftExpr: ${binExpr2.leftExpr.toTokens()}")
    println("binExpr2.rightExpr: ${binExpr2.rightExpr.toTokens()}")
    var binExpr3 = BinaryExpr(quote(($(binExpr1)) * z)) // 正确:得到 (x + y) * z
    println("binExpr3: ${binExpr3.toTokens()}")
    return input
}
// 使用宏之前,需要先编译宏 cjc ./src/macrodefines/*.cj --compile-macro

// 非属性宏
// 非属性宏只接受被转换的代码,不接受其他参数(属性)
// 当宏作用于声明时,一般可以省略括号
public macro noneAttributeDemo(input: Tokens): Tokens {
    // 宏展开过程作用于仓颉语法树,宏展开后,编译器会继续进行后续的编译过程

    // 在编译 macro_call.cj 的期间输出,即对宏定义求值
    println("在宏内部")
    return input
}

// 属性宏
// 属性宏的定义会增加一个 Tokens 类型的输入
// 属性宏调用时新增的入参 attrTokens 通过 [] 传入
public macro attributeDemo(attr: Tokens, input: Tokens): Tokens {
    println('${attr} ${input}')
    return attr + input
}

// 仓颉语言不支持宏定义的嵌套;有条件地支持在宏定义和宏调用中嵌套宏调用
// 宏定义中嵌套宏调用
public macro embedInvokeDemo(input: Tokens): Tokens {
    let v = parseDecl(input)
    @getIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

// 宏调用嵌套宏调用
// 将嵌套内层的宏(addToMul 和 Bar)展开后,再去展开外层的宏(Foo)。
// 允许出现多层宏嵌套,代码变换的规则总是由内向外去依次展开宏
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)))
}

// 嵌套宏之间可以传递消息
// 内层宏可以调用库函数 assertParentContext 来保证内层宏调用一定嵌套在特定的外层宏调用中
// 库函数 InsideParentContext 同样用于检查内层宏调用是否嵌套在特定的外层宏调用中,该函数返回一个布尔值
public macro Outer(input: Tokens): Tokens {
    // Outer 宏通过 getChildMessages 函数接收到 Inner 发送的一组信息对象(Outer 中可以调用多次 Inner)
    let messages = getChildMessages("Inner")
    let getTotalFunc = quote(public func getCnt() {
                       )
    for (m in messages) {
        // 通过该信息对象的 getString 函数接收对应的值
        let identName = m.getString("identifierName")
        // let value = m.getString("key")            // 接收多组消息
        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.append(funcDecl)
    return decl.toTokens()
}

public macro Inner(input: Tokens): Tokens {
    // assertParentContext("Outer")
    if (insideParentContext('Outer')) {
        println('Inner宏处于Outer宏内部')
    } else {
        println('Inner宏未处于Outer宏内部')
    }

    // 内层宏也可以通过发送键/值对的方式与外层宏通信
    // 当内层宏执行时,通过调用标准库函数 setItem 向外层宏发送信息;
    // 随后,当外层宏执行时,调用标准库函数 getChildMessages 接收每一个内层宏发送的信息(一组键/值对映射
    let decl = parseDecl(input)
    // 内层宏 Inner 通过 setItem 向外层宏发送信息
    setItem("identifierName", decl.identifier.value)
    return input
}

public macro diagDemo(input: Tokens): Tokens {
    for (i in 0..input.size) {
        println(input[i].toTokens())
        if (input[i].kind == IDENTIFIER) {
            diagReport(DiagReportLevel.ERROR, input[i..(i + 1)], "diagDemo修饰的表达式中不允许包含标识符", "不合法的标志符")
        }
    }
    return input
}

// 可使用的宏
macro package cj_exercise.usablemacro

import std.ast.{Token, Tokens, ToTokens, TokenKind, Expr, LitConstExpr, parseExpr, parseExprFragment, diagReport, 
    DiagReportLevel, FuncDecl}
import std.convert.Parsable
import std.collection.ArrayList

// @power[10](n)
public macro power(attrib: Tokens, input: Tokens) {
    let attribExpr = parseExpr(attrib)
    // 确认输入的属性 attrib 是一个整数字面量,否则通过 diagReport 报错
    if (let Some(litExpr) <- attribExpr as LitConstExpr) {
        let lit = litExpr.literal
        if (lit.kind != TokenKind.INTEGER_LITERAL) {
            diagReport(DiagReportLevel.ERROR, attrib, "属性必须是整型字面量", "期望整型字面量")
        }
        // 将这个字面量解析为整数 n
        var n = Int64.parse(lit.value)
        // 设 result 为当前积累的输出代码,首先添加 var _power_vn 的声明
        // 这里)换行,是为了最终生成的文件代码换行
        var result = quote(var _power_vn = $(input)
        )
        // 布尔变量 flag 表示 var _power_result 是否已经被初始化
        var flag = false
        while (n > 0) {
            // 奇数和偶数都会走进这里。所以_power_result一定有
            // 奇数 首次就会进这里
            // 偶数 倒数第二次得到的是1,也会走到这里
            // 如果整除2过程中 得到计数,就会走进这里
            if (n % 2 == 1) {
                if (!flag) {
                    // 代码 var _power_result = _power_vn
                    result += quote(var _power_result = _power_vn
                    )
                    flag = true
                } else {
                    // 代码 _power_result *= _power_vn
                    result += quote(_power_result *= _power_vn
                    )
                }
            }
            n /= 2
            if (n > 0) {
                // 平方
                result += quote(_power_vn *= _power_vn
                )
            }
        }
        // 添加返回 _power_result 的代码
        result += quote(_power_result)
        return result
    } else {
        diagReport(DiagReportLevel.ERROR, attrib, "属性必须是整型字面量", "期望整型字面量")
    }
    return input
}

// @Memoize[true]
public macro Memoize(attrib: Tokens, input: Tokens) {
    // 对属性和输入做合法性检查。属性必须是布尔字面量
    if (attrib.size != 1 || attrib[0].kind != TokenKind.BOOL_LITERAL) {
        diagReport(DiagReportLevel.ERROR, attrib, "属性必须是bool字面量(true 或 false)",
            "期望 bool字面量(true 或 false)")
    }

    let memoized = (attrib[0].value == "true")
    // 如果为 false 则直接返回输入
    if (!memoized) {
        return input
    }

    let fd = FuncDecl(input)
    // 检查输入必须能够解析为函数声明(FuncDecl),并且必须包含正好一个参数
    if (fd.funcParams.size != 1) {
        diagReport(DiagReportLevel.ERROR, fd.lParen + fd.funcParams.toTokens() + fd.rParen, "修饰的函数必须只有一个参数",
            "期望函数只有一个参数")
    }
    // HashMap变量名。  _memoize_原始func名字_map
    let memoMap = Token(TokenKind.IDENTIFIER, "_memoize_" + fd.identifier.value + "_map")
    // 函数参数
    let arg1 = fd.funcParams[0]

    return quote(
        // 创建HashMap, Key为func参数类型,Value为func返回类型
        var $(memoMap) = HashMap<$(arg1.paramType), $(fd.declType)>()
        // 重新实现原有函数。原有函数名称、原有函数参数、原有函数返回值类型
        func $(fd.identifier)($(arg1)): $(fd.declType) {
            // 取参数作为key所对应的值,如果有就返回
            if ($(memoMap).contains($(arg1.identifier))) {
                return $(memoMap).get($(arg1.identifier)).getOrThrow()
            }
            // 将原有函数的实现包装到闭包中,立即执行
            let _memoize_eval_result = { => 
            $(fd.block.nodes) 
            }()
            // 将执行结果保存到HashMap中
            $(memoMap).put($(arg1.identifier), _memoize_eval_result)
            return _memoize_eval_result
        }
    )
}

// @dprint(x, y, x + y)
public macro dprint(input: Tokens) {
    let exprs = ArrayList<Expr>()
    // 变量 index 保存当前解析的位置
    var index: Int64 = 0
    // 使用 while 循环从索引 0 开始依次解析每个表达式
    while (true) {
        // 每次调用 parseExprFragment 时,从当前位置开始,并返回解析后的位置(以及解析得到的表达式)
        let (expr, nextIndex) = parseExprFragment(input, startFrom: index)
        exprs.append(expr)
        // 如果解析后的位置到达了输入的结尾,则退出循环
        if (nextIndex == input.size) {
            break
        }
        // 否则检查到达的位置是否是一个逗号,如果不是逗号,报错并退出
        // 如果是逗号,跳过这个逗号并开始下一轮的解析
        if (input[nextIndex].kind != TokenKind.COMMA) {
            diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex + 1], "必须是逗号分割的多个表达式", "期望逗号")
        }
        index = nextIndex + 1 // 跳过逗号
    }
    // 在得到表达式的列表后,依次输出每个表达式
    let result = quote()
    for (expr in exprs) {
        result.append(
            quote(
            print($(expr.toTokens().toString()) + " = ")
            println($(expr))
        ))
    }
    return result
}

// @linq(from x in 1..=10 where x % 2 == 1 select x * x)
public macro linq(input: Tokens) {
    // from <variable> in <list> where <condition> select <expression>
    // variable 是一个标识符,list、condition 和 expression 都是表达式
    let syntaxMsg = "Syntax is \"from <attrib> in <table> where <cond> select <expr>\""
    if (input.size == 0 || input[0].value != "from") {
        diagReport(DiagReportLevel.ERROR, input[0..1], syntaxMsg, "期望 from 关键字")
    }
    if (input.size <= 1 || input[1].kind != TokenKind.IDENTIFIER) {
        diagReport(DiagReportLevel.ERROR, input[1..2], syntaxMsg, "期望一个标识符")
    }
    let attribute = input[1]
    if (input.size <= 2 || input[2].value != "in") {
        diagReport(DiagReportLevel.ERROR, input[2..3], syntaxMsg, "期望 in 关键字")
    }
    var index: Int64 = 3
    let (table, nextIndex) = parseExprFragment(input, startFrom: index)
    if (nextIndex == input.size || input[nextIndex].value != "where") {
        diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex + 1], syntaxMsg, "期望 where 关键字")
    }
    index = nextIndex + 1 // 跳过where
    let (cond, nextIndex2) = parseExprFragment(input, startFrom: index)
    if (nextIndex2 == input.size || input[nextIndex2].value != "select") {
        diagReport(DiagReportLevel.ERROR, input[nextIndex2..nextIndex2 + 1], syntaxMsg, "期望 select 关键字")
    }
    index = nextIndex2 + 1 // 跳过select
    let (expr, nextIndex3) = parseExprFragment(input, startFrom: index)

    return quote(
        for ($(attribute) in $(table)) {
            if ($(cond)) {
                println($(expr))
            }
        }
    )
}

public macro stringify(input: Tokens): Tokens {
    let inputStr = input.toString()
    let result = quote(
        { =>
            print($(inputStr) + " = ")
            println($(input))
            $(input)
        }())

    return result
}

public macro stringify2(input: Tokens): Tokens {
    let inputStr = input.toString()
    let ret = input
    return quote(
        println($(inputStr) + ' = ' + ($(input)).toString())
    )
}

参考资料

  1. 仓颉编程语言开发指南 developer.huawei.com/consumer/cn…
  2. 仓颉编程语言白皮书 developer.huawei.com/consumer/cn…
  3. 仓颉编程语言语言规约 developer.huawei.com/consumer/cn…
  4. 白皮书中的宏 developer.huawei.com/consumer/cn…