Skip to content

Commit

Permalink
Add JVM backend
Browse files Browse the repository at this point in the history
  • Loading branch information
mrjameshamilton committed Oct 20, 2021
1 parent 9c2986e commit 9679121
Show file tree
Hide file tree
Showing 27 changed files with 3,224 additions and 398 deletions.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,46 @@

![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

```shell
./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
Expand Down
15 changes: 10 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,30 @@ 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")
}

tasks.test {
useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}

Expand Down
155 changes: 123 additions & 32 deletions src/main/kotlin/eu/jameshamilton/klox/Main.kt
Original file line number Diff line number Diff line change
@@ -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<String>) {
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<String>())
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/eu/jameshamilton/klox/compile/Builtins.kt
Original file line number Diff line number Diff line change
@@ -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<Parameter>, val code: Composer.() -> Composer) :
FunctionStmt(name, NATIVE, params, emptyList()) {

override fun toString(): String = "<native fn>"
}
63 changes: 63 additions & 0 deletions src/main/kotlin/eu/jameshamilton/klox/compile/ClassBuilderExt.kt
Original file line number Diff line number Diff line change
@@ -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 <reified T : Method> 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<Int> = { 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
}
5 changes: 5 additions & 0 deletions src/main/kotlin/eu/jameshamilton/klox/compile/ClassPoolExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package eu.jameshamilton.klox.compile

import proguard.classfile.ClassPool

fun ClassPool.contains(className: String) = this.getClass(className) != null
Loading

0 comments on commit 9679121

Please sign in to comment.