From 4bbfede4060d38083aafd8a6c7ab2847e0e55ebf Mon Sep 17 00:00:00 2001 From: nea Date: Mon, 21 Aug 2023 16:15:59 +0200 Subject: method binding --- TestOutput.xml | 23 ++++++++ src/CoreBindings.kt | 2 +- src/ErrorReporter.kt | 8 +++ src/LispErrorReporter.kt | 15 ------ src/LispExecutionContext.kt | 1 - src/LispParser.kt | 5 ++ src/bind/AutoBinder.kt | 124 ++++++++++++++++++++++++++++++++++++++++++++ src/bind/LispBinding.kt | 7 +++ test/res/scratch.lisp | 2 + test/src/TestLisp.kt | 15 +++++- 10 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 TestOutput.xml create mode 100644 src/ErrorReporter.kt delete mode 100644 src/LispErrorReporter.kt create mode 100644 src/bind/AutoBinder.kt create mode 100644 src/bind/LispBinding.kt diff --git a/TestOutput.xml b/TestOutput.xml new file mode 100644 index 0000000..aaef8ea --- /dev/null +++ b/TestOutput.xml @@ -0,0 +1,23 @@ + hello world ++ 16.2 +- -2.0 +* 100.0 +/ 0.16666666666666666 +============ +left evaluated +truthy value +left evaluated +falsey value +============ +Error: Could not resolve variable sc at /home/nea/src/nealisp/build/resources/test/scratch.lisp:21:30 until 21:32 +This should fail nil +This should work 42.0 +============ +Running tests +This should be 1.0 1.0 +]]> \ No newline at end of file diff --git a/src/CoreBindings.kt b/src/CoreBindings.kt index 94b3852..b90072e 100644 --- a/src/CoreBindings.kt +++ b/src/CoreBindings.kt @@ -15,7 +15,7 @@ object CoreBindings { return@externalRawCall stackFrame.setValueLocal(name.label, context.resolveValue(stackFrame, value)) } - private fun isTruthy(data: LispData): Boolean? { + fun isTruthy(data: LispData): Boolean? { if (data == trueValue) return true if (data == falseValue) return false return null diff --git a/src/ErrorReporter.kt b/src/ErrorReporter.kt new file mode 100644 index 0000000..b4ba636 --- /dev/null +++ b/src/ErrorReporter.kt @@ -0,0 +1,8 @@ +package moe.nea.lisp + +interface ErrorReporter { + fun reportError(string: String): LispData + fun reportError(string: String, exception: Throwable): LispData { + return reportError("$string: $exception") + } +} \ No newline at end of file diff --git a/src/LispErrorReporter.kt b/src/LispErrorReporter.kt deleted file mode 100644 index d9ad148..0000000 --- a/src/LispErrorReporter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package moe.nea.lisp - -class LispErrorReporter { - - data class LispError(val name: String, val position: LispPosition) - - - val errors = listOf() - - fun reportError(name: String, position: HasLispPosition) { - println("LISP ERROR: $name at ${position.position}") - } - - -} diff --git a/src/LispExecutionContext.kt b/src/LispExecutionContext.kt index 6398cc4..306e111 100644 --- a/src/LispExecutionContext.kt +++ b/src/LispExecutionContext.kt @@ -2,7 +2,6 @@ package moe.nea.lisp class LispExecutionContext() { - private val errorReporter = LispErrorReporter() val rootStackFrame = StackFrame(null) val unloadedModules = mutableMapOf() val modules = mutableMapOf>() diff --git a/src/LispParser.kt b/src/LispParser.kt index 3341779..48d0d45 100644 --- a/src/LispParser.kt +++ b/src/LispParser.kt @@ -10,10 +10,15 @@ class LispParser private constructor(filename: String, string: String) { fun parse(filename: String, string: String): LispAst.Program { return LispParser(filename, string).program } + fun parse(file: File): LispAst.Program { return parse(file.absolutePath, file.readText()) } + fun isValidIdentifier(name: String): Boolean { + return name.isNotEmpty() && name.first() in validStartingIdentifiers && name.all { it in validIdentifiers } + } + val digits = "1234567890" val hexDigits = digits + "abcdefABCDEF" val alphabet = "abcdefghijklmnopqrstuvwxyz" diff --git a/src/bind/AutoBinder.kt b/src/bind/AutoBinder.kt new file mode 100644 index 0000000..83d5d86 --- /dev/null +++ b/src/bind/AutoBinder.kt @@ -0,0 +1,124 @@ +package moe.nea.lisp.bind + +import moe.nea.lisp.* +import java.lang.invoke.MethodHandles +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.lang.reflect.Parameter + +class AutoBinder { + + private fun mapLispData(parameter: Parameter): ((Iterator, ErrorReporter) -> Any)? { + if (LispData::class.java.isAssignableFrom(parameter.type)) return { a, b -> parameter.type.cast(a.next()) } + return null + } + + private fun mapErrorReporter(parameter: Parameter): ((Iterator, ErrorReporter) -> Any)? { + if (ErrorReporter::class.java.isAssignableFrom(parameter.type)) return { a, b -> b } + return null + } + + private fun mapString(parameter: Parameter): ((Iterator, ErrorReporter) -> Any?)? { + if (String::class.java == parameter.type) return { a, b -> + when (val x = a.next()) { + is LispData.LispString -> x.string + is LispData.Atom -> x.label + else -> null.also { b.reportError("Could not coerce $x to string") } + } + } + return null + } + + private fun mapBoolean(parameter: Parameter): ((Iterator, ErrorReporter) -> Any?)? { + if (Boolean::class.java.isAssignableFrom(parameter.type)) return { a, b -> + val x = a.next() + val y = CoreBindings.isTruthy(x) + if (y == null) { + b.reportError("Could not coerce $x to a boolean") + } + y + } + return null + } + + private fun mapNumber(parameter: Parameter): ((Iterator, ErrorReporter) -> Any?)? { + if (Double::class.java.isAssignableFrom(parameter.type)) return { a, b -> + when (val x = a.next()) { + is LispData.LispNumber -> x.value + else -> null.also { b.reportError("Could not coerce $x to number") } + } + } + if (Float::class.java.isAssignableFrom(parameter.type)) return { a, b -> + when (val x = a.next()) { + is LispData.LispNumber -> x.value.toFloat() + else -> null.also { b.reportError("Could not coerce $x to number") } + } + } + if (Int::class.java.isAssignableFrom(parameter.type)) return { a, b -> + when (val x = a.next()) { + is LispData.LispNumber -> x.value.toInt() + else -> null.also { b.reportError("Could not coerce $x to number") } + } + } + if (Long::class.java.isAssignableFrom(parameter.type)) return { a, b -> + when (val x = a.next()) { + is LispData.LispNumber -> x.value.toLong() + else -> null.also { b.reportError("Could not coerce $x to number") } + } + } + return null + } + + + val objectMappers = mutableListOf<((Parameter) -> (((Iterator, ErrorReporter) -> Any?)?))>( + ::mapLispData, + ::mapErrorReporter, + ::mapNumber, + ::mapString, + ::mapBoolean, + ) + + + fun generateInstanceBindings(obj: Any): Map { + val bindings = mutableMapOf() + for (method in obj.javaClass.methods) { + val annotation = method.getAnnotation(LispBinding::class.java) + if (annotation == null) continue + require(LispParser.isValidIdentifier(annotation.name)) + bindings[annotation.name] = wrapMethod(obj, annotation.name, method) + } + // TODO: field bindings + return bindings + } + + private val lookup = MethodHandles.publicLookup() + fun wrapMethod(obj: Any, name: String, method: Method): LispData.LispExecutable { + var mh = lookup.unreflect(method) + if (method.modifiers and Modifier.STATIC == 0) { + mh = mh.bindTo(obj) + } + val objectMappers = method.parameters.map { param -> + objectMappers.firstNotNullOfOrNull { it.invoke(param) } + ?: error("Could not find object mapper for parameter $param") + } + return LispData.externalCall(name) { args, fReportError -> + try { + val iterator = args.iterator() + val e = object : ErrorReporter { + override fun reportError(string: String): LispData { + return fReportError(string) + } + } + val p = objectMappers.map { it.invoke(iterator, e) ?: return@externalCall LispData.LispNil } + if (iterator.hasNext()) return@externalCall fReportError("Too many arguments") + mh.invokeWithArguments(p) as LispData + } catch (e: Exception) { + fReportError("$name threw an exception: $e") + } + } + } + + fun bindTo(obj: Any, frame: StackFrame) { + frame.variables.putAll(generateInstanceBindings(obj)) + } +} \ No newline at end of file diff --git a/src/bind/LispBinding.kt b/src/bind/LispBinding.kt new file mode 100644 index 0000000..7af0e67 --- /dev/null +++ b/src/bind/LispBinding.kt @@ -0,0 +1,7 @@ +package moe.nea.lisp.bind + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION) +annotation class LispBinding( + val name: String, +) diff --git a/test/res/scratch.lisp b/test/res/scratch.lisp index 3a1fa66..667f903 100644 --- a/test/res/scratch.lisp +++ b/test/res/scratch.lisp @@ -24,3 +24,5 @@ (debuglog "============") (debuglog "Running tests") + +(debuglog "This should be 1.0" (funny-method 1.1 "test" false)) \ No newline at end of file diff --git a/test/src/TestLisp.kt b/test/src/TestLisp.kt index 1f287ca..205bea2 100644 --- a/test/src/TestLisp.kt +++ b/test/src/TestLisp.kt @@ -1,15 +1,27 @@ +import moe.nea.lisp.LispData import moe.nea.lisp.LispExecutionContext import moe.nea.lisp.LispParser import moe.nea.lisp.TestResultFormatter +import moe.nea.lisp.bind.AutoBinder +import moe.nea.lisp.bind.LispBinding import java.io.File import javax.xml.stream.XMLOutputFactory import kotlin.system.exitProcess object T +object TestBindings { + @LispBinding("funny-method") + fun funnyMethod(arg: Int, test: String, boolean: Boolean): LispData { + if (boolean) + println("From java: $test") + return LispData.LispNumber(arg.toDouble()) + } + +} fun main() { - val otherP = LispParser.parse(File(T::class.java.getResource("/test.lisp")!!.file)) + val otherP = LispParser.parse(File(T::class.java.getResource("/scratch.lisp")!!.file)) val executionContext = LispExecutionContext() executionContext.setupStandardBindings() executionContext.registerModule( @@ -17,6 +29,7 @@ fun main() { LispParser.parse(File(T::class.java.getResource("/secondary.lisp")!!.file)) ) val bindings = executionContext.genBindings() + AutoBinder().bindTo(TestBindings, bindings) val testResults = executionContext.runTests(otherP, "Test", bindings) val w = XMLOutputFactory.newFactory() .createXMLStreamWriter(File("TestOutput.xml").bufferedWriter()) -- cgit