+
Skip to content

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

Merged
merged 6 commits into from
Jul 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/main/kotlin/compiler/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -655,14 +655,15 @@ class Parser(inputTokens: List<Token>) {
// Parse lambda: { var, var -> ... } or { ... }
private fun pLambdaFun(): N_EXPR? {
consume(T_BRACE_OPEN)?.also {
var done = false
val args = mutableListOf<N_IDENTIFIER>()
while (!done) {
done = true
while (nextAre(T_IDENTIFIER, T_COMMA)) {
consume(T_IDENTIFIER)?.also { args.add(N_IDENTIFIER(it.string)) }
consume(T_COMMA)
}
while (nextAre(T_IDENTIFIER, T_ARROW)) {
Copy link
Owner Author

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.

consume(T_IDENTIFIER)?.also { args.add(N_IDENTIFIER(it.string)) }
consume(T_COMMA)?.also { done = false }
}
if (args.isNotEmpty()) consume(T_ARROW) ?: fail("missing arrow after function literal var declaration")
if (args.isNotEmpty()) consume(T_ARROW) ?: fail("missing arrow after function var declaration")
val code = mutableListOf<N_STATEMENT>()
while (!nextIs(T_BRACE_CLOSE)) {
pStatement()?.also { code.add(it) } ?: fail("non-statement in braces")
Expand Down
15 changes: 11 additions & 4 deletions src/main/kotlin/server/mcp/MCP.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,17 @@ object MCP {
timeMap.remove(task.timeID)
taskMap.remove(task.id)
val result = task.execute()
if (result is Task.Result.Suspend) {
task.setTime(result.seconds)
timeMap[task.timeID] = task
taskMap[task.id] = task
when (result) {
is Task.Result.Suspend -> {
task.setTime(result.seconds)
timeMap[task.timeID] = task
taskMap[task.id] = task
}
is Task.Result.Failed -> {
task.connection?.sendText(result.e.toString())
task.connection?.sendText(task.stackDump())
}
is Task.Result.Finished -> { }
}
task = getNextTask()
}
Expand Down
37 changes: 26 additions & 11 deletions src/main/kotlin/server/mcp/Task.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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
Copy link
Owner Author

Choose a reason for hiding this comment

The 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) {
Expand All @@ -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.
Copy link
Owner Author

@gilmore606 gilmore606 Jul 12, 2025

Choose a reason for hiding this comment

The 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>,
Expand All @@ -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,
Expand Down
44 changes: 42 additions & 2 deletions src/main/kotlin/value/VList.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Copy link
Owner Author

Choose a reason for hiding this comment

The 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() }
})
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/vm/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.dlfsystems.yegg.vm

import com.dlfsystems.yegg.server.mcp.Task
import com.dlfsystems.yegg.value.VObj
import com.dlfsystems.yegg.value.Value


interface Context {
Expand All @@ -15,4 +16,6 @@ interface Context {
var ticksLeft: Int
var callsLeft: Int

fun executeForResult(exe: Executable, args: List<Value>): Value

}
11 changes: 7 additions & 4 deletions src/main/kotlin/vm/VM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class VM(
// Used by optimizer opcodes.
private var dropReturnValue: Boolean = false

// Value of this execution as an expression. Returned if we have no other return value.
private var exprValue: Value = VVoid

private inline fun push(v: Value) = stack.addFirst(v)
private inline fun peek() = stack.first()
private inline fun pop() = stack.removeFirst()
Expand Down Expand Up @@ -95,7 +98,7 @@ class VM(
when (word.opcode) {

O_DISCARD -> {
if (stack.isNotEmpty()) pop()
if (stack.isNotEmpty()) exprValue = pop()
}

// Value ops
Expand Down Expand Up @@ -165,10 +168,10 @@ class VM(
O_JUMP -> {
val addr = next().address!!
// Unresolved jump dest means end-of-code
if (addr >= 0) pc = addr else return Result.Return(VVoid)
if (addr >= 0) pc = addr else return Result.Return(exprValue)
}
O_RETURN -> {
if (stack.isEmpty()) return Result.Return(VVoid)
if (stack.isEmpty()) return Result.Return(exprValue)
if (stack.size > 1) fail(E_SYS, "stack polluted on return! ${dumpStack()}")
return Result.Return(pop())
}
Expand Down Expand Up @@ -397,7 +400,7 @@ class VM(
else -> fail(E_SYS, "unknown opcode $word")
}
}
return Result.Return(if (stack.isEmpty()) VVoid else pop())
return Result.Return(if (stack.isEmpty()) exprValue else pop())
}

}
48 changes: 48 additions & 0 deletions src/test/kotlin/LangTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:

@Test
fun `List of mixed value types sortedBy()`() = yeggTest {
    runForOutput($$"""
        foo = [0, "b", 1, true, "a", 2, false].sortedBy({ it })
        for (x in foo) notifyConn("$x")
    """, """
        0
        false
        1
        true
        2
        a
        b
    """)
}

I'm not sure that is how it would shake out, but hopefully it illustrates what I'm talking about.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I added a test for this!

""", """
me
egg
beer
cheese
""")
}
}
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载