-
Notifications
You must be signed in to change notification settings - Fork 0
Filter, Map, SortedBy #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4d95def
0eb3f4e
4bdee89
3c045a9
4f73f20
20deb02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import com.dlfsystems.yegg.util.NanoID | |
import com.dlfsystems.yegg.util.systemEpoch | ||
import com.dlfsystems.yegg.value.VObj | ||
import com.dlfsystems.yegg.value.VTask | ||
import com.dlfsystems.yegg.value.VVoid | ||
import com.dlfsystems.yegg.value.Value | ||
import com.dlfsystems.yegg.vm.* | ||
import com.dlfsystems.yegg.vm.VMException.Type.* | ||
|
@@ -47,21 +48,21 @@ class Task( | |
|
||
|
||
sealed interface Result { | ||
data object Finished: Result | ||
@JvmInline value class Suspend(val seconds: Int): Result | ||
data class Finished(val v: Value): Result | ||
data class Suspend(val seconds: Int): Result | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was very little actual optimization value in making these jvminline value classes to begin with, so I'm not doing it here anymore. |
||
data class Failed(val e: Exception): Result | ||
} | ||
|
||
// Execute the top stack frame. | ||
// On a Suspend, return Suspend upward to MCP. | ||
// On a Call, push a new stack frame. | ||
// On a Return, pop a stack frame, and save the return value to pass to the next iteration (the previous frame). | ||
// Continue until the stack is empty. | ||
|
||
fun execute(): Result { | ||
fun execute(toDepth: Int = 0): Result { | ||
var vReturn: Value? = resumeResult | ||
resumeResult = null | ||
try { | ||
while (stack.isNotEmpty()) { | ||
while (stack.size > toDepth) { | ||
stack.first().execute(vReturn).also { result -> | ||
vReturn = null | ||
when (result) { | ||
|
@@ -80,15 +81,28 @@ class Task( | |
} | ||
} | ||
} | ||
val result = Result.Finished(vReturn ?: VVoid) | ||
return result | ||
} catch (e: Exception) { | ||
connection?.sendText(e.toString()) | ||
connection?.sendText(stackDump()) | ||
return Result.Failed(e) | ||
} | ||
} | ||
|
||
// Execute an exe immediately for a return value. The task cannot suspend. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally i was making a whole new Task to execute lambdas from the builtins, but I realized I could trick the existing Task into stopping at the depth of the new lambda exe and returning me the value. |
||
// Used by system functions to call verb code in an existing Task. | ||
override fun executeForResult(exe: Executable, args: List<Value>): Value { | ||
push(vThis, exe, args) | ||
execute(toDepth = stack.size - 1).also { result -> | ||
when (result) { | ||
is Result.Suspend -> fail(E_LIMIT, "cannot suspend in verb called by system") | ||
is Result.Failed -> throw result.e | ||
is Result.Finished -> return result.v | ||
} | ||
} | ||
return Result.Finished | ||
return VVoid | ||
} | ||
|
||
// Add a VM to the stack to run an exe. | ||
fun push( | ||
private fun push( | ||
vThis: VObj, | ||
exe: Executable, | ||
args: List<Value>, | ||
|
@@ -99,12 +113,13 @@ class Task( | |
) | ||
} | ||
|
||
fun pop(): VM { | ||
private fun pop(): VM { | ||
return stack.removeFirst() | ||
} | ||
|
||
fun stackDump() = stack.joinToString(prefix = "...", separator = "\n...", postfix = "\n") | ||
|
||
|
||
companion object { | ||
fun make( | ||
exe: Executable, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,8 +44,8 @@ data class VList(var v: MutableList<Value> = mutableListOf()): Value() { | |
private fun propSorted(): VList { | ||
if (v.isEmpty()) return make(v) | ||
return make(when (v[0]) { | ||
is VInt -> v.sortedBy { (it as VInt).v } | ||
is VFloat -> v.sortedBy { (it as VFloat).v } | ||
is VInt -> v.sortedBy { (it as? VInt)?.v ?: 0 } | ||
is VFloat -> v.sortedBy { (it as? VFloat)?.v ?: 0f } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated bugfix for sorting mixed lists -- if you sort a list of mixed value types we do our best and assume you want them sorted by the value semantics of the first element. |
||
else -> v.sortedBy { it.asString() } | ||
}) | ||
} | ||
|
@@ -109,6 +109,10 @@ data class VList(var v: MutableList<Value> = mutableListOf()): Value() { | |
"clear" -> verbClear(args) | ||
"reverse" -> verbReverse(args) | ||
"shuffle" -> verbShuffle(args) | ||
"first" -> verbFirst(c, args) | ||
"filter" -> verbFilter(c, args) | ||
"map" -> verbMap(c, args) | ||
"sortedBy" -> verbSortedBy(c, args) | ||
else -> null | ||
} | ||
|
||
|
@@ -222,6 +226,42 @@ data class VList(var v: MutableList<Value> = mutableListOf()): Value() { | |
return VVoid | ||
} | ||
|
||
private fun verbFirst(c: Context, args: List<Value>): Value { | ||
requireArgCount(args, 1, 1) | ||
if (args[0] !is VFun) fail(E_TYPE, "${args[0].type} is not FUN") | ||
v.forEach { ele -> | ||
c.executeForResult(args[0] as VFun, listOf(ele)).also { | ||
if (it.isTrue()) return ele | ||
} | ||
} | ||
return VVoid | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This caught my eye because I would normally expect to get back a null in this situation. Seeing this is making me wonder about the reasons languages distinguish between void/unit and nullable types in situations like this. I wonder if it's related to the problem you're dealing with in this PR. Would that distinction allow you to explicitly signal when a value should not be popped from the stack after an execution? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually an issue I've been back-of-head thinking about for a while that I will probably need to nail down; we should chat about this over booze or something sometime. :) Basically, VVoid is acting as both "null" and "Unit" (in Kotlin-speak) in yeggcode right now -- it's what we return when there's nothing to return. So when I have to return some value to something in yeggcode I default to VVoid. What this means is, when you're writing yeggcode and you call someList.first({ }) and there's no match, you DO get back "null" -- VVoid -- the only null-type value a Value can be. I have a vague idea that I'll want Kotlin type semantics around "do a thing if a value is non-VVoid" but I haven't worked anything out yet. |
||
} | ||
|
||
private fun verbFilter(c: Context, args: List<Value>): Value { | ||
requireArgCount(args, 1, 1) | ||
if (args[0] !is VFun) fail(E_TYPE, "${args[0].type} is not FUN") | ||
return make(v.filter { c.executeForResult(args[0] as VFun, listOf(it)).isTrue() }) | ||
} | ||
|
||
private fun verbMap(c: Context, args: List<Value>): Value { | ||
requireArgCount(args, 1, 1) | ||
if (args[0] !is VFun) fail(E_TYPE, "${args[0].type} is not FUN") | ||
return make(v.map { c.executeForResult(args[0] as VFun, listOf(it)) }) | ||
} | ||
|
||
private fun verbSortedBy(c: Context, args: List<Value>): Value { | ||
requireArgCount(args, 1, 1) | ||
if (args[0] !is VFun) fail(E_TYPE, "${args[0].type} is not FUN") | ||
if (v.isEmpty()) return make(v) | ||
val pairs = v.map { it to c.executeForResult(args[0] as VFun, listOf(it)) } | ||
return make(when (pairs[0].second) { | ||
is VInt -> pairs.sortedBy { (it.second as? VInt)?.v ?: 0 } | ||
is VFloat -> pairs.sortedBy { (it.second as? VFloat)?.v ?: 0f } | ||
else -> pairs.sortedBy { it.second.asString() } | ||
Comment on lines
+258
to
+260
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is pretty interesting 😎 It took me a minute to convince myself that it should work for all of your value types. |
||
}.map { it.first }) | ||
} | ||
|
||
// Check bounds of passed arg as a position in this list. | ||
private fun positionArg(arg: Value): Int { | ||
if (arg.type != Type.INT) fail(E_TYPE, "invalid ${arg.type} list position") | ||
val pos = (arg as VInt).v | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -184,4 +184,52 @@ class LangTest: YeggTest() { | |
""") | ||
} | ||
|
||
@Test | ||
fun `Mixed list sorted by first value type`() = yeggTest { | ||
runForOutput($$""" | ||
foo = [36, 9, "hello", 88, 5.6] | ||
notifyConn(foo.sorted) | ||
""", """ | ||
"hello", 5.6, 9, 36, 88 | ||
""") | ||
} | ||
|
||
@Test | ||
fun `List filter`() = yeggTest { | ||
runForOutput($$""" | ||
foo = [1,5,7,12,26,31,74].filter({ it % 2 == 0 }) | ||
notifyConn(foo) | ||
""", """ | ||
12, 26, 74 | ||
""") | ||
} | ||
|
||
@Test | ||
fun `List map`() = yeggTest { | ||
verb("sys", "resultOf", $$""" | ||
[input] = args | ||
return input * 10 | ||
""") | ||
runForOutput($$""" | ||
foo = [1,3,5].map({ "got ${$sys.resultOf(it)}" }) | ||
for (x in foo) notifyConn("$x") | ||
""", """ | ||
got 10 | ||
got 30 | ||
got 50 | ||
""") | ||
} | ||
|
||
@Test | ||
fun `List sortedBy`() = yeggTest { | ||
runForOutput($$""" | ||
foo = ["beer", "egg", "cheese", "me"].sortedBy({ it.length }) | ||
for (x in foo) notifyConn("$x") | ||
Comment on lines
+224
to
+227
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might already have something liek this, but it would be nice to have a test with a list of mixed value types to make sure they're ordered as expected. Something like:
I'm not sure that is how it would shake out, but hopefully it illustrates what I'm talking about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, I added a test for this! |
||
""", """ | ||
me | ||
egg | ||
beer | ||
cheese | ||
""") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bugfix for parsing lambdas -- the argless form { ... } could not be parsed if it started with an identifier, for instance { x > 10 } -- the parser saw this as an incomplete arg declaration.