From 9679121072e1b044a1489d356f810930cd56b148 Mon Sep 17 00:00:00 2001 From: James Hamilton Date: Wed, 20 Oct 2021 08:18:27 +0200 Subject: [PATCH] Add JVM backend --- README.md | 29 +- build.gradle.kts | 15 +- src/main/kotlin/eu/jameshamilton/klox/Main.kt | 155 +- .../eu/jameshamilton/klox/compile/Builtins.kt | 27 + .../klox/compile/ClassBuilderExt.kt | 63 + .../klox/compile/ClassPoolExt.kt | 5 + .../eu/jameshamilton/klox/compile/Compiler.kt | 1280 +++++++++++++++++ .../jameshamilton/klox/compile/ComposerExt.kt | 398 +++++ .../klox/compile/InvokeDynamicCounter.kt | 89 ++ .../eu/jameshamilton/klox/compile/Resolver.kt | 329 +++++ .../klox/compile/StackSizeComputer.kt | 161 +++ .../klox/{ => interpret}/Interpreter.kt | 43 +- .../klox/{ => interpret}/LoxCallable.kt | 11 +- .../klox/{ => interpret}/Resolver.kt | 54 +- .../kotlin/eu/jameshamilton/klox/io/Writer.kt | 37 + .../eu/jameshamilton/klox/{ => parse}/AST.kt | 215 ++- .../jameshamilton/klox/{ => parse}/Checker.kt | 43 +- .../jameshamilton/klox/{ => parse}/Parser.kt | 18 +- .../jameshamilton/klox/{ => parse}/Scanner.kt | 5 +- .../jameshamilton/klox/{ => parse}/Token.kt | 4 +- .../klox/util/ClassPoolClassLoader.kt | 27 + .../klox/InvokeDynamicCounterTest.kt | 58 + .../kotlin/eu/jameshamilton/klox/KLoxTest.kt | 114 +- .../eu/jameshamilton/klox/ParserTest.kt | 88 -- .../eu/jameshamilton/klox/ScannerTest.kt | 128 -- .../klox/StackSizeComputerTest.kt | 219 +++ .../eu/jameshamilton/klox/util/TestUtil.kt | 7 + 27 files changed, 3224 insertions(+), 398 deletions(-) create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/Builtins.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/ClassBuilderExt.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/ClassPoolExt.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/Compiler.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/ComposerExt.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/InvokeDynamicCounter.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/Resolver.kt create mode 100644 src/main/kotlin/eu/jameshamilton/klox/compile/StackSizeComputer.kt rename src/main/kotlin/eu/jameshamilton/klox/{ => interpret}/Interpreter.kt (88%) rename src/main/kotlin/eu/jameshamilton/klox/{ => interpret}/LoxCallable.kt (89%) rename src/main/kotlin/eu/jameshamilton/klox/{ => interpret}/Resolver.kt (69%) create mode 100644 src/main/kotlin/eu/jameshamilton/klox/io/Writer.kt rename src/main/kotlin/eu/jameshamilton/klox/{ => parse}/AST.kt (56%) rename src/main/kotlin/eu/jameshamilton/klox/{ => parse}/Checker.kt (78%) rename src/main/kotlin/eu/jameshamilton/klox/{ => parse}/Parser.kt (96%) rename src/main/kotlin/eu/jameshamilton/klox/{ => parse}/Scanner.kt (97%) rename src/main/kotlin/eu/jameshamilton/klox/{ => parse}/Token.kt (74%) create mode 100644 src/main/kotlin/eu/jameshamilton/klox/util/ClassPoolClassLoader.kt create mode 100644 src/test/kotlin/eu/jameshamilton/klox/InvokeDynamicCounterTest.kt delete mode 100644 src/test/kotlin/eu/jameshamilton/klox/ParserTest.kt delete mode 100644 src/test/kotlin/eu/jameshamilton/klox/ScannerTest.kt create mode 100644 src/test/kotlin/eu/jameshamilton/klox/StackSizeComputerTest.kt create mode 100644 src/test/kotlin/eu/jameshamilton/klox/util/TestUtil.kt diff --git a/README.md b/README.md index 2f6b09b..e661fb5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ![Build status](https://img.shields.io/github/workflow/status/mrjameshamilton/klox/CI) ![Coverage](.github/badges/jacoco.svg) -A Kotlin implementation of lox, the language from [Crafting Interpreters](https://craftinginterpreters.com/). +A Kotlin implementation of lox, the language from [Crafting Interpreters](https://craftinginterpreters.com/), +with a JVM backend built with [ProGuardCORE](https://github.com/guardsquare/proguard-core). ## Building @@ -10,13 +11,37 @@ A Kotlin implementation of lox, the language from [Crafting Interpreters](https: ./gradlew build ``` +The build task will execute all tests and create an output jar `lib/klox.jar`. + ## Executing +A wrapper script `bin/klox` is provided for convenience in the `bin/` directory: + ```shell -bin/klox [script.lox] +$ bin/klox --help +Usage: klox options_list +Arguments: + script -> Lox Script (optional) { String } +Options: + --outJar, -o -> output jar { String } + --useInterpreter, -i -> use interpreter instead of JVM compiler when executing + --debug -> enable debugging + --dumpClasses, -d -> dump textual representation of classes (instead of executing) + --help, -h -> Usage info ``` Execute without a script for a REPL, otherwise the provided Lox script will be executed. +If a Lox script is provided, by default, the script will be executed by compiling it for +the JVM and executing the compiled code. The interpreter can be used instead to execute the script +by passing the `--useInterpreter` option (useful for comparing interpreted vs compiled runtime!). + +The compiler can generate a jar for the given script by passing the `--outJar` option (in +this case the script will not be executed by `klox`) e.g. + +```shell +$ bin/klox myScript.lox --outJar myScript.jar +$ java -jar myScript.jar +``` ## Example Lox program diff --git a/build.gradle.kts b/build.gradle.kts index 59ec6b4..54c7a0d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,17 +8,22 @@ plugins { } group = "eu.jameshamilton" -version = "1.0" +version = "2.0" repositories { mavenCentral() } dependencies { + implementation("com.guardsquare:proguard-core:8.0.1") + implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.3") + compileOnly("org.jetbrains:annotations:22.0.0") + testImplementation("org.jetbrains.kotlin:kotlin-test:1.5.21") - testImplementation("io.kotest:kotest-runner-junit5-jvm:4.6.1") - testImplementation("io.kotest:kotest-assertions-core-jvm:4.6.1") - testImplementation("io.kotest:kotest-property-jvm:4.6.1") + testImplementation("io.kotest:kotest-runner-junit5-jvm:4.6.3") + testImplementation("io.kotest:kotest-assertions-core-jvm:4.6.3") + testImplementation("io.kotest:kotest-property-jvm:4.6.3") + testImplementation("io.kotest:kotest-framework-datatest:4.6.3") testImplementation("io.mockk:mockk:1.12.0") } @@ -26,7 +31,7 @@ tasks.test { useJUnitPlatform() } -tasks.withType() { +tasks.withType { kotlinOptions.jvmTarget = "11" } diff --git a/src/main/kotlin/eu/jameshamilton/klox/Main.kt b/src/main/kotlin/eu/jameshamilton/klox/Main.kt index d248f38..f981f45 100644 --- a/src/main/kotlin/eu/jameshamilton/klox/Main.kt +++ b/src/main/kotlin/eu/jameshamilton/klox/Main.kt @@ -1,71 +1,162 @@ package eu.jameshamilton.klox -import eu.jameshamilton.klox.TokenType.EOF -import java.io.BufferedReader +import eu.jameshamilton.klox.compile.Compiler +import eu.jameshamilton.klox.compile.contains +import eu.jameshamilton.klox.interpret.Interpreter +import eu.jameshamilton.klox.interpret.Resolver +import eu.jameshamilton.klox.interpret.RuntimeError +import eu.jameshamilton.klox.io.writeJar +import eu.jameshamilton.klox.parse.Checker +import eu.jameshamilton.klox.parse.Parser +import eu.jameshamilton.klox.parse.Program +import eu.jameshamilton.klox.parse.Scanner +import eu.jameshamilton.klox.parse.Token +import eu.jameshamilton.klox.parse.TokenType.EOF +import eu.jameshamilton.klox.util.ClassPoolClassLoader +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.optional +import proguard.classfile.ClassPool +import proguard.classfile.ProgramClass +import proguard.classfile.attribute.Attribute.SOURCE_FILE +import proguard.classfile.attribute.SourceFileAttribute +import proguard.classfile.editor.AttributesEditor +import proguard.classfile.editor.ConstantPoolEditor +import proguard.classfile.visitor.ClassPrinter import java.io.File -import java.io.InputStreamReader import kotlin.system.exitProcess +val parser = ArgParser("klox") +val script by parser.argument(ArgType.String, description = "Lox Script").optional() +val outJar by parser.option(ArgType.String, shortName = "o", description = "output jar") +val useInterpreter by parser.option(ArgType.Boolean, shortName = "i", description = "use interpreter instead of JVM compiler when executing") +val debug by parser.option(ArgType.Boolean, description = "enable debugging") +val dumpClasses by parser.option(ArgType.Boolean, shortName = "d", description = "dump textual representation of classes (instead of executing)") + var hadError: Boolean = false var hadRuntimeError: Boolean = false +val programClassPool = ClassPool() + fun main(args: Array) { - if (args.size > 1) { - println("Usage: klox [script]") - exitProcess(64) - } else if (args.size == 1) { - runFile(args[0]) + parser.parse(args) + if (script != null) { + if (useInterpreter == true) { + interpret(File(script)) + } else { + compile(File(script), if (outJar != null) File(outJar) else null)?.let { + if (outJar == null && dumpClasses == null) run(it) + } + } + if (hadRuntimeError) exitProcess(65) + if (hadError) exitProcess(75) } else { + if (outJar != null) { + error(0, "outJar is only applicable when executing a script") + } + + if (dumpClasses != null) { + error(0, "dumpClasses is only applicable when executing a script with the compiler") + } + + if (hadError) exitProcess(75) + runPrompt() } } fun runPrompt() { - val input = InputStreamReader(System.`in`) - val reader = BufferedReader(input) val interpreter = Interpreter() while (true) { print("> ") - val line = reader.readLine() ?: break - run(line, interpreter) - hadError = false + val program = parse(readLine() ?: break) + val resolver = Resolver(interpreter) + program?.let { + it.accept(resolver) + if (hadError) return@let + try { + interpreter.interpret(program.stmts) + } catch (e: StackOverflowError) { + System.err.println("Stack overflow.") + hadRuntimeError = true + } + hadError = false + } } } -fun runFile(path: String) { - run(File(path).readText()) - if (hadError) exitProcess(65) - if (hadError) exitProcess(75) -} - -fun run(code: String, interpreter: Interpreter = Interpreter()) { +fun parse(code: String): Program? { hadError = false hadRuntimeError = false val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) - val stmts = parser.parse() + val program = parser.parse() - if (hadError) return + if (hadError) return null val checker = Checker() - checker.check(stmts) + program.accept(checker) - if (hadError) return + if (hadError) return null - val resolver = Resolver(interpreter) + return program +} - resolver.resolve(stmts) +fun interpret(file: File) { + val code = file.readText() + val program = parse(code) + val interpreter = Interpreter() + val resolver = Resolver(interpreter) + program?.let { + it.accept(resolver) + if (hadError) return + try { + interpreter.interpret(program.stmts) + } catch (e: StackOverflowError) { + System.err.println("Stack overflow.") + hadRuntimeError = true + } + } +} - if (hadError) return +fun compile(file: File, outJar: File? = null): ClassPool? { + if (debug == true) println("Compiling $file...") + val code = file.readText() + val program = parse(code) + program?.let { + val programClassPool = Compiler().compile(program) + + programClassPool.classesAccept { clazz -> + with(ConstantPoolEditor(clazz as ProgramClass)) { + AttributesEditor(clazz, true).addAttribute( + SourceFileAttribute( + addUtf8Constant(SOURCE_FILE), + addUtf8Constant(file.name) + ) + ) + } + } + + if (outJar != null) writeJar(programClassPool, outJar, "Main") + if (dumpClasses == true) programClassPool.classesAccept(ClassPrinter()) + return programClassPool + } + return null +} - try { - interpreter.interpret(stmts) - } catch (e: StackOverflowError) { - System.err.println("Stack overflow.") - hadRuntimeError = true +fun run(programClassPool: ClassPool) { + if (programClassPool.contains("Main")) { + if (debug == true) println("Executing...") + val clazzLoader = ClassPoolClassLoader(programClassPool) + Thread.currentThread().contextClassLoader = clazzLoader + clazzLoader + .loadClass("Main") + .declaredMethods + .single { it.name == "main" } + .invoke(null, emptyArray()) } } diff --git a/src/main/kotlin/eu/jameshamilton/klox/compile/Builtins.kt b/src/main/kotlin/eu/jameshamilton/klox/compile/Builtins.kt new file mode 100644 index 0000000..2227dd0 --- /dev/null +++ b/src/main/kotlin/eu/jameshamilton/klox/compile/Builtins.kt @@ -0,0 +1,27 @@ +package eu.jameshamilton.klox.compile + +import eu.jameshamilton.klox.parse.FunctionStmt +import eu.jameshamilton.klox.parse.FunctionType.* +import eu.jameshamilton.klox.parse.Parameter +import eu.jameshamilton.klox.parse.Token +import eu.jameshamilton.klox.parse.TokenType.* +import proguard.classfile.editor.CompactCodeAttributeComposer as Composer + +// Built-in Klox functions + +val builtIns = listOf( + NativeFunctionStmt(Token(IDENTIFIER, "clock"), emptyList()) { + invokestatic("java/lang/System", "currentTimeMillis", "()J") + l2d() + pushDouble(1000.0) + ddiv() + box("java/lang/Double") + areturn() + } +) + +class NativeFunctionStmt(override val name: Token, override val params: List, val code: Composer.() -> Composer) : + FunctionStmt(name, NATIVE, params, emptyList()) { + + override fun toString(): String = "" +} diff --git a/src/main/kotlin/eu/jameshamilton/klox/compile/ClassBuilderExt.kt b/src/main/kotlin/eu/jameshamilton/klox/compile/ClassBuilderExt.kt new file mode 100644 index 0000000..aae35da --- /dev/null +++ b/src/main/kotlin/eu/jameshamilton/klox/compile/ClassBuilderExt.kt @@ -0,0 +1,63 @@ +package eu.jameshamilton.klox.compile + +import proguard.classfile.AccessConstants.PUBLIC +import proguard.classfile.Method +import proguard.classfile.ProgramClass +import proguard.classfile.VersionConstants.CLASS_VERSION_1_6 +import proguard.classfile.attribute.BootstrapMethodInfo +import proguard.classfile.editor.BootstrapMethodsAttributeAdder +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.ConstantPoolEditor +import proguard.classfile.editor.CompactCodeAttributeComposer as Composer + +private val EMPTY_CLASS: ProgramClass = ClassBuilder( + CLASS_VERSION_1_6, + PUBLIC, + "EMPTY", + "java/lang/Object" +).programClass + +fun ClassBuilder.addMethod(u2accessFlags: Int, name: String, descriptor: String, composer: Composer.() -> Composer): ClassBuilder { + // Inefficient, since the composition has to be done twice (first to compute the codeLength) but API is improved. + // It also means that any code in the composer should not have side-effects, since it will be + // executed twice! + val countingComposer = composer(object : Composer(EMPTY_CLASS) { + override fun label(label: Label): proguard.classfile.editor.CompactCodeAttributeComposer { + // Adding a label without specifying a maxCodeFragmentSize would + // normally throw an ArrayIndexOutOfBoundsException. + // Since this first composer execution is only needed for + // counting the size, it doesn't matter about adding labels. + return this + } + }) + + return addMethod(u2accessFlags, name, descriptor, countingComposer.codeLength) { + composer(it) + } +} + +inline fun ClassBuilder.addAndReturnMethod(u2accessFlags: Int, name: String, descriptor: String, noinline composer: Composer.() -> Composer): T { + addMethod(u2accessFlags, name, descriptor, composer) + return programClass.findMethod(name, descriptor) as T +} + +// TODO how to get bootstrap method ID when needed? invokedynamic(0, "invoke") +fun ClassBuilder.addBootstrapMethod(kind: Int, className: String, name: String, descriptor: String, arguments: (ConstantPoolEditor) -> Array = { emptyArray() }): ClassBuilder { + val constantPoolEditor = ConstantPoolEditor(programClass) + val bootstrapMethodsAttributeAdder = BootstrapMethodsAttributeAdder(programClass) + + val args = arguments(constantPoolEditor) + + val bootstrapMethodInfo = BootstrapMethodInfo( + constantPoolEditor.addMethodHandleConstant( + kind, + constantPoolEditor.addMethodrefConstant(className, name, descriptor, null, null) + ), + args.size, + args.toIntArray() + ) + + bootstrapMethodsAttributeAdder.visitBootstrapMethodInfo(programClass, bootstrapMethodInfo) + + return this +} diff --git a/src/main/kotlin/eu/jameshamilton/klox/compile/ClassPoolExt.kt b/src/main/kotlin/eu/jameshamilton/klox/compile/ClassPoolExt.kt new file mode 100644 index 0000000..8353fae --- /dev/null +++ b/src/main/kotlin/eu/jameshamilton/klox/compile/ClassPoolExt.kt @@ -0,0 +1,5 @@ +package eu.jameshamilton.klox.compile + +import proguard.classfile.ClassPool + +fun ClassPool.contains(className: String) = this.getClass(className) != null diff --git a/src/main/kotlin/eu/jameshamilton/klox/compile/Compiler.kt b/src/main/kotlin/eu/jameshamilton/klox/compile/Compiler.kt new file mode 100644 index 0000000..2e9bbdc --- /dev/null +++ b/src/main/kotlin/eu/jameshamilton/klox/compile/Compiler.kt @@ -0,0 +1,1280 @@ +package eu.jameshamilton.klox.compile + +import eu.jameshamilton.klox.compile.Resolver.Companion.captured +import eu.jameshamilton.klox.compile.Resolver.Companion.definedIn +import eu.jameshamilton.klox.compile.Resolver.Companion.depth +import eu.jameshamilton.klox.compile.Resolver.Companion.isCaptured +import eu.jameshamilton.klox.compile.Resolver.Companion.isDefined +import eu.jameshamilton.klox.compile.Resolver.Companion.isGlobalLateInit +import eu.jameshamilton.klox.compile.Resolver.Companion.javaClassName +import eu.jameshamilton.klox.compile.Resolver.Companion.javaName +import eu.jameshamilton.klox.compile.Resolver.Companion.slot +import eu.jameshamilton.klox.compile.Resolver.Companion.varDef +import eu.jameshamilton.klox.compile.Resolver.Companion.variables +import eu.jameshamilton.klox.debug +import eu.jameshamilton.klox.hadError +import eu.jameshamilton.klox.parse.AssignExpr +import eu.jameshamilton.klox.parse.BinaryExpr +import eu.jameshamilton.klox.parse.BlockStmt +import eu.jameshamilton.klox.parse.BreakStmt +import eu.jameshamilton.klox.parse.CallExpr +import eu.jameshamilton.klox.parse.ClassStmt +import eu.jameshamilton.klox.parse.ContinueStmt +import eu.jameshamilton.klox.parse.Expr +import eu.jameshamilton.klox.parse.ExprStmt +import eu.jameshamilton.klox.parse.FunctionStmt +import eu.jameshamilton.klox.parse.FunctionType.* +import eu.jameshamilton.klox.parse.GetExpr +import eu.jameshamilton.klox.parse.GroupingExpr +import eu.jameshamilton.klox.parse.IfStmt +import eu.jameshamilton.klox.parse.LiteralExpr +import eu.jameshamilton.klox.parse.LogicalExpr +import eu.jameshamilton.klox.parse.PrintStmt +import eu.jameshamilton.klox.parse.Program +import eu.jameshamilton.klox.parse.ReturnStmt +import eu.jameshamilton.klox.parse.SetExpr +import eu.jameshamilton.klox.parse.Stmt +import eu.jameshamilton.klox.parse.SuperExpr +import eu.jameshamilton.klox.parse.ThisExpr +import eu.jameshamilton.klox.parse.Token +import eu.jameshamilton.klox.parse.TokenType.AND +import eu.jameshamilton.klox.parse.TokenType.BANG +import eu.jameshamilton.klox.parse.TokenType.BANG_EQUAL +import eu.jameshamilton.klox.parse.TokenType.EQUAL_EQUAL +import eu.jameshamilton.klox.parse.TokenType.FUN +import eu.jameshamilton.klox.parse.TokenType.GREATER +import eu.jameshamilton.klox.parse.TokenType.GREATER_EQUAL +import eu.jameshamilton.klox.parse.TokenType.LESS +import eu.jameshamilton.klox.parse.TokenType.LESS_EQUAL +import eu.jameshamilton.klox.parse.TokenType.MINUS +import eu.jameshamilton.klox.parse.TokenType.OR +import eu.jameshamilton.klox.parse.TokenType.PLUS +import eu.jameshamilton.klox.parse.TokenType.SLASH +import eu.jameshamilton.klox.parse.TokenType.STAR +import eu.jameshamilton.klox.parse.UnaryExpr +import eu.jameshamilton.klox.parse.VarStmt +import eu.jameshamilton.klox.parse.VariableExpr +import eu.jameshamilton.klox.parse.WhileStmt +import eu.jameshamilton.klox.programClassPool +import proguard.classfile.AccessConstants.ABSTRACT +import proguard.classfile.AccessConstants.FINAL +import proguard.classfile.AccessConstants.INTERFACE +import proguard.classfile.AccessConstants.PRIVATE +import proguard.classfile.AccessConstants.PUBLIC +import proguard.classfile.AccessConstants.STATIC +import proguard.classfile.AccessConstants.VARARGS +import proguard.classfile.ClassPool +import proguard.classfile.LibraryClass +import proguard.classfile.ProgramClass +import proguard.classfile.ProgramMethod +import proguard.classfile.VersionConstants.CLASS_VERSION_1_8 +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.constant.MethodHandleConstant.REF_INVOKE_STATIC +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.CompactCodeAttributeComposer.Label +import proguard.classfile.visitor.AllMethodVisitor +import proguard.classfile.visitor.ClassPrinter +import proguard.classfile.visitor.ClassVersionFilter +import proguard.preverify.CodePreverifier +import proguard.classfile.editor.CompactCodeAttributeComposer as Composer + +class Compiler : Program.Visitor { + + fun compile(program: Program): ClassPool { + initialize(programClassPool) + return program.accept(this) + } + + override fun visitProgram(program: Program): ClassPool { + val mainFunction = FunctionStmt( + Token(FUN, "Main"), + SCRIPT, + params = emptyList(), + body = builtIns + program.stmts + ) + + mainFunction.accept(Resolver()) + + if (hadError) return ClassPool() + + val (mainClass, _) = FunctionCompiler().compile(mainFunction) + + ClassBuilder(mainClass).addMethod(PUBLIC or STATIC, "main", "([Ljava/lang/String;)V", 100) { + with(it) { // TODO why addMethod extension fun doesn't work with the catchall here? + val (tryStart, tryEnd) = try_ { + new_(targetClass.name) + dup() + aconst_null() + invokespecial(targetClass.name, "", "(L$KLOX_CALLABLE;)V") + aload_0() + invokeinterface(KLOX_CALLABLE, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") + pop() + return_() + } + catch_(tryStart, tryEnd, "java/lang/StackOverflowError") { + pop() + getstatic("java/lang/System", "err", "Ljava/io/PrintStream;") + ldc("Stack overflow.") + invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/Object;)V") + return_() + } + catchAll(tryStart, tryEnd) { + if (debug == true) dup() + getstatic("java/lang/System", "err", "Ljava/io/PrintStream;") + swap() + invokevirtual("java/lang/Throwable", "getMessage", "()Ljava/lang/String;") + invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/Object;)V") + if (debug == true) invokevirtual("java/lang/Throwable", "printStackTrace", "()V") + return_() + } + } + } + + return preverify(programClassPool) + } + + /** + * Preverifies the code of the classes in the given class pool, + * adding StackMapTable attributes to code that requires it. + * @param programClassPool the classes to be preverified. + */ + private fun preverify(programClassPool: ClassPool): ClassPool { + programClassPool.classesAccept { clazz -> + try { + clazz.accept( + ClassVersionFilter( + CLASS_VERSION_1_8, + AllMethodVisitor( + AllAttributeVisitor( + CodePreverifier(false) + ) + ) + ) + ) + } catch (e: Exception) { + clazz.accept(ClassPrinter()) + throw e + } + } + + return programClassPool + } + + private inner class FunctionCompiler(private val enclosingCompiler: FunctionCompiler? = null) : Stmt.Visitor, Expr.Visitor { + private lateinit var composer: Composer + private lateinit var functionStmt: FunctionStmt + + fun compile(functionStmt: FunctionStmt): Pair { + this.functionStmt = functionStmt + val (clazz, method) = create(functionStmt) + composer = Composer(clazz) + with(composer) { + if (functionStmt is NativeFunctionStmt) { + functionStmt.code(this) + } else { + beginCodeFragment(65_535) + + if (functionStmt.params.isNotEmpty()) { + aload_1() + unpackarray(functionStmt.params.size) { i -> + declare(functionStmt, functionStmt.params[i]) + } + } + + for (captured in functionStmt.captured) { + aload_0() + ldc(captured.javaName) + invokevirtual(targetClass.name, "getCaptured", "(Ljava/lang/String;)L$KLOX_CAPTURED_VAR;") + astore(functionStmt.slot(captured)) + } + + functionStmt.body.forEach { + it.accept(this@FunctionCompiler) + } + + if (functionStmt.kind == INITIALIZER) { + aload_0() + invokeinterface(KLOX_FUNCTION, "getReceiver", "()L$KLOX_INSTANCE;") + areturn() + } else if (functionStmt.body.count { it is ReturnStmt } == 0) { + aconst_null() + areturn() + } + + endCodeFragment() + } + + try { + addCodeAttribute(clazz, method) + } catch (e: Exception) { + codeAttribute.accept(clazz, method, ClassPrinter()) + throw e + } + + return Pair(clazz, method) + } + } + + override fun visitExprStmt(exprStmt: ExprStmt): Unit = with(composer) { + exprStmt.expression.accept(this@FunctionCompiler) + when (val size = exprStmt.expression.accept(StackSizeComputer())) { + 1 -> pop() + else -> for (i in 0 until size) pop() + } + } + + override fun visitPrintStmt(printStmt: PrintStmt): Unit = with(composer) { + println { + printStmt.expression.accept(this@FunctionCompiler) + stringify() + } + } + + override fun visitVarStmt(varStmt: VarStmt): Unit = with(composer) { + if (varStmt.initializer != null) varStmt.initializer!!.accept(this@FunctionCompiler) else aconst_null() + + declare(functionStmt, varStmt) + } + + override fun visitBlockStmt(block: BlockStmt) = block.stmts.forEach { it.accept(this) } + + override fun visitIfStmt(ifStmt: IfStmt): Unit = with(composer) { + val (elseLabel, endLabel) = labels(2) + ifStmt.condition.accept(this@FunctionCompiler) + ifnontruthy(elseLabel) + ifStmt.thenBranch.accept(this@FunctionCompiler) + goto_(endLabel) + label(elseLabel) + ifStmt.elseBranch?.accept(this@FunctionCompiler) + label(endLabel) + } + + private lateinit var currentLoopBodyLabel: Label + private lateinit var currentLoopEndLabel: Label + + override fun visitWhileStmt(whileStmt: WhileStmt): Unit = with(composer) { + val (conditionLabel, loopBody, endLabel) = labels(3) + currentLoopBodyLabel = loopBody + currentLoopEndLabel = endLabel + label(conditionLabel) + whileStmt.condition.accept(this@FunctionCompiler) + ifnontruthy(endLabel) + label(loopBody) + whileStmt.body.accept(this@FunctionCompiler) + goto_(conditionLabel) + label(endLabel) + } + + override fun visitBreakStmt(breakStmt: BreakStmt): Unit = with(composer) { + goto_(currentLoopEndLabel) + } + + override fun visitContinueStmt(continueStmt: ContinueStmt): Unit = with(composer) { + goto_(currentLoopBodyLabel) + } + + override fun visitBinaryExpr(binaryExpr: BinaryExpr) { + fun binaryOp(resultType: String, op: Composer.() -> Unit) = with(composer) { + val (tryStart, tryEnd) = try_ { + binaryExpr.left.accept(this@FunctionCompiler) + unbox("java/lang/Double") + binaryExpr.right.accept(this@FunctionCompiler) + unbox("java/lang/Double") + } + op(this) + box(resultType) + catch_(tryStart, tryEnd, "java/lang/ClassCastException") { + pop() + throw_("java/lang/RuntimeException", "Operands must be numbers.") + } + } + + fun comparison(op: Composer.(label: Label) -> Unit) = binaryOp("java/lang/Boolean") { + val (l0, l1) = labels(2) + op(composer, l0) + iconst_1() + goto_(l1) + label(l0) + iconst_0() + label(l1) + } + + fun equalequal(resultComposer: (Composer.() -> Unit)? = null) = with(composer) { + val (notNaN, notNaNPop, end) = labels(3) + binaryExpr.left.accept(this@FunctionCompiler) + dup() + binaryExpr.right.accept(this@FunctionCompiler) + dup() + // A, A, B, B + instanceof_("java/lang/Double") + ifeq(notNaNPop) + dup() + checkcast("java/lang/Double") + invokevirtual("java/lang/Double", "isNaN", "()Z") + ifeq(notNaNPop) + dup_x2() + pop() + instanceof_("java/lang/Double") + ifeq(notNaN) + dup() + checkcast("java/lang/Double") + invokevirtual("java/lang/Double", "isNaN", "()Z") + ifeq(notNaN) + pop2() // both NaN, so not equal + iconst_0() + goto_(end) + + label(notNaNPop) // there's an extra first param on the stack + // A, A, B + invokestatic("java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z") + swap() + pop() + goto_(end) + + label(notNaN) + invokestatic("java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z") + + label(end) + resultComposer?.let { it(composer) } + box("java/lang/Boolean") + } + + when (binaryExpr.operator.type) { + PLUS -> { + binaryExpr.left.accept(this@FunctionCompiler) + binaryExpr.right.accept(this@FunctionCompiler) + composer.helper("Main", "add", stackInputSize = 2, stackResultSize = 1) { + val (nonNumeric, addStringDouble, addDoubleString, addCombination, throwLabel) = labels(5) + aload_0() + ifnull(throwLabel) + aload_1() + ifnull(throwLabel) + + aload_0() + instanceof_("java/lang/Double") + ifeq(nonNumeric) + aload_1() + instanceof_("java/lang/Double") + ifeq(nonNumeric) + aload_0() + unbox("java/lang/Double") + aload_1() + unbox("java/lang/Double") + dadd() + box("java/lang/Double") + areturn() + + label(nonNumeric) + aload_0() + instanceof_("java/lang/String") + ifeq(addStringDouble) + aload_1() + instanceof_("java/lang/String") + ifeq(addStringDouble) + concat( + { aload_0() }, + { aload_1() } + ) + areturn() + + label(addStringDouble) + aload_0() + instanceof_("java/lang/String") + ifeq(addDoubleString) + aload_1() + instanceof_("java/lang/Double") + ifne(addCombination) + + label(addDoubleString) + aload_1() + instanceof_("java/lang/String") + ifeq(throwLabel) + aload_0() + instanceof_("java/lang/Double") + ifeq(throwLabel) + + label(addCombination) + concat( + { aload_0().stringify() }, + { aload_1().stringify() } + ) + areturn() + + label(throwLabel) + throw_("java/lang/RuntimeException", "Operands must be two numbers or two strings.") + } + } + MINUS -> binaryOp("java/lang/Double") { dsub() } + SLASH -> binaryOp("java/lang/Double") { ddiv() } + STAR -> binaryOp("java/lang/Double") { dmul() } + GREATER -> comparison { dcmpl(); ifle(it) } + GREATER_EQUAL -> comparison { dcmpl(); iflt(it) } + LESS -> comparison { dcmpg(); ifge(it) } + LESS_EQUAL -> comparison { dcmpg(); ifgt(it) } + BANG_EQUAL -> equalequal { + iconst_1() + ixor() + } + EQUAL_EQUAL -> equalequal() + else -> {} + } + } + + override fun visitUnaryExpr(unaryExpr: UnaryExpr): Unit = with(composer) { + unaryExpr.right.accept(this@FunctionCompiler) + + when (unaryExpr.operator.type) { + BANG -> { + val (label0, end) = labels(2) + ifnontruthy(label0) + FALSE() + goto_(end) + label(label0) + TRUE() + label(end) + } + MINUS -> { + val (tryStart, tryEnd) = try_ { + boxed("java/lang/Double") { + dneg() + } + } + catch_(tryStart, tryEnd, "java/lang/RuntimeException") { + pop() + throw_("java/lang/RuntimeException", "Operand must be a number.") + } + } + else -> {} + } + } + + override fun visitGroupingExpr(groupingExpr: GroupingExpr) = groupingExpr.expression.accept(this) + + override fun visitLiteralExpr(literalExpr: LiteralExpr): Unit = with(composer) { + when (literalExpr.value) { + is Boolean -> if (literalExpr.value) TRUE() else FALSE() + is String -> ldc(literalExpr.value) + is Double -> pushDouble(literalExpr.value).box("java/lang/Double") + else -> if (literalExpr.value == null) aconst_null() + } + } + + override fun visitVariableExpr(variableExpr: VariableExpr): Unit = with(composer) { + if (variableExpr.isDefined) { + aload(functionStmt.slot(variableExpr.varDef!!)) + if (variableExpr.varDef!!.isCaptured) unbox(variableExpr.varDef!!) + } else { + throw_("java/lang/RuntimeException", "Undefined variable '${variableExpr.name.lexeme}'.") + } + } + + override fun visitAssignExpr(assignExpr: AssignExpr): Unit = with(composer) { + if (!assignExpr.isDefined) { + throw_("java/lang/RuntimeException", "Undefined variable '${assignExpr.name.lexeme}'.") + return + } + + val varDef = assignExpr.varDef!! + + if (varDef.isCaptured) { + aload(functionStmt.slot(varDef)) + assignExpr.value.accept(this@FunctionCompiler) + dup_x1() + invokevirtual(KLOX_CAPTURED_VAR, "setValue", "(Ljava/lang/Object;)V") + } else { + assignExpr.value.accept(this@FunctionCompiler) + dup() + astore(functionStmt.slot(varDef)) + } + } + + override fun visitLogicalExpr(logicalExpr: LogicalExpr): Unit = with(composer) { + val (end) = labels(1) + when (logicalExpr.operator.type) { + OR -> { + logicalExpr.left.accept(this@FunctionCompiler) + dup() + iftruthy(end) + pop() + logicalExpr.right.accept(this@FunctionCompiler) + } + AND -> { + logicalExpr.left.accept(this@FunctionCompiler) + dup() + ifnontruthy(end) + pop() + logicalExpr.right.accept(this@FunctionCompiler) + } + else -> {} + } + label(end) + } + + override fun visitCallExpr(callExpr: CallExpr): Unit = with(composer) { + callExpr.callee.accept(this@FunctionCompiler) + + val (tryStart, tryEnd) = try_ { + checkcast(KLOX_CALLABLE) + } + + callExpr.arguments.forEach { it.accept(this@FunctionCompiler) } + invokedynamic( + 0, + "invoke", + """(L$KLOX_CALLABLE;${"Ljava/lang/Object;".repeat(callExpr.arguments.size)})Ljava/lang/Object;""" + ) + + // TODO create only one of these handlers per method + catch_(tryStart, tryEnd, "java/lang/ClassCastException") { + pop() + new_("java/lang/RuntimeException") + dup() + ldc("Can only call functions and classes.") + invokespecial("java/lang/RuntimeException", "", "(Ljava/lang/String;)V") + athrow() + } + } + + override fun visitFunctionStmt(functionStmt: FunctionStmt): Unit = with(composer) { + val (clazz, _) = FunctionCompiler(enclosingCompiler = this@FunctionCompiler).compile(functionStmt) + new_(clazz) + dup() + aload_0() + invokespecial(clazz.name, "", "(L$KLOX_CALLABLE;)V") + + if (functionStmt.captured.isNotEmpty()) dup() + + if (functionStmt.kind != INITIALIZER && functionStmt.kind != METHOD && functionStmt.kind != GETTER && functionStmt.kind != CLASS) { + // Don't need to store, it should remain on the stack so that it can be added to the class + declare(this@FunctionCompiler.functionStmt, functionStmt) + } + + if (functionStmt.captured.isNotEmpty()) { + for (varDef in functionStmt.captured) { + dup() + aload_0() + if (enclosingCompiler?.functionStmt != null) { + for (i in enclosingCompiler.functionStmt.depth downTo varDef.definedIn.depth) { + invokeinterface(KLOX_CALLABLE, "getEnclosing", "()L$KLOX_CALLABLE;") + } + checkcast(varDef.definedIn.javaClassName) + } + ldc(varDef.javaName) + dup_x1() + invokevirtual(varDef.definedIn.javaClassName, "getCaptured", "(Ljava/lang/String;)L$KLOX_CAPTURED_VAR;") + invokevirtual(functionStmt.javaClassName, "capture", "(Ljava/lang/String;L$KLOX_CAPTURED_VAR;)V") + } + pop() + } + } + + override fun visitReturnStmt(returnStmt: ReturnStmt): Unit = with(composer) { + when { + returnStmt.value != null -> returnStmt.value.accept(this@FunctionCompiler) + functionStmt.kind == INITIALIZER -> aload_0().invokeinterface(KLOX_FUNCTION, "getReceiver", "()L$KLOX_INSTANCE;") + else -> aconst_null() + } + + areturn() + } + + override fun visitClassStmt(classStmt: ClassStmt): Unit = with(composer) { + val clazz = create(classStmt) + val (isSuperClass) = labels(1) + new_(clazz) + dup() + aload_0() + if (classStmt.superClass != null) { + classStmt.superClass.accept(this@FunctionCompiler) + dup() + instanceof_(KLOX_CLASS) + ifne(isSuperClass) + throw_("java/lang/RuntimeException", "Superclass must be a class.") + label(isSuperClass) + invokespecial(clazz.name, "", "(L$KLOX_CALLABLE;L$KLOX_CLASS;)V") + } else { + invokespecial(clazz.name, "", "(L$KLOX_CALLABLE;)V") + } + + if (classStmt.methods.isNotEmpty()) dup() + + declare(functionStmt, classStmt) + + for (method in classStmt.methods) { + dup() + dup() + method.accept(this@FunctionCompiler) + dup_x2() + invokevirtual(classStmt.javaClassName, "addMethod", "(L$KLOX_FUNCTION;)V") + invokevirtual(method.javaClassName, "setOwner", "(L$KLOX_CLASS;)V") + } + } + + override fun visitGetExpr(getExpr: GetExpr): Unit = with(composer) { + val (notInstance, notInstanceAndNotClass, maybeGetter, end) = labels(4) + getExpr.obj.accept(this@FunctionCompiler) + dup() + instanceof_(KLOX_INSTANCE) + ifeq(notInstance) + checkcast(KLOX_INSTANCE) + ldc(getExpr.name.lexeme) + invokevirtual(KLOX_INSTANCE, "get", "(Ljava/lang/String;)Ljava/lang/Object;") + dup() + instanceof_(KLOX_FUNCTION) + ifne(maybeGetter) + goto_(end) + + label(maybeGetter) + dup() + invokeinterface(KLOX_CALLABLE, "arity", "()I") + iconst_m1() + ificmpne(end) + invokedynamic(0, "invoke", "(L$KLOX_CALLABLE;)Ljava/lang/Object;") + goto_(end) + + label(notInstance) + dup() + instanceof_(KLOX_CLASS) + ifeq(notInstanceAndNotClass) + checkcast(KLOX_CLASS) + ldc(getExpr.name.lexeme) + invokeinterface(KLOX_CLASS, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + goto_(end) + + label(notInstanceAndNotClass) + throw_("java/lang/RuntimeException", "Only instances have properties.") + + label(end) + } + + override fun visitSetExpr(setExpr: SetExpr): Unit = with(composer) { + val (notInstance, end) = labels(2) + setExpr.obj.accept(this@FunctionCompiler) + dup() + instanceof_(KLOX_INSTANCE) + ifeq(notInstance) + checkcast(KLOX_INSTANCE) + ldc(setExpr.name.lexeme) + setExpr.value.accept(this@FunctionCompiler) + dup_x2() + invokevirtual(KLOX_INSTANCE, "set", "(Ljava/lang/String;Ljava/lang/Object;)V") + goto_(end) + + label(notInstance) + throw_("java/lang/RuntimeException", "Only instances have fields.") + + label(end) + } + + override fun visitSuperExpr(superExpr: SuperExpr): Unit = with(composer) { + val (superMethodNotFound, end) = labels(2) + ldc(superExpr.method.lexeme) + aload_0() + for (i in 0 until superExpr.depth - 1) { + invokeinterface(KLOX_CALLABLE, "getEnclosing", "()L$KLOX_CALLABLE;") + } + checkcast(KLOX_FUNCTION) + invokeinterface(KLOX_FUNCTION, "getOwner", "()L$KLOX_CLASS;") + invokeinterface(KLOX_CLASS, "getSuperClass", "()L$KLOX_CLASS;") + swap() + invokeinterface(KLOX_CLASS, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + dup() + ifnull(superMethodNotFound) + + aload_0() + invokeinterface(KLOX_FUNCTION, "getReceiver", "()L$KLOX_INSTANCE;") + invokeinterface(KLOX_FUNCTION, "bind", "(L$KLOX_INSTANCE;)L$KLOX_FUNCTION;") + goto_(end) + + label(superMethodNotFound) + pop() + + throw_("java/lang/RuntimeException") { + concat( + { ldc("Undefined property '") }, + { ldc(superExpr.method.lexeme) }, + { ldc("'.") } + ) + } + + label(end) + } + + override fun visitThisExpr(thisExpr: ThisExpr): Unit = with(composer) { + aload_0() + for (i in 0 until thisExpr.depth - 1) { + invokeinterface(KLOX_CALLABLE, "getEnclosing", "()L$KLOX_CALLABLE;") + } + checkcast(KLOX_FUNCTION) + invokeinterface(KLOX_FUNCTION, "getReceiver", "()L$KLOX_INSTANCE;") + } + + fun create(classStmt: ClassStmt): ProgramClass { + val clazz = ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC, + classStmt.javaClassName, + "java/lang/Object" + ) + .addInterface(KLOX_CLASS) + .addInterface(KLOX_CALLABLE) + .addField(PRIVATE or FINAL, "__enclosing", "L$KLOX_CALLABLE;") + .addField(PRIVATE or FINAL, "__methods", "Ljava/util/Map;") + .apply { if (classStmt.superClass != null) addField(PRIVATE or FINAL, "__superClass", "L$KLOX_CLASS;") } + .addMethod(PUBLIC, "", """(L$KLOX_CALLABLE;${if (classStmt.superClass != null) "L$KLOX_CLASS;" else ""})V""") { + aload_0() + dup() + invokespecial("java/lang/Object", "", "()V") + aload_1() + putfield(targetClass.name, "__enclosing", "L$KLOX_CALLABLE;") + if (classStmt.superClass != null) { + aload_0() + aload_2() + putfield(targetClass.name, "__superClass", "L$KLOX_CLASS;") + } + aload_0() + new_("java/util/HashMap") + dup() + invokespecial("java/util/HashMap", "", "()V") + putfield(targetClass.name, "__methods", "Ljava/util/Map;") + return_() + } + .addMethod(PUBLIC or VARARGS, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") { + val (noConstructor, end) = labels(2) + new_(KLOX_INSTANCE) + dup() + dup() + aload_0() + invokespecial(KLOX_INSTANCE, "", "(L$KLOX_CLASS;)V") + aload_0() + ldc("init") + invokeinterface(KLOX_CLASS, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + dup() + ifnull(noConstructor) + checkcast(KLOX_FUNCTION) + swap() + invokeinterface(KLOX_FUNCTION, "bind", "(L$KLOX_INSTANCE;)L$KLOX_FUNCTION;") + aload_1() + invokeinterface(KLOX_FUNCTION, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") + pop() + goto_(end) + + label(noConstructor) + pop2() + + label(end) + areturn() + } + .addMethod(PUBLIC, "getName", "()Ljava/lang/String;") { + ldc(classStmt.javaClassName) + areturn() + } + .addMethod(PUBLIC, "arity", "()I") { + val (end) = labels(1) + aload_0() + ldc("init") + invokevirtual(targetClass.name, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + dup() + ifnull(end) + invokeinterface(KLOX_CALLABLE, "arity", "()I") + ireturn() + + label(end) + pop() + iconst_0() + ireturn() + } + .addMethod(PUBLIC, "getEnclosing", "()L$KLOX_CALLABLE;") { + aload_0() + getfield(targetClass.name, "__enclosing", "L$KLOX_CALLABLE;") + areturn() + } + .addMethod(PUBLIC, "getSuperClass", "()L$KLOX_CLASS;") { + if (classStmt.superClass != null) { + aload_0().getfield(targetClass.name, "__superClass", "L$KLOX_CLASS;") + } else { + aconst_null() + } + areturn() + } + .addMethod(PUBLIC, "addMethod", "(L$KLOX_FUNCTION;)V") { + aload_0() + getfield(targetClass.name, "__methods", "Ljava/util/Map;") + aload_1() + invokeinterface(KLOX_FUNCTION, "getName", "()Ljava/lang/String;") + aload_1() + invokeinterface( + "java/util/Map", + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" + ) + pop() + return_() + } + .addMethod(PUBLIC, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") { + val (end, endNull) = labels(2) + aload_0() + getfield(targetClass.name, "__methods", "Ljava/util/Map;") + aload_1() + invokeinterface("java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;") + dup() + ifnonnull(end) + pop() + aload_0() + invokeinterface(KLOX_CLASS, "getSuperClass", "()L$KLOX_CLASS;") + dup() + ifnull(endNull) + aload_1() + invokeinterface(KLOX_CLASS, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + + label(end) + checkcast(KLOX_FUNCTION) + areturn() + + label(endNull) + aconst_null() + areturn() + } + .addMethod(PUBLIC, "toString", "()Ljava/lang/String;") { + ldc(classStmt.javaClassName) + areturn() + } + .apply { + programClassPool.addClass(programClass) + }.programClass + + return clazz + } + + private fun create(functionStmt: FunctionStmt): Pair { + val clazz = ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC, + functionStmt.javaClassName, + "java/lang/Object" + ) + .addInterface(KLOX_FUNCTION) + .addField(PRIVATE or FINAL, "__enclosing", "L$KLOX_CALLABLE;") + .addField(PRIVATE or FINAL, "__captured", "Ljava/util/Map;") + .addField(PRIVATE or FINAL, "__owner", "L$KLOX_CLASS;") + .addMethod(PUBLIC, "", "(L$KLOX_CALLABLE;)V") { + aload_0() + dup() + invokespecial("java/lang/Object", "", "()V") + aload_1() + putfield(targetClass.name, "__enclosing", "L$KLOX_CALLABLE;") + aload_0() + new_("java/util/HashMap") + dup() + invokespecial("java/util/HashMap", "", "()V") + putfield(targetClass.name, "__captured", "Ljava/util/Map;") + val globalLateInitVariables = functionStmt.variables.filter { it.isGlobalLateInit } + // Create null captured values for global lateinit variables + for ((i, variable) in globalLateInitVariables.withIndex()) { + if (i == 0) aload_0() + dup() + ldc(variable.javaName) + aconst_null() + box(variable) + invokevirtual(targetClass.name, "capture", "(Ljava/lang/String;L$KLOX_CAPTURED_VAR;)V") + if (i == globalLateInitVariables.size - 1) pop() + } + return_() + } + .addMethod(PUBLIC, "getName", "()Ljava/lang/String;") { + ldc(functionStmt.name.lexeme) + areturn() + } + .addMethod(PUBLIC, "arity", "()I") { + if (functionStmt.kind == GETTER) iconst_m1() else iconst(functionStmt.params.size) + ireturn() + } + .addMethod(PUBLIC, "bind", "(L$KLOX_INSTANCE;)L$KLOX_FUNCTION;") { + if (functionStmt.isBindable) { + aload_0() + invokevirtual(targetClass.name, "clone", "()Ljava/lang/Object;") + checkcast(targetClass.name) + dup() + aload_1() + putfield(targetClass.name, "this", "L$KLOX_INSTANCE;") + areturn() + } else { + throw_("java/lang/UnsupportedOperationException", "${functionStmt.name.lexeme} cannot be bound.") + } + } + .addMethod(PUBLIC, "getEnclosing", "()L$KLOX_CALLABLE;") { + aload_0() + getfield(targetClass.name, "__enclosing", "L$KLOX_CALLABLE;") + areturn() + } + .addMethod(PUBLIC, "capture", "(Ljava/lang/String;L$KLOX_CAPTURED_VAR;)V") { + aload_0() + getfield(targetClass.name, "__captured", "Ljava/util/Map;") + aload_1() + aload_2() + invokeinterface("java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") + pop() + return_() + } + .addMethod(PUBLIC, "getCaptured", "()Ljava/util/Map;") { + aload_0() + getfield(targetClass.name, "__captured", "Ljava/util/Map;") + areturn() + } + .addMethod(PUBLIC, "getOwner", "()L$KLOX_CLASS;") { + aload_0().getfield(targetClass.name, "__owner", "L$KLOX_CLASS;") + areturn() + } + .addMethod(PUBLIC, "setOwner", "(L$KLOX_CLASS;)V") { + aload_0() + aload_1() + putfield(targetClass.name, "__owner", "L$KLOX_CLASS;") + return_() + } + .addMethod(PUBLIC, "getCaptured", "(Ljava/lang/String;)L$KLOX_CAPTURED_VAR;") { + aload_0() + getfield(targetClass.name, "__captured", "Ljava/util/Map;") + aload_1() + invokeinterface("java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;") + checkcast(KLOX_CAPTURED_VAR) + areturn() + } + .addMethod(PUBLIC, "getReceiver", "()L$KLOX_INSTANCE;") { + if (functionStmt.isBindable) { + aload_0() + getfield(targetClass.name, "this", "L$KLOX_INSTANCE;") + areturn() + } else { + throw_("java/lang/UnsupportedOperationException", "${functionStmt.name.lexeme} cannot be bound.") + } + } + .addMethod(PUBLIC or VARARGS, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") + .addMethod(PUBLIC, "toString", "()Ljava/lang/String;") { + if (functionStmt.kind == NATIVE) { + ldc("") + } else { + ldc("") + } + areturn() + } + .apply { + if (InvokeDynamicCounter().count(functionStmt) > 0) { + addBootstrapMethod( + REF_INVOKE_STATIC, + KLOX_INVOKER, + "bootstrap", + "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;" + ) + } + if (functionStmt.isBindable) { + addInterface("java/lang/Cloneable") + addField(PRIVATE, "this", "L$KLOX_INSTANCE;") + addMethod(PUBLIC, "clone", "()Ljava/lang/Object;") { + aload_0() + invokespecial("java/lang/Object", "clone", "()Ljava/lang/Object;") + areturn() + } + } + programClassPool.addClass(programClass) + }.programClass + + return Pair(clazz, clazz.findMethod("invoke", null) as ProgramMethod) + } + } + + private val FunctionStmt.isBindable + get() = kind == FUNCTION || kind == METHOD || kind == INITIALIZER || kind == GETTER + + private fun initialize(programClassPool: ClassPool) = with(programClassPool) { + addClass( + ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC or ABSTRACT or INTERFACE, + KLOX_CALLABLE, + "java/lang/Object" + ) + .addMethod(PUBLIC or ABSTRACT, "getName", "()Ljava/lang/String;") + .addMethod(PUBLIC or ABSTRACT, "arity", "()I") + .addMethod(PUBLIC or ABSTRACT, "getEnclosing", "()L$KLOX_CALLABLE;") + .addMethod(PUBLIC or ABSTRACT or VARARGS, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") + .programClass + ) + + addClass( + ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC or ABSTRACT or INTERFACE, + KLOX_FUNCTION, + "java/lang/Object" + ) + .addInterface(KLOX_CALLABLE) + .addMethod(PUBLIC or ABSTRACT, "bind", "(L$KLOX_INSTANCE;)L$KLOX_FUNCTION;") + .addMethod(PUBLIC or ABSTRACT, "getReceiver", "()L$KLOX_INSTANCE;") + .addMethod(PUBLIC or ABSTRACT, "getOwner", "()L$KLOX_CLASS;") + .programClass + ) + + addClass( + ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC or ABSTRACT or INTERFACE, + KLOX_CLASS, + "java/lang/Object" + ) + .addInterface(KLOX_CALLABLE) + .addMethod(PUBLIC or ABSTRACT, "getSuperClass", "()L$KLOX_CLASS;") + .addMethod(PUBLIC or ABSTRACT, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + .programClass + ) + + addClass( + ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC, + KLOX_CAPTURED_VAR, + "java/lang/Object" + ) + .addField(PRIVATE or FINAL, "value", "Ljava/lang/Object;") + .addMethod(PUBLIC, "", "(Ljava/lang/Object;)V") { + aload_0() + dup() + invokespecial("java/lang/Object", "", "()V") + aload_1() + putfield(targetClass.name, "value", "Ljava/lang/Object;") + return_() + } + .addMethod(PUBLIC, "getValue", "()Ljava/lang/Object;") { + aload_0() + getfield(targetClass.name, "value", "Ljava/lang/Object;") + areturn() + } + .addMethod(PUBLIC, "setValue", "(Ljava/lang/Object;)V") { + aload_0() + aload_1() + putfield(targetClass.name, "value", "Ljava/lang/Object;") + return_() + } + .addMethod(PUBLIC, "toString", "()Ljava/lang/String;") { + concat( + { ldc("") } + ) + areturn() + } + .programClass + ) + + addClass( + ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC, + KLOX_INSTANCE, + "java/lang/Object" + ) + .addField(PRIVATE or FINAL, "klass", "L$KLOX_CLASS;") + .addField(PRIVATE or FINAL, "fields", "Ljava/util/Map;") + .addMethod(PUBLIC, "", "(L$KLOX_CLASS;)V") { + aload_0() + invokespecial("java/lang/Object", "", "()V") + aload_0() + aload_1() + putfield(targetClass.name, "klass", "L$KLOX_CLASS;") + aload_0() + new_("java/util/HashMap") + dup() + invokespecial("java/util/HashMap", "", "()V") + putfield(targetClass.name, "fields", "Ljava/util/Map;") + return_() + } + .addMethod(PUBLIC, "getKlass", "()L$KLOX_CLASS;") { + aload_0() + getfield(targetClass.name, "klass", "L$KLOX_CLASS;") + areturn() + } + .addMethod(PUBLIC, "get", "(Ljava/lang/String;)Ljava/lang/Object;") { + val (bind, end) = labels(2) + aload_0() + getfield(targetClass.name, "fields", "Ljava/util/Map;") + aload_1() + invokeinterface("java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;") + dup() + ifnonnull(end) + pop() + aload_0() + getfield(targetClass.name, "klass", "L$KLOX_CLASS;") + aload_1() + invokeinterface(KLOX_CLASS, "findMethod", "(Ljava/lang/String;)L$KLOX_FUNCTION;") + dup() + ifnonnull(bind) + pop() + throw_("java/lang/RuntimeException") { + concat( + { ldc("Undefined property '") }, + { aload_1() }, + { ldc("'.") } + ) + } + + label(bind) + checkcast(KLOX_FUNCTION) + aload_0() + invokeinterface(KLOX_FUNCTION, "bind", "(L$KLOX_INSTANCE;)L$KLOX_FUNCTION;") + + label(end) + areturn() + } + .addMethod(PUBLIC, "set", "(Ljava/lang/String;Ljava/lang/Object;)V") { + aload_0() + getfield(targetClass.name, "fields", "Ljava/util/Map;") + aload_1() + aload_2() + invokeinterface("java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") + pop() + return_() + } + .addMethod(PUBLIC, "toString", "()Ljava/lang/String;") { + concat( + { aload_0().getfield(targetClass.name, "klass", "L$KLOX_CLASS;") }, + { ldc(" instance") } + ) + areturn() + } + .programClass + ) + + addClass( + ClassBuilder( + CLASS_VERSION_1_8, + PUBLIC, + KLOX_INVOKER, + "java/lang/Object" + ) + .addMethod(PUBLIC, "", "()V") { + aload_0() + invokespecial("java/lang/Object", "", "()V") + return_() + } + .addMethod(PUBLIC or STATIC or VARARGS, "invoke", "(L$KLOX_CALLABLE;[Ljava/lang/Object;)Ljava/lang/Object;") { + val (nonNull, nonNegativeArity, correctArity) = labels(3) + aload_0() + ifnonnull(nonNull) + throw_("java/lang/Exception", "Can only call functions and classes.") + + label(nonNull) + aload_0() + invokeinterface(KLOX_CALLABLE, "arity", "()I") + iconst_m1() + ificmpne(nonNegativeArity) + // Getter has arity -1 + aload_0() + iconst_0() + anewarray("java/lang/Object") + invokeinterface(KLOX_CALLABLE, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") + areturn() + + label(nonNegativeArity) + aload_0() + invokeinterface(KLOX_CALLABLE, "arity", "()I") + dup() + istore(4) + aload_1() + arraylength() + dup() + istore(5) + ificmpeq(correctArity) + throw_("java/lang/RuntimeException") { + concat( + { ldc("Expected ") }, + { iload(4).box("java/lang/Integer") }, + { ldc(" arguments but got ") }, + { iload(5).box("java/lang/Integer") }, + { ldc(".") } + ) + } + + label(correctArity) + aload_0() + aload_1() + invokeinterface(KLOX_CALLABLE, "invoke", "([Ljava/lang/Object;)Ljava/lang/Object;") + areturn() + } + .addMethod(PUBLIC or STATIC, "bootstrap", "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;") { + val (default, multipleParams, createCallSite) = labels(3) + aload_2() + invokevirtual("java/lang/invoke/MethodType", "parameterCount", "()I") + istore_3() + ldc("invoke") + aload_1() + invokevirtual("java/lang/String", "equals", "(Ljava/lang/Object;)Z") + ifeq(default) + aload_0() + ldc(targetClass) + aload_1() + ldc(LibraryClass(PUBLIC, "java/lang/Object", null)) + ldc(programClassPool.getClass(KLOX_CALLABLE)) + iconst_1() + anewarray("java/lang/Class") + dup() + iconst_0() + ldc(LibraryClass(PUBLIC, "[Ljava/lang/Object;", "java/lang/Object")) + aastore() + invokestatic("java/lang/invoke/MethodType", "methodType", "(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;") + invokevirtual("java/lang/invoke/MethodHandles\$Lookup", "findStatic", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;") + astore(4) + iload_3() + iconst_1() + ificmpne(multipleParams) + // single param + aload(4) + dup() + invokevirtual("java/lang/invoke/MethodHandle", "type", "()Ljava/lang/invoke/MethodType;") + iconst_1() + iconst_2() + invokevirtual("java/lang/invoke/MethodType", "dropParameterTypes", "(II)Ljava/lang/invoke/MethodType;") + invokevirtual("java/lang/invoke/MethodHandle", "asType", "(Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;") + astore(4) + goto_(createCallSite) + + label(multipleParams) + aload(4) + dup() + invokevirtual("java/lang/invoke/MethodHandle", "type", "()Ljava/lang/invoke/MethodType;") + iconst_1() + invokevirtual("java/lang/invoke/MethodType", "parameterType", "(I)Ljava/lang/Class;") + invokevirtual("java/lang/invoke/MethodHandle", "asVarargsCollector", "(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandle;") + astore(4) + + label(createCallSite) + aload(4) + aload_2() + invokevirtual("java/lang/invoke/MethodHandle", "asType", "(Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;") + astore(4) + new_("java/lang/invoke/ConstantCallSite") + dup() + aload(4) + invokespecial("java/lang/invoke/ConstantCallSite", "", "(Ljava/lang/invoke/MethodHandle;)V") + areturn() + + label(default) + throw_("java/lang/RuntimeException") { + concat( + { ldc("Invalid dynamic method call '") }, + { aload_1() }, + { ldc("'.") } + ) + } + } + .programClass + ) + } + + companion object { + const val KLOX_CALLABLE = "klox/KLoxCallable" + const val KLOX_FUNCTION = "klox/KLoxFunction" + const val KLOX_CLASS = "klox/KLoxClass" + const val KLOX_INSTANCE = "klox/KLoxInstance" + const val KLOX_CAPTURED_VAR = "klox/CapturedVar" + const val KLOX_INVOKER = "klox/Invoker" + } +} diff --git a/src/main/kotlin/eu/jameshamilton/klox/compile/ComposerExt.kt b/src/main/kotlin/eu/jameshamilton/klox/compile/ComposerExt.kt new file mode 100644 index 0000000..600f4ab --- /dev/null +++ b/src/main/kotlin/eu/jameshamilton/klox/compile/ComposerExt.kt @@ -0,0 +1,398 @@ +package eu.jameshamilton.klox.compile + +import eu.jameshamilton.klox.compile.Compiler.Companion.KLOX_CAPTURED_VAR +import eu.jameshamilton.klox.compile.Resolver.Companion.isCaptured +import eu.jameshamilton.klox.compile.Resolver.Companion.isGlobalLateInit +import eu.jameshamilton.klox.compile.Resolver.Companion.javaName +import eu.jameshamilton.klox.compile.Resolver.Companion.slot +import eu.jameshamilton.klox.debug +import eu.jameshamilton.klox.parse.FunctionStmt +import eu.jameshamilton.klox.parse.VarDef +import eu.jameshamilton.klox.programClassPool +import proguard.classfile.AccessConstants.PUBLIC +import proguard.classfile.AccessConstants.STATIC +import proguard.classfile.ProgramClass +import proguard.classfile.VersionConstants.CLASS_VERSION_1_8 +import proguard.classfile.attribute.CodeAttribute +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.CodeAttributeComposer +import proguard.classfile.editor.CompactCodeAttributeComposer.Label +import proguard.classfile.util.ClassUtil.internalPrimitiveTypeFromNumericClassName +import proguard.classfile.editor.CompactCodeAttributeComposer as Composer + +fun Composer.labels(n: Int): List