summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-08-21 16:15:59 +0200
committernea <nea@nea.moe>2023-08-21 16:15:59 +0200
commit4bbfede4060d38083aafd8a6c7ab2847e0e55ebf (patch)
treef0ab50180d2fa1c872ba8c20a4b055cd87bed3c9
parentde694f30b5e2f6fd62a00e5a622abe0a2411afb9 (diff)
downloadnealisp-4bbfede4060d38083aafd8a6c7ab2847e0e55ebf.tar.gz
nealisp-4bbfede4060d38083aafd8a6c7ab2847e0e55ebf.tar.bz2
nealisp-4bbfede4060d38083aafd8a6c7ab2847e0e55ebf.zip
method binding
-rw-r--r--TestOutput.xml23
-rw-r--r--src/CoreBindings.kt2
-rw-r--r--src/ErrorReporter.kt8
-rw-r--r--src/LispErrorReporter.kt15
-rw-r--r--src/LispExecutionContext.kt1
-rw-r--r--src/LispParser.kt5
-rw-r--r--src/bind/AutoBinder.kt124
-rw-r--r--src/bind/LispBinding.kt7
-rw-r--r--test/res/scratch.lisp2
-rw-r--r--test/src/TestLisp.kt15
10 files changed, 184 insertions, 18 deletions
diff --git a/TestOutput.xml b/TestOutput.xml
new file mode 100644
index 0000000..aaef8ea
--- /dev/null
+++ b/TestOutput.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" ?><testsuites><testsuite name="Test" tests="0" skipped="0" failures="0" errors="0" timestamp="2023-08-21T04:12:42"><properties></properties><system-out><![CDATA[Hello, World, here is an atom: :iamanatom
+:myfunworks
+:atom
+a :test
+... [:work, :whatever]
+<native function pure.r> 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
+]]></system-out></testsuite></testsuites> \ 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<LispError>()
-
- 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<String, LispAst.Program>()
val modules = mutableMapOf<String, Map<String, LispData>>()
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<LispData>, 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<LispData>, ErrorReporter) -> Any)? {
+ if (ErrorReporter::class.java.isAssignableFrom(parameter.type)) return { a, b -> b }
+ return null
+ }
+
+ private fun mapString(parameter: Parameter): ((Iterator<LispData>, 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<LispData>, 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<LispData>, 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<LispData>, ErrorReporter) -> Any?)?))>(
+ ::mapLispData,
+ ::mapErrorReporter,
+ ::mapNumber,
+ ::mapString,
+ ::mapBoolean,
+ )
+
+
+ fun generateInstanceBindings(obj: Any): Map<String, LispData> {
+ val bindings = mutableMapOf<String, LispData>()
+ 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())