前言
本章对应官方教程第6章。在之前的教程中我们为Kaleidoscope实现了一些基本的功能,但现在它有个大问题,那就是没有更多的操作符。所以本章内容展示了如何为让Kaleidoscope支持自定义操作符。
教程如下:
开始
既然我们要支持自定义运算符,那么肯定是需要在函数的处理上提供支持。因为我们需要支持一元运算符和二元运算符,所以需要定义两个token
。他们分别是unary
,用于扩展一元运算符。和binary
,用于扩展二元运算符。
我们举两个例子说明用户自定义操作符的用法。
用于扩展一元运算符的函数写法,扩展了"非"
操作符。
def unary ! (v)
if v then
0
else
1;
用于扩展二元运算符的函数写法,扩展了"或"
操作符。
def binary | 5 (LHS RHS)
if LHS then
1
else if RHS then
1
else
0;
Token解析
需要做的第一件还是完善token
的解析。
enum Token {
...
case binary
case unary
...
}
else if identifierStr == "binary" {
currentToken = CurrentToken(token: .binary, val: "binary")
} else if identifierStr == "unary" {
currentToken = CurrentToken(token: .unary, val: "unary")
}
扩展AST Node
既然我们是用def
去自定义操作符,那么我们肯定需要改变之前的AST数据结构。
首先我们来看PrototypeAST
的改变。
enum PrototypeKind: Int {
case identifier
case unary
case binary
}
class PrototypeAST {
let name: String
let args: [String]
let isOperator: Bool//是否是运算符定义函数
let precedence: UInt//运算符优先级
//是否是二元运算符定义函数
private var isBinaryOp: Bool {
return isOperator && args.count == 2
}
//是否是一元运算符定义函数
private var isUnaryOp: Bool {
return isOperator && args.count == 1
}
//运算符定义名字
var operatorName: String? {
guard isUnaryOp || isOperator else {
return nil
}
return String(Array(name).last!)
}
init(_ name: String, _ args: [String], _ isOperator: Bool = false, _ precedence: UInt = 0) {
self.name = name
self.args = args
self.isOperator = isOperator
self.precedence = precedence
}
func codeGen() -> Function {
let doubles = Array(repeating: FloatType.double, count: args.count)
let ft = FunctionType(doubles, FloatType.double, variadic: false)
var f: Function = theModule.addFunction(name, type: ft)
//这其实是默认linkage,这里为了和官方教程保持一致,显示的写一下
f.linkage = .external
//设置参数名
var p = f.firstParameter
for i in 0..<args.count {
p?.name = args[i]
p = p?.next()
}
return f
}
}
紧接着我们需要改变parsePrototype()
方法。
/// 解析函数原型
///
/// - Returns: 函数原型AST
func parsePrototype() -> PrototypeAST {
var fnName: String
let kind: PrototypeKind
var binaryPrecedence: UInt = 30
switch lexer.currentToken!.token {
case .identifier:
fnName = lexer.currentToken!.val
kind = .identifier
lexer.nextToken()
break
case .binary:
lexer.nextToken()
//不是ASCII字符不可以使用
guard Array(lexer.currentToken!.val)[0].isASCII else {
fatalError("Expected binary operator.")
}
fnName = "binary"
fnName += lexer.currentToken!.val
kind = .binary
lexer.nextToken()
//解析二元表达式优先级
if lexer.currentToken!.token == .number {
let num = UInt(lexer.currentToken!.val)!
if num < 1 || num > 100 {
fatalError("Invalid precedence: must be 1...100.")
}
binaryPrecedence = num
lexer.nextToken()
}
break
default:
fatalError("Expected function name in prototype.")
}
if lexer.currentToken!.val != "(" {
fatalError("Expected '(' in prototype")
}
lexer.nextToken()
var argNames: [String] = []
while lexer.currentToken!.token == .identifier {
argNames.append(lexer.currentToken!.val)
lexer.nextToken()
}
if lexer.currentToken!.val != ")" {
fatalError("Expected ')' in prototype")
}
lexer.nextToken()
if kind != .identifier && kind.rawValue != argNames.count {
fatalError("Invalid number of operands for operator.")
}
return PrototypeAST(fnName, argNames, kind.rawValue != 0, binaryPrecedence)
}
其实这里的变化无非就是需要多解析一元表达式和二元表达式这两种情况而已。
解析完AST之后我们还需要支持代码生成,所以我们在BinaryExprAST
中的codeGen()
方法里支持一下。
func codeGen() -> IRValue? {
...
//如果走到这里了,说明这个运算符是用户自己定义的
let fn = getFunction(named: "binary" + op)
guard fn != nil else {
fatalError("\(String(describing: fn)) binary operator not found!")
}
let ops = [l!, r!]
return builder.buildCall(fn!, args: ops, name: "binaryOp")
}
在FunctionAST
的codeGen()
方法里把自定义操作符放在全局操作符表中。
func codeGen() -> Function? {
...
//如果是操作符,把他放在全局的操作符表中
if proto.isOperator {
BinOpPrecedence[proto.operatorName!] = proto.precedence
}
...
}
支持一元运算符
由于之前在Kaleidoscope中不支持一元运算符,所以我们需要新增一个AST Node UnaryExprAST
。
class UnaryExprAST: ExprAST {
let op: String
let operand: ExprAST
init(_ op: String, _ operand: ExprAST) {
self.op = op
self.operand = operand
}
}
接着按照惯例我们在Parser
中添加解析逻辑。
/// 解析一元表达式
///
/// - Returns: AST
private func parseUnaryExpr() -> ExprAST? {
//当前token不是操作符,那就是基本类型
if lexer.currentToken!.val == "(" ||
lexer.currentToken!.val == "," ||
Array(lexer.currentToken!.val)[0].isLetter ||
Array(lexer.currentToken!.val)[0].isNumber {
return parsePrimary()
}
let op = lexer.currentToken!.val
lexer.nextToken()
//这里需要递归的处理一元运算符,比如说 !! x,这里有两个!!需要处理
if let operand = parseUnaryExpr() {
return UnaryExprAST(op, operand)
}
return nil
}
为了调用这个方法,我们需要改变之前调用parsePrimary()
方法的地方改为调用parseUnaryExpr()
方法。
private func parseBinOpRHS(_ exprPrec: Int, _ lhs: inout ExprAST) -> ExprAST? {
while true {
...
//解析二元运算符右边的表达式
var rhs = parseUnaryExpr()
guard rhs != nil else {
return nil
}
...
}
func parseExpression() -> ExprAST? {
var lhs = parseUnaryExpr()
guard lhs != nil else {
return nil
}
return parseBinOpRHS(0, &lhs!)
}
接着我们为parsePrototype()
方法添加解析支持。
...
case .unary:
lexer.nextToken()
guard Array(lexer.currentToken!.val)[0].isASCII else {
fatalError("Expected unary operator.")
}
fnName = "unary"
fnName += lexer.currentToken!.val
kind = .unary
lexer.nextToken()
break
...
最后我们实现UnaryExprAST
的codeGen()
方法即可。
func codeGen() -> IRValue? {
let operandVal = operand.codeGen()
guard operandVal != nil else {
return nil
}
let fn = getFunction(named: "unary" + op)
guard fn != nil else {
fatalError("Unknow unary operator.")
}
return builder.buildCall(fn!, args: [operandVal!], name: "unaryOp")
}
测试
一元运算符
//输入
def unary ! (v) if v then 0 else 1;
def testfunc(x) !x;
testfunc(1);
//输出
Read function definition:
define i64 @"unary!"(i64 %v) {
entry:
%ifCond = icmp eq i64 %v, 0
%. = select i1 %ifCond, i64 1, i64 0
ret i64 %.
}
Read function definition:
define i64 @testfunc(i64 %x) {
entry:
%unaryOp = call i64 @"unary!"(i64 %x)
ret i64 %unaryOp
}
Read top-level expression:
define i64 @__anon_expr() {
entry:
%call = call i64 @testfunc(i64 1)
ret i64 %call
}
Evaluated to 0.
二元运算符
//输入
def binary > 10 (LHS RHS) RHS < LHS;
def testfunc(v) if v > 10 then 1 else 0;
testfunc(1);
//输出
Read function definition:
define i64 @"binary>"(i64 %LHS, i64 %RHS) {
entry:
%boolCmp = icmp slt i64 %RHS, %LHS
%0 = sext i1 %boolCmp to i64
ret i64 %0
}
Read function definition:
define i64 @testfunc(i64 %v) {
entry:
%binaryOp = call i64 @"binary>"(i64 %v, i64 10)
%ifCond = icmp eq i64 %binaryOp, 0
%. = select i1 %ifCond, i64 0, i64 1
ret i64 %.
}
Read top-level expression:
define i64 @__anon_expr() {
entry:
%call = call i64 @testfunc(i64 1)
ret i64 %call
}
Evaluated to 0.