生成字节码

在这篇文章中,我们将看到如何为我们的语言生成字节码。 到目前为止,我们已经看到了如何构建一种语言来表达我们想要的东西,如何验证该语言,如何为该语言构建编辑器,但实际上我们仍然无法运行代码。 是时候解决这个问题了。 通过为JVM进行编译,我们的代码将能够在各种平台上运行。 对我来说听起来很棒!

jvm_bytecode_write_your_own_compiler

建立自己的语言的系列

以前的帖子:

  1. 建立词法分析器
  2. 建立一个解析器
  3. 创建带有语法突出显示的编辑器
  4. 使用自动补全功能构建编辑器
  5. 将解析树映射到抽象语法树
  6. 模型转换模型
  7. 验证方式

代码在GitHub上的标签08_bytecode可用

添加打印声明

在跳入字节码生成之前,我们只需在我们的语言中添加一条打印语句即可。 这很容易:我们只需要在词法分析器和解析器定义中更改几行,就可以了。

// Changes to lexer
PRINT              : 'print';
 
// Changes to parser
statement : varDeclaration # varDeclarationStatement
          | assignment     # assignmentStatement
          | print          # printStatement ;
 
print : PRINT LPAREN expression RPAREN ;

我们的编译器的一般结构

让我们从编译器的入口点开始。 我们将从标准输入或文件中获取代码(将被指定为第一个参数)。 一旦获得代码,我们将尝试构建AST并检查词汇和语法错误。 如果没有,我们将验证AST并检查语义错误。 如果仍然没有错误,我们继续进行字节码生成。

fun main(args: Array<String>) {
    val code : InputStream? = when (args.size) {
        0 -> System.`in`
        1 -> FileInputStream(File(args[0]))
        else -> {
            System.err.println("Pass 0 arguments or 1")
            System.exit(1)
            null
        }
    }
    val parsingResult = SandyParserFacade.parse(code!!)
    if (!parsingResult.isCorrect()) {
        println("ERRORS:")
        parsingResult.errors.forEach { println(" * L${it.position.line}: ${it.message}") }
        return
    }
    val root = parsingResult.root!!
    println(root)
    val errors = root.validate()
    if (errors.isNotEmpty()) {
        println("ERRORS:")
        errors.forEach { println(" * L${it.position.line}: ${it.message}") }
        return
    }
    val bytes = JvmCompiler().compile(root, "MyClass")
    val fos = FileOutputStream("MyClass.class")
    fos.write(bytes)
    fos.close()
}

请注意,在此示例中,我们始终生成一个名为MyClass的类文件。 大概以后,我们想找到一种为类文件指定名称的方法,但是现在这已经足够了。

使用ASM生成字节码

现在,让我们潜入有趣的部分。 JvmCompiler编译方法是我们生成字节的地方,以后我们将这些字节保存到类文件中。 我们如何产生这些字节? 在ASM的帮助下,该库是生成Bytecode的库。 现在,我们可以自己生成bytes数组,但要点是,它将涉及到一些无聊的任务,例如生成类池结构。 ASM为我们做到了。 我们仍然需要对JVM的结构有一些了解,但是我们可以生存下来而无需成为专家的精髓。

class JvmCompiler {
 
    fun compile(root: SandyFile, name: String) : ByteArray {
        // this is how we tell ASM that we want to start writing a new class. We ask it to calculate some values for us
        val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS)
        // here we specify that the class is in the format introduced with Java 8 (so it would require a JRE >= 8 to run)
        // we also specify the name of the class, the fact it extends Object and it implements no interfaces
        cw.visit(V1_8, ACC_PUBLIC, name, null, "java/lang/Object", null)
        // our class will have just one method: the main method. We have to specify its signature
        // this string just says that it takes an array of Strings and return nothing (void)
        val mainMethodWriter = cw.visitMethod(ACC_PUBLIC or ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null)
        mainMethodWriter.visitCode()
        // labels are used by ASM to mark points in the code
        val methodStart = Label()
        val methodEnd = Label()
        // with this call we indicate to what point in the method the label methodStart corresponds
        mainMethodWriter.visitLabel(methodStart)
 
        // Variable declarations:
        // we find all variable declarations in our code and we assign to them an index value
        // our vars map will tell us which variable name corresponds to which index
        var nextVarIndex = 0
        val vars = HashMap<String, Var>()
        root.specificProcess(VarDeclaration::class.java) {
            val index = nextVarIndex++
            vars[it.varName] = Var(it.type(vars), index)
            mainMethodWriter.visitLocalVariable(it.varName, it.type(vars).jvmDescription, null, methodStart, methodEnd, index)
        }
 
        // time to generate bytecode for all the statements
        root.statements.forEach { s ->
            when (s) {
                is VarDeclaration -> {
                    // we calculate the type of the variable (more details later)
                    val type = vars[s.varName]!!.type
                    // the JVM is a stack based machine: it operated with values we have put on the stack
                    // so as first thing when we meet a variable declaration we put its value on the stack
                    s.value.pushAs(mainMethodWriter, vars, type)
                    // now, depending on the type of the variable we use different operations to store the value
                    // we put on the stack into the variable. Note that we refer to the variable using its index, not its name
                    when (type) {
                        IntType -> mainMethodWriter.visitVarInsn(ISTORE, vars[s.varName]!!.index)
                        DecimalType -> mainMethodWriter.visitVarInsn(DSTORE, vars[s.varName]!!.index)
                        else -> throw UnsupportedOperationException(type.javaClass.canonicalName)
                    }
                }
                is Print -> {
                    // this means that we access the field "out" of "java.lang.System" which is of type "java.io.PrintStream"
                    mainMethodWriter.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")
                    // we push the value we want to print on the stack
                    s.value.push(mainMethodWriter, vars)
                    // we call the method println of System.out to print the value. It will take its parameter from the stack
                    // note that we have to tell the JVM which variant of println to call. To do that we describe the signature of the method,
                    // depending on the type of the value we want to print. If we want to print an int we will produce the signature "(I)V",
                    // we will produce "(D)V" for a double
                    mainMethodWriter.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(${s.value.type(vars).jvmDescription})V", false)
                }
                is Assignment -> {
                    val type = vars[s.varName]!!.type
                    // This code is the same we have seen for variable declarations
                    s.value.pushAs(mainMethodWriter, vars, type)
                    when (type) {
                        IntType -> mainMethodWriter.visitVarInsn(ISTORE, vars[s.varName]!!.index)
                        DecimalType -> mainMethodWriter.visitVarInsn(DSTORE, vars[s.varName]!!.index)
                        else -> throw UnsupportedOperationException(type.javaClass.canonicalName)
                    }
                }
                else -> throw UnsupportedOperationException(s.javaClass.canonicalName)
            }
        }
 
        // We just says that here is the end of the method
        mainMethodWriter.visitLabel(methodEnd)
        // And we had the return instruction
        mainMethodWriter.visitInsn(RETURN)
        mainMethodWriter.visitEnd()
        mainMethodWriter.visitMaxs(-1, -1)
        cw.visitEnd()
        return cw.toByteArray()
    }
 
}

关于类型

好的,我们已经看到我们的代码使用类型。 这是必需的,因为根据类型,我们需要使用不同的说明。 例如,将值放入整数变量中,我们使用ISTORE;而将值放入双重变量中,我们使用DSTORE 。 当我们以整数调用System.out.println时,我们需要指定签名(I)V,而当我们调用它以打印双精度字符时,则需要指定(D)V

为了做到这一点,我们需要知道每个表达式的类型。 在我们的超级简单语言中,我们现在仅使用intdouble 。 在真实的语言中,我们可能想使用更多的类型,但这足以向您展示这些原理。

interface SandyType {
    // given a type we want to get the corresponding string used in the JVM
    // for example: int -> I, double -> D, Object -> Ljava/lang/Object; String -> [Ljava.lang.String;
    val jvmDescription: String
}
 
object IntType : SandyType {
    override val jvmDescription: String
        get() = "I"
}
 
object DecimalType : SandyType {
    override val jvmDescription: String
        get() = "D"
}
 
fun Expression.type(vars: Map<String, Var>) : SandyType {
    return when (this) {
        // an int literal has type int. Easy :)
        is IntLit -> IntType
        is DecLit -> DecimalType
        // the result of a binary expression depends on the type of the operands
        is BinaryExpression -> {
            val leftType = left.type(vars)
            val rightType = right.type(vars)
            if (leftType != IntType && leftType != DecimalType) {
                throw UnsupportedOperationException()
            }
            if (rightType != IntType && rightType != DecimalType) {
                throw UnsupportedOperationException()
            }
            // an operation on two integers produces integers
            if (leftType == IntType && rightType == IntType) {
                return IntType
            // if at least a double is involved the result is a double
            } else {
                return DecimalType
            }
        }
        // when we refer to a variable the type is the type of the variable
        is VarReference -> vars[this.varName]!!.type
        // when we cast to a value, the resulting value is that type :)
        is TypeConversion -> this.targetType.toSandyType()
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
}

表达方式

如我们所见,JVM是基于堆栈的计算机。 因此,每次我们想使用一个值时,都会将其压入堆栈,然后执行一些操作。 让我们看看如何将值推入堆栈

// Convert, if needed
fun Expression.pushAs(methodWriter: MethodVisitor, vars: Map<String, Var>, desiredType: SandyType) {
    push(methodWriter, vars)
    val myType = type(vars)
    if (myType != desiredType) {
        if (myType == IntType && desiredType == DecimalType) {
            methodWriter.visitInsn(I2D)
        } else if (myType == DecimalType && desiredType == IntType) {
            methodWriter.visitInsn(D2I)
        } else {
            throw UnsupportedOperationException("Conversion from $myType to $desiredType")
        }
    }
}
 
fun Expression.push(methodWriter: MethodVisitor, vars: Map<String, Var>) {
    when (this) {
        // We have specific operations to push integers and double values
        is IntLit -> methodWriter.visitLdcInsn(Integer.parseInt(this.value))
        is DecLit -> methodWriter.visitLdcInsn(java.lang.Double.parseDouble(this.value))
        // to push a sum we first push the two operands and then invoke an operation which
        // depend on the type of the operands (do we sum integers or doubles?)
        is SumExpression -> {
            left.pushAs(methodWriter, vars, this.type(vars))
            right.pushAs(methodWriter, vars, this.type(vars))
            when (this.type(vars)) {
                IntType -> methodWriter.visitInsn(IADD)
                DecimalType -> methodWriter.visitInsn(DADD)
                else -> throw UnsupportedOperationException("Summing ${this.type(vars)}")
            }
        }
        is SubtractionExpression -> {
            left.pushAs(methodWriter, vars, this.type(vars))
            right.pushAs(methodWriter, vars, this.type(vars))
            when (this.type(vars)) {
                IntType -> methodWriter.visitInsn(ISUB)
                DecimalType -> methodWriter.visitInsn(DSUB)
                else -> throw UnsupportedOperationException("Summing ${this.type(vars)}")
            }
        }
        is DivisionExpression -> {
            left.pushAs(methodWriter, vars, this.type(vars))
            right.pushAs(methodWriter, vars, this.type(vars))
            when (this.type(vars)) {
                IntType -> methodWriter.visitInsn(IDIV)
                DecimalType -> methodWriter.visitInsn(DDIV)
                else -> throw UnsupportedOperationException("Summing ${this.type(vars)}")
            }
        }
        is MultiplicationExpression -> {
            left.pushAs(methodWriter, vars, this.type(vars))
            right.pushAs(methodWriter, vars, this.type(vars))
            when (this.type(vars)) {
                IntType -> methodWriter.visitInsn(IMUL)
                DecimalType -> methodWriter.visitInsn(DMUL)
                else -> throw UnsupportedOperationException("Summing ${this.type(vars)}")
            }
        }
        // to push a variable we just load the value from the symbol table
        is VarReference -> {
            val type = vars[this.varName]!!.type
            when (type) {
                IntType -> methodWriter.visitVarInsn(ILOAD, vars[this.varName]!!.index)
                DecimalType -> methodWriter.visitVarInsn(DLOAD, vars[this.varName]!!.index)
                else -> throw UnsupportedOperationException(type.javaClass.canonicalName)
            }
        }
        // the pushAs operation take care of conversions, as needed
        is TypeConversion -> {
            this.value.pushAs(methodWriter, vars, this.targetType.toSandyType())
        }
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
}

摇篮

我们还可以创建gradle任务来编译源文件

main = "me.tomassetti.sandy.compiling.JvmKt"
    args = "$sourceFile"
    classpath = sourceSets.main.runtimeClasspath
}

结论

我们没有详细介绍,我们急于浏览代码。 我的目的只是给您概述用于生成字节码的一般策略。 当然,如果您想构建一种严肃的语言,则需要做一些研究并了解JVM的内部,这是无可避免的。 我只是希望这个简短的介绍足以使您了解到这并不那么令人恐惧或复杂,大多数人都认为。

翻译自: https://www.javacodegeeks.com/2016/09/generating-bytecode.html

### 回答1: 是的,即使源代码中有语法错误,编译器仍然可以生成字节码。编译器会尽力生成字节码,即使源代码中存在语法错误。但是,生成字节码可能无法正常运行,因为它可能依赖于语法错误中缺失的信息。因此,如果在编译时发现了语法错误,最好修复它们,以便生成字节码能够正常运行。 ### 回答2: 是的,即使源代码存在语法错误,编译器仍然可以生成字节码。编译器在将源代码转换为可执行文件或字节码文件的过程中,首先会进行词法分析和语法分析,来检测和解析源代码中的语法错误。如果源代码中存在语法错误,编译器将会发现错误并产生相应的错误信息。 然而,在编译过程中发现语法错误并不意味着编译器会停止编译,它会尽可能多地进行编译工作并生成字节码。这是因为编译器通常采用逐行编译的方式,即使某行代码存在错误,也不会影响其他行的编译。 由于编译器会尽可能多地编译源代码,因此即使存在语法错误,编译器仍然可以生成字节码文件。但是,生成字节码文件可能无法被虚拟机正确解释执行,因为虚拟机在执行字节码时会对语法进行严格检查。因此,即使编译器可以生成字节码文件,但在运行时可能会出现错误。 总而言之,源代码存在语法错误时,编译器仍然可以生成字节码。然而,生成字节码可能无法正确执行,因为虚拟机会对语法进行检查。因此,为了确保正确的执行,我们仍然应该尽量避免源代码中的语法错误。 ### 回答3: 编译器在将源代码转换为目标机器的字节码之前,会对源代码进行词法分析、语法分析等一系列的处理过程。其中,语法分析是检查源代码是否符合编程语言的语法规则。 如果源代码中存在语法错误,即不符合语法规则,编译器会在语法分析阶段发现错误,并生成相应的错误信息。在这种情况下,编译器会停止生成字节码,并将错误信息通知给开发者。 因此,即使源代码存在语法错误,编译器不会生成字节码。源代码必须要通过语法分析,无错误地符合语法规则,才能生成有效的字节码。这是因为字节码是由编译器根据源代码正确的语法结构生成的,它才能正确地被虚拟机解释执行。 所以,可以说只有当源代码没有语法错误时,编译器才能生成有效的字节码。如果源代码存在语法错误,编译器不能生成字节码。因此,编程时应尽量避免语法错误,以确保编译器能够正确生成可执行的字节码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值