Compare commits

..

No commits in common. "9b90fe370b51c88a7a08eb8fb146da61fae2b302" and "ef95ed440571d831120952c8c5f9de750206a56b" have entirely different histories.

24 changed files with 89 additions and 719 deletions

View File

@ -18,7 +18,6 @@
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`. - Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
- Do not use `statement { ... }` or other inline suspend lambdas in compiler hot paths (e.g., parsing/var declarations, initializer thunks). - Do not use `statement { ... }` or other inline suspend lambdas in compiler hot paths (e.g., parsing/var declarations, initializer thunks).
- If you need a wrapper for delegated properties, check for `getValue` explicitly and return a concrete `Statement` object when missing; avoid `onNotFoundResult` lambdas. - If you need a wrapper for delegated properties, check for `getValue` explicitly and return a concrete `Statement` object when missing; avoid `onNotFoundResult` lambdas.
- For any code in `commonMain`, verify it is Kotlin Multiplatform compatible before finishing. Do not use JVM-only APIs or Java-backed convenience methods such as `Map.putIfAbsent`; prefer stdlib/common equivalents and run at least the relevant compile/test task that exercises the `commonMain` source set.
- If wasmJs browser tests hang, first run `:lynglib:wasmJsNodeTest` and look for wasm compilation errors; hangs usually mean module instantiation failed. - If wasmJs browser tests hang, first run `:lynglib:wasmJsNodeTest` and look for wasm compilation errors; hangs usually mean module instantiation failed.
- Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead. - Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead.

View File

@ -1,4 +1,4 @@
# Lyng CLI (`lyng`) ### Lyng CLI (`lyng`)
The Lyng CLI is the reference command-line tool for the Lyng language. It lets you: The Lyng CLI is the reference command-line tool for the Lyng language. It lets you:
@ -8,7 +8,7 @@ The Lyng CLI is the reference command-line tool for the Lyng language. It lets y
- Format Lyng source files via the built-in `fmt` subcommand. - Format Lyng source files via the built-in `fmt` subcommand.
## Building on Linux #### Building on Linux
Requirements: Requirements:
- JDK 17+ (for Gradle and the JVM distribution) - JDK 17+ (for Gradle and the JVM distribution)
@ -20,7 +20,7 @@ The repository provides convenience scripts in `bin/` for local builds and insta
Note: In this repository the scripts are named `bin/local_release` and `bin/local_jrelease`. In some environments these may be aliased as `bin/release` and `bin/jrelease`. The steps below use the actual file names present here. Note: In this repository the scripts are named `bin/local_release` and `bin/local_jrelease`. In some environments these may be aliased as `bin/release` and `bin/jrelease`. The steps below use the actual file names present here.
### Option A: Native linuxX64 executable (`lyng`) ##### Option A: Native linuxX64 executable (`lyng`)
1) Build the native binary: 1) Build the native binary:
@ -39,7 +39,7 @@ What this does:
- Produces `distributables/lyng-linuxX64.zip` containing the `lyng` executable. - Produces `distributables/lyng-linuxX64.zip` containing the `lyng` executable.
### Option B: JVM distribution (`jlyng` launcher) ##### Option B: JVM distribution (`jlyng` launcher)
This creates a JVM distribution with a launcher script, packages it as a downloadable zip, and links it to `~/bin/jlyng`. This creates a JVM distribution with a launcher script, packages it as a downloadable zip, and links it to `~/bin/jlyng`.
@ -54,12 +54,12 @@ What this does:
- Creates a symlink `~/bin/jlyng` pointing to the launcher script. - Creates a symlink `~/bin/jlyng` pointing to the launcher script.
## Usage #### Usage
Once installed, ensure `~/bin` is on your `PATH`. You can then use either the native `lyng` or the JVM `jlyng` launcher (both have the same CLI surface). Once installed, ensure `~/bin` is on your `PATH`. You can then use either the native `lyng` or the JVM `jlyng` launcher (both have the same CLI surface).
### Running scripts ##### Running scripts
- Run a script by file name and pass arguments to `ARGV`: - Run a script by file name and pass arguments to `ARGV`:
@ -87,7 +87,7 @@ lyng --version
lyng --help lyng --help
``` ```
### Local imports for file execution ##### Local imports for file execution
When you execute a script file, the CLI builds a temporary local import manager rooted at the directory that contains the entry script. When you execute a script file, the CLI builds a temporary local import manager rooted at the directory that contains the entry script.
@ -144,7 +144,7 @@ Rationale:
- Explicit `package` remains available as a consistency check instead of a second, conflicting naming system. - Explicit `package` remains available as a consistency check instead of a second, conflicting naming system.
- The import search space stays local to the executed script, which avoids accidental cross-project resolution. - The import search space stays local to the executed script, which avoids accidental cross-project resolution.
## Use in shell scripts ### Use in shell scripts
Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example: Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example:
@ -152,7 +152,7 @@ Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts direct
println("Hello, world!") println("Hello, world!")
### Formatting source: `fmt` subcommand ##### Formatting source: `fmt` subcommand
Format Lyng files with the built-in formatter. Format Lyng files with the built-in formatter.
@ -194,7 +194,7 @@ lyng fmt --spacing --wrap src/file.lyng
``` ```
## Notes #### Notes
- Both native and JVM distributions expose the same CLI interface. Use whichever best fits your environment. - Both native and JVM distributions expose the same CLI interface. Use whichever best fits your environment.
- When executing scripts, all positional arguments after the script name are available in Lyng as `ARGV`. - When executing scripts, all positional arguments after the script name are available in Lyng as `ARGV`.

View File

@ -1,68 +0,0 @@
import lyng.buffer
import lyng.io.net
val host = "127.0.0.1"
val port = 8092
val N = 5
val server = Net.tcpListen(port, host)
println("start tcp server at $host:$port")
fun serveClient(client: TcpSocket) = launch {
try {
while (true) {
val data = client.read()
if (data == null) break
var line = (data as Buffer).decodeUtf8()
line = "[" + client.remoteAddress() + "]> " + line
println(line)
}
} catch (e) {
println("ERROR [reader]: " + e)
}
}
fun serveRequests(server: TcpServer) = launch {
val readers = []
try {
for (i in 0..<5) {
val client = server.accept()
println("accept new connection: " + client.remoteAddress())
readers.add(serveClient(client as TcpSocket))
}
} catch (e) {
println("ERROR [listener]: " + e)
} finally {
server.close()
}
for (i in 0..<readers.size) {
val reader = readers[i]
(reader as Deferred).await()
}
}
val srv = serveRequests(server as TcpServer)
var clients = []
for (i in 0..<N) {
//delay(500)
clients.add(launch {
try{
val socket = Net.tcpConnect(host, port)
socket.writeUtf8("ping1ping2ping3ping4ping5")
socket.flush()
socket.close()
} catch (e) {
println("ERROR [client]: " + e)
}
})
}
for (i in 0..<clients.size) {
val c = clients[i]
(c as Deferred).await()
println("client done")
}
srv.await()
delay(10000)
println("FIN")

View File

@ -1,48 +1,21 @@
import lyng.buffer
import lyng.io.net import lyng.io.net
val host = "127.0.0.1" val server = Net.tcpListen(0, "127.0.0.1")
val clientCount = 1000
val server = Net.tcpListen(0, host, clientCount, true) as TcpServer
val port = server.localAddress().port val port = server.localAddress().port
val accepted = launch {
fun payloadFor(index: Int) = "$index:${Random.nextInt()}:${Random.nextInt()}" val client = server.accept()
val line = (client.read(4) as Buffer).decodeUtf8()
launch { client.writeUtf8("echo:" + line)
try { client.flush()
while(true) { client.close()
val client = server.accept() as TcpSocket server.close()
launch { line
try {
val source = client.readLine()
if( source != null ) {
client.writeUtf8("pong: $source\n")
client.flush()
}
} finally {
client.close()
}
}
}
} finally {
server.close()
}
} }
val replies = (0..<clientCount).map { index -> val socket = Net.tcpConnect("127.0.0.1", port)
val payload = payloadFor(index) socket.writeUtf8("ping")
launch { socket.flush()
val socket = Net.tcpConnect(host, port) as TcpSocket val reply = (socket.read(16) as Buffer).decodeUtf8()
try { socket.close()
socket.writeUtf8(payload + "\n") println("${accepted.await()}: $reply")
socket.flush()
val reply = socket.readLine()
assertEquals("pong: $payload", reply)
reply
} finally {
socket.close()
}
}
}.joinAll()
assertEquals(clientCount, replies.size)
println("OK: $clientCount concurrent tcp clients")

View File

@ -18,7 +18,6 @@ slf4j = "2.0.17"
[libraries] [libraries]
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
clikt-core = { module = "com.github.ajalt.clikt:clikt-core", version.ref = "clikt" }
clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref = "clikt" } clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref = "clikt" }
mordant-core = { module = "com.github.ajalt.mordant:mordant-core", version.ref = "mordant" } mordant-core = { module = "com.github.ajalt.mordant:mordant-core", version.ref = "mordant" }
mordant-jvm-jna = { module = "com.github.ajalt.mordant:mordant-jvm-jna", version.ref = "mordant" } mordant-jvm-jna = { module = "com.github.ajalt.mordant:mordant-jvm-jna", version.ref = "mordant" }

View File

@ -52,12 +52,6 @@ kotlin {
linuxX64 { linuxX64 {
binaries { binaries {
executable() executable()
all {
if (buildType == org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.RELEASE) {
debuggable = true
optimized = false
}
}
} }
} }
sourceSets { sourceSets {
@ -69,7 +63,7 @@ kotlin {
// filesystem access into the execution Scope by default. // filesystem access into the execution Scope by default.
implementation(project(":lyngio")) implementation(project(":lyngio"))
implementation(libs.okio) implementation(libs.okio)
implementation(libs.clikt.core) implementation(libs.clikt)
implementation(kotlin("stdlib-common")) implementation(kotlin("stdlib-common"))
// optional support for rendering markdown in help messages // optional support for rendering markdown in help messages
// implementation(libs.clikt.markdown) // implementation(libs.clikt.markdown)

View File

@ -17,7 +17,7 @@
package net.sergeych package net.sergeych
import com.github.ajalt.clikt.core.CoreCliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.core.subcommands
@ -43,7 +43,6 @@ import net.sergeych.lyng.io.net.createNetModule
import net.sergeych.lyng.io.ws.createWsModule import net.sergeych.lyng.io.ws.createWsModule
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyngio.net.shutdownSystemNetEngine
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
@ -71,28 +70,10 @@ data class CommandResult(
val error: String val error: String
) )
private val baseCliImportManagerDefer = globalDefer {
val manager = Script.defaultImportManager.copy().apply {
installCliModules(this)
}
manager.newStdScope()
manager
}
private fun ImportManager.invalidateCliModuleCaches() {
invalidatePackageCache("lyng.io.fs")
invalidatePackageCache("lyng.io.console")
invalidatePackageCache("lyng.io.http")
invalidatePackageCache("lyng.io.ws")
invalidatePackageCache("lyng.io.net")
}
val baseScopeDefer = globalDefer { val baseScopeDefer = globalDefer {
baseCliImportManagerDefer.await().copy().apply { Script.newScope().apply {
invalidateCliModuleCaches()
}.newStdScope().apply {
installCliBuiltins() installCliBuiltins()
addConst("ARGV", ObjList(mutableListOf())) installCliModules(importManager)
} }
} }
@ -214,47 +195,35 @@ private fun discoverLocalCliModules(entryFile: Path): List<LocalCliModule> {
) )
} }
val packageName = declaredPackage ?: expectedPackage val packageName = declaredPackage ?: expectedPackage
val previous = seenPackages[packageName] val previous = seenPackages.putIfAbsent(packageName, file)
if (previous != null) { if (previous != null) {
throw ScriptError( throw ScriptError(
source.startPos, source.startPos,
"duplicate local module '$packageName': ${previous.toString()} and ${file.toString()}" "duplicate local module '$packageName': ${previous.toString()} and ${file.toString()}"
) )
} }
seenPackages[packageName] = file
LocalCliModule(packageName, source) LocalCliModule(packageName, source)
} }
.toList() .toList()
} }
private fun registerLocalCliModules(manager: ImportManager, modules: List<LocalCliModule>) { private fun registerLocalCliModules(manager: ImportManager, entryFile: Path) {
for (module in modules) { for (module in discoverLocalCliModules(entryFile)) {
manager.addPackage(module.packageName) { scope -> manager.addPackage(module.packageName) { scope ->
scope.eval(module.source) scope.eval(module.source)
} }
} }
} }
private suspend fun ImportManager.newCliScope(argv: List<String>): Scope = private suspend fun newCliScope(argv: List<String>, entryFileName: String? = null): Scope {
newStdScope().apply { val manager = baseScopeDefer.await().importManager.copy()
if (entryFileName != null) {
registerLocalCliModules(manager, canonicalPath(entryFileName.toPath()))
}
return manager.newStdScope().apply {
installCliBuiltins() installCliBuiltins()
addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList())) addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList()))
} }
internal suspend fun newCliScope(argv: List<String>, entryFileName: String? = null): Scope {
val baseManager = baseCliImportManagerDefer.await().copy().apply {
invalidateCliModuleCaches()
}
if (entryFileName == null) {
return baseManager.newCliScope(argv)
}
val entryFile = canonicalPath(entryFileName.toPath())
val localModules = discoverLocalCliModules(entryFile)
if (localModules.isEmpty()) {
return baseManager.newCliScope(argv)
}
registerLocalCliModules(baseManager, localModules)
return baseManager.newCliScope(argv)
} }
fun runMain(args: Array<String>) { fun runMain(args: Array<String>) {
@ -278,7 +247,7 @@ fun runMain(args: Array<String>) {
.main(args) .main(args)
} }
private class Fmt : CoreCliktCommand(name = "fmt") { private class Fmt : CliktCommand(name = "fmt") {
private val checkOnly by option("--check", help = "Check only; print files that would change").flag() private val checkOnly by option("--check", help = "Check only; print files that would change").flag()
private val inPlace by option("-i", "--in-place", help = "Write changes back to files").flag() private val inPlace by option("-i", "--in-place", help = "Write changes back to files").flag()
private val enableSpacing by option("--spacing", help = "Apply spacing normalization").flag() private val enableSpacing by option("--spacing", help = "Apply spacing normalization").flag()
@ -336,7 +305,7 @@ private class Fmt : CoreCliktCommand(name = "fmt") {
} }
} }
private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktCommand() { private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
override val invokeWithoutSubcommand = true override val invokeWithoutSubcommand = true
override val printHelpOnEmptyArgs = true override val printHelpOnEmptyArgs = true
@ -412,7 +381,6 @@ suspend fun executeSource(source: Source, initialScope: Scope? = null) {
evalOnCliDispatcher(session, source) evalOnCliDispatcher(session, source)
} finally { } finally {
session.cancelAndJoin() session.cancelAndJoin()
shutdownSystemNetEngine()
} }
} }

View File

@ -1,172 +0,0 @@
package net.sergeych
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.ObjString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class CliTcpServerRegressionTest {
@Test
fun reducedTcpServerExampleRunsWithCopiedCliImportManager() = runBlocking {
val cliScope = newCliScope(emptyList())
val session = EvalSession(cliScope)
try {
val result = evalOnCliDispatcher(
session,
Source(
"<tcp-server-regression>",
"""
import lyng.buffer
import lyng.io.net
val host = "127.0.0.1"
val server = Net.tcpListen(0, host)
val port = server.localAddress().port
val accepted = launch {
val client = server.accept()
val line = (client.read(4) as Buffer).decodeUtf8()
client.close()
server.close()
line
}
val socket = Net.tcpConnect(host, port)
socket.writeUtf8("ping")
socket.flush()
socket.close()
accepted.await()
""".trimIndent()
)
)
assertEquals("ping", (result as ObjString).value)
} finally {
session.cancelAndJoin()
}
}
@Test
fun concurrentTcpExampleRunsInCliScope() = runBlocking {
val cliScope = newCliScope(emptyList())
val session = EvalSession(cliScope)
try {
val result = evalOnCliDispatcher(
session,
Source(
"<tcp-server-concurrency-cli>",
"""
import lyng.io.net
val host = "127.0.0.1"
val clientCount = 32
val server: TcpServer = Net.tcpListen(0, host, 32, true) as TcpServer
val port: Int = server.localAddress().port
fun payloadFor(index: Int): String {
"${'$'}index:${'$'}{Random.nextInt()}:${'$'}{Random.nextInt()}"
}
fun handleClient(client: TcpSocket): String {
try {
val source = client.readLine()
if( source == null ) {
return "server-eof"
}
val reply = "pong: ${'$'}source"
client.writeUtf8(reply + "\n")
client.flush()
reply
} finally {
client.close()
}
}
val serverJob: Deferred = launch {
var handlers: List<Deferred> = List()
try {
for( i in 0..<32 ) {
val client: TcpSocket = server.accept() as TcpSocket
handlers += launch {
handleClient(client)
}
}
handlers.joinAll()
} finally {
server.close()
}
}
val clientJobs = (0..<clientCount).map { index ->
val payload = payloadFor(index)
launch {
val socket: TcpSocket = Net.tcpConnect(host, port) as TcpSocket
try {
socket.writeUtf8(payload + "\n")
socket.flush()
val reply = socket.readLine()
if( reply == null ) {
"client-eof:${'$'}payload"
} else {
assertEquals("pong: ${'$'}payload", reply)
reply
}
} finally {
socket.close()
}
}
}
val replies = clientJobs.joinAll()
val serverReplies = serverJob.await() as List<Object>
assertEquals(clientCount, replies.size)
assertEquals(clientCount, serverReplies.size)
assertEquals(replies.toSet, serverReplies.toSet)
"OK:${'$'}clientCount:${'$'}{replies.toSet}:${'$'}{serverReplies.toSet}"
""".trimIndent()
)
)
val text = (result as ObjString).value
assertTrue(text.startsWith("OK:32:"), text)
} finally {
session.cancelAndJoin()
}
}
@Test
fun mixedModuleAndLocalCapturesWorkInCliScope() = runBlocking {
val cliScope = newCliScope(emptyList())
val session = EvalSession(cliScope)
try {
val result = evalOnCliDispatcher(
session,
Source(
"<cli-capture-regression>",
"""
val prefix = "pong"
val jobs = (0..<32).map { index ->
val payload = "${'$'}index:${'$'}{Random.nextInt()}"
launch {
delay(5)
"${'$'}prefix:${'$'}payload"
}
}
jobs.joinAll()
""".trimIndent()
)
) as net.sergeych.lyng.obj.ObjList
assertEquals(32, result.list.size)
assertEquals(32, result.list.map { (it as ObjString).value }.toSet().size)
} finally {
session.cancelAndJoin()
}
}
}

View File

@ -28,8 +28,6 @@ import java.net.InetAddress
actual fun getSystemNetEngine(): LyngNetEngine = AndroidKtorNetEngine actual fun getSystemNetEngine(): LyngNetEngine = AndroidKtorNetEngine
actual fun shutdownSystemNetEngine() {}
private object AndroidKtorNetEngine : LyngNetEngine { private object AndroidKtorNetEngine : LyngNetEngine {
private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) } private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) }

View File

@ -97,5 +97,3 @@ internal object UnsupportedLyngNetEngine : LyngNetEngine {
} }
expect fun getSystemNetEngine(): LyngNetEngine expect fun getSystemNetEngine(): LyngNetEngine
expect fun shutdownSystemNetEngine()

View File

@ -18,7 +18,7 @@
package net.sergeych.lyng.io.net package net.sergeych.lyng.io.net
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import net.sergeych.lyng.Compiler import net.sergeych.lyng.Compiler
@ -30,92 +30,44 @@ import kotlin.test.assertEquals
class LyngNetTcpServerExampleTest { class LyngNetTcpServerExampleTest {
private fun concurrentTcpScript(clientCount: Int): String = """
import lyng.io.net
val host = "127.0.0.1"
val clientCount = $clientCount
val server: TcpServer = Net.tcpListen(0, host, clientCount, true) as TcpServer
val port: Int = server.localAddress().port
fun payloadFor(index: Int): String {
"${'$'}index:${'$'}{Random.nextInt()}:${'$'}{Random.nextInt()}"
}
fun handleClient(client: TcpSocket): String {
try {
val source = client.readLine()
if( source == null ) {
return "server-eof"
}
val reply = "pong: ${'$'}source"
client.writeUtf8(reply + "\n")
client.flush()
reply
} finally {
client.close()
}
}
val serverJob: Deferred = launch {
var handlers: List<Deferred> = List()
try {
for( i in 0..<${clientCount} ) {
val client: TcpSocket = server.accept() as TcpSocket
handlers += launch {
handleClient(client)
}
}
handlers.joinAll()
} finally {
server.close()
}
}
val clientJobs: List<Deferred> = (0..<clientCount).map { index ->
val payload = payloadFor(index)
launch {
val socket: TcpSocket = Net.tcpConnect(host, port) as TcpSocket
try {
socket.writeUtf8(payload + "\n")
socket.flush()
val reply = socket.readLine()
if( reply == null ) {
"client-eof:${'$'}payload"
}
else {
assertEquals("pong: ${'$'}payload", reply)
reply
}
} finally {
socket.close()
}
}
}
val replies = clientJobs.joinAll()
val serverReplies = serverJob.await() as List<Object>
assertEquals(clientCount, replies.size)
assertEquals(clientCount, serverReplies.size)
assertEquals(replies.toSet, serverReplies.toSet)
"OK:${'$'}clientCount"
""".trimIndent()
@Test @Test
fun tcpServerExampleSurvivesConcurrentLoopbackLoad() = runBlocking { fun tcpServerExampleRoundTripsOverLoopback() = runTest {
val engine = getSystemNetEngine() val engine = getSystemNetEngine()
if (!engine.isSupported || !engine.isTcpAvailable || !engine.isTcpServerAvailable) return@runBlocking if (!engine.isSupported || !engine.isTcpAvailable || !engine.isTcpServerAvailable) return@runTest
val scope = Script.newScope() val scope = Script.newScope()
createNetModule(PermitAllNetAccessPolicy, scope) createNetModule(PermitAllNetAccessPolicy, scope)
val code = """
import lyng.buffer
import lyng.io.net
val server = Net.tcpListen(0, "127.0.0.1")
val port = server.localAddress().port
val accepted = launch {
val client = server.accept()
val line = (client.read(4) as Buffer).decodeUtf8()
client.writeUtf8("echo:" + line)
client.flush()
client.close()
server.close()
line
}
val socket = Net.tcpConnect("127.0.0.1", port)
socket.writeUtf8("ping")
socket.flush()
val reply = (socket.read(16) as Buffer).decodeUtf8()
socket.close()
"${'$'}{accepted.await()}: ${'$'}reply"
""".trimIndent()
val result = withContext(Dispatchers.Default) { val result = withContext(Dispatchers.Default) {
withTimeout(20_000) { withTimeout(5_000) {
Compiler.compile(concurrentTcpScript(clientCount = 32)).execute(scope).inspect(scope) Compiler.compile(code).execute(scope).inspect(scope)
} }
} }
assertEquals("\"OK:32\"", result) assertEquals("\"ping: echo:ping\"", result)
} }
} }

View File

@ -1,68 +0,0 @@
package net.sergeych.lyngio.net
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class NetConcurrentLoopbackTest {
@Test
fun concurrentTcpRoundTripsWorkAtEngineLevel() = runBlocking {
val engine = getSystemNetEngine()
if (!engine.isSupported || !engine.isTcpAvailable || !engine.isTcpServerAvailable) return@runBlocking
withTimeout(10_000) {
val clients = 32
val server = engine.tcpListen(host = "127.0.0.1", port = 0, backlog = 64, reuseAddress = true)
val serverJob = async {
coroutineScope {
val handlers = ArrayList<kotlinx.coroutines.Deferred<String>>(clients)
repeat(clients) {
val client = server.accept()
handlers += async {
try {
val payload = client.readLine()
val reply = "pong:$payload"
client.writeUtf8("$reply\n")
client.flush()
reply
} finally {
client.close()
}
}
}
handlers.awaitAll()
}
}
val clientJobs = coroutineScope {
(0 until clients).map { index ->
async {
val payload = "ping:$index"
val socket = engine.tcpConnect("127.0.0.1", server.localAddress().port, timeoutMillis = 2_000, noDelay = true)
try {
socket.writeUtf8("$payload\n")
socket.flush()
socket.readLine()
} finally {
socket.close()
}
}
}
}
val clientReplies = clientJobs.awaitAll()
val serverReplies = serverJob.await()
server.close()
assertEquals((0 until clients).map { "pong:ping:$it" }, clientReplies)
assertEquals((0 until clients).map { "pong:ping:$it" }, serverReplies)
assertTrue(clientReplies.all { it != null })
}
}
}

View File

@ -1,14 +1,8 @@
package net.sergeych.lyngio.net package net.sergeych.lyngio.net
private val systemNetEngine: LyngNetEngine = createNativeKtorNetEngine( actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
isSupported = true, isSupported = true,
isTcpAvailable = true, isTcpAvailable = true,
isTcpServerAvailable = true, isTcpServerAvailable = true,
isUdpAvailable = true, isUdpAvailable = true,
) )
actual fun getSystemNetEngine(): LyngNetEngine = systemNetEngine
actual fun shutdownSystemNetEngine() {
shutdownNativeKtorNetEngine(systemNetEngine)
}

View File

@ -13,8 +13,6 @@ import org.khronos.webgl.Uint8Array
actual fun getSystemNetEngine(): LyngNetEngine = jsNodeNetEngineOrNull ?: UnsupportedLyngNetEngine actual fun getSystemNetEngine(): LyngNetEngine = jsNodeNetEngineOrNull ?: UnsupportedLyngNetEngine
actual fun shutdownSystemNetEngine() {}
private val jsNodeNetEngineOrNull: LyngNetEngine? by lazy { private val jsNodeNetEngineOrNull: LyngNetEngine? by lazy {
if (!isNodeRuntime()) return@lazy null if (!isNodeRuntime()) return@lazy null
val net = requireNodeModule("net") ?: return@lazy null val net = requireNodeModule("net") ?: return@lazy null

View File

@ -45,8 +45,6 @@ import java.net.InetAddress
actual fun getSystemNetEngine(): LyngNetEngine = JvmKtorNetEngine actual fun getSystemNetEngine(): LyngNetEngine = JvmKtorNetEngine
actual fun shutdownSystemNetEngine() {}
private object JvmKtorNetEngine : LyngNetEngine { private object JvmKtorNetEngine : LyngNetEngine {
private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) } private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) }

View File

@ -1,14 +1,8 @@
package net.sergeych.lyngio.net package net.sergeych.lyngio.net
private val systemNetEngine: LyngNetEngine = createNativeKtorNetEngine( actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
isSupported = true, isSupported = true,
isTcpAvailable = true, isTcpAvailable = true,
isTcpServerAvailable = true, isTcpServerAvailable = true,
isUdpAvailable = true, isUdpAvailable = true,
) )
actual fun getSystemNetEngine(): LyngNetEngine = systemNetEngine
actual fun shutdownSystemNetEngine() {
shutdownNativeKtorNetEngine(systemNetEngine)
}

View File

@ -1,5 +1,3 @@
package net.sergeych.lyngio.net package net.sergeych.lyngio.net
actual fun getSystemNetEngine(): LyngNetEngine = UnsupportedLyngNetEngine actual fun getSystemNetEngine(): LyngNetEngine = UnsupportedLyngNetEngine
actual fun shutdownSystemNetEngine() {}

View File

@ -40,10 +40,7 @@ private class NativeKtorNetEngine(
override val isTcpServerAvailable: Boolean, override val isTcpServerAvailable: Boolean,
override val isUdpAvailable: Boolean, override val isUdpAvailable: Boolean,
) : LyngNetEngine { ) : LyngNetEngine {
private var selectorManager: SelectorManager? = null private val selectorManager: SelectorManager by lazy { SelectorManager(Dispatchers.Default) }
private fun selectorManager(): SelectorManager =
selectorManager ?: SelectorManager(Dispatchers.Default).also { selectorManager = it }
override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> { override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> {
val rawAddress = InetSocketAddress(host, port).resolveAddress() val rawAddress = InetSocketAddress(host, port).resolveAddress()
@ -65,7 +62,7 @@ private class NativeKtorNetEngine(
noDelay: Boolean, noDelay: Boolean,
): LyngTcpSocket { ): LyngTcpSocket {
val connectBlock: suspend () -> Socket = { val connectBlock: suspend () -> Socket = {
aSocket(selectorManager()).tcp().connect(host, port) { aSocket(selectorManager).tcp().connect(host, port) {
this.noDelay = noDelay this.noDelay = noDelay
} }
} }
@ -80,7 +77,7 @@ private class NativeKtorNetEngine(
reuseAddress: Boolean, reuseAddress: Boolean,
): LyngTcpServer { ): LyngTcpServer {
val bindHost = host ?: "0.0.0.0" val bindHost = host ?: "0.0.0.0"
val server = aSocket(selectorManager()).tcp().bind(bindHost, port) { val server = aSocket(selectorManager).tcp().bind(bindHost, port) {
backlogSize = backlog backlogSize = backlog
this.reuseAddress = reuseAddress this.reuseAddress = reuseAddress
} }
@ -89,16 +86,11 @@ private class NativeKtorNetEngine(
override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket { override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket {
val bindHost = host ?: "0.0.0.0" val bindHost = host ?: "0.0.0.0"
val socket = aSocket(selectorManager()).udp().bind(bindHost, port) { val socket = aSocket(selectorManager).udp().bind(bindHost, port) {
this.reuseAddress = reuseAddress this.reuseAddress = reuseAddress
} }
return NativeLyngUdpSocket(socket) return NativeLyngUdpSocket(socket)
} }
fun shutdown() {
selectorManager?.close()
selectorManager = null
}
} }
private class NativeLyngTcpSocket( private class NativeLyngTcpSocket(
@ -222,7 +214,3 @@ private fun ByteArray.toIpHostString(): String = when (size) {
} }
else -> error("Unsupported IP address length: $size") else -> error("Unsupported IP address length: $size")
} }
internal fun shutdownNativeKtorNetEngine(engine: LyngNetEngine) {
(engine as? NativeKtorNetEngine)?.shutdown()
}

View File

@ -24,7 +24,6 @@ import net.sergeych.lyng.bridge.bind
import net.sergeych.lyng.bridge.bindObject import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bytecode.CmdFunction import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdVm import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
@ -636,20 +635,16 @@ class Script(
addConst("MapEntry", ObjMapEntry.type) addConst("MapEntry", ObjMapEntry.type)
addFn("launch") { addFn("launch") {
val rawCallable = requireOnlyArg<Obj>() val callable = requireOnlyArg<Obj>()
val currentScope = requireScope() val captured = this
// Freeze non-module lexical state at launch time so each coroutine gets
// its own view of loop locals and other transient frame values.
val captured = if (currentScope is ModuleScope) currentScope else currentScope.snapshotForClosure()
val callable = (rawCallable as? BytecodeLambdaCallable)?.freezeForLaunch(captured) ?: rawCallable
val session = EvalSession.currentOrNull() val session = EvalSession.currentOrNull()
val deferred = if (session != null) { val deferred = if (session != null) {
session.launchTrackedDeferred { session.launchTrackedDeferred {
ScopeBridge(captured).call(callable) captured.call(callable)
} }
} else { } else {
globalDefer { globalDefer {
ScopeBridge(captured).call(callable) captured.call(callable)
} }
} }
ObjDeferred(deferred) ObjDeferred(deferred)

View File

@ -1034,18 +1034,15 @@ class BytecodeCompiler(
val typeValue = compileRefWithFallback(ref.castTypeRef(), null, ref.castPos()) ?: return null val typeValue = compileRefWithFallback(ref.castTypeRef(), null, ref.castPos()) ?: return null
val objValue = ensureObjSlot(value) val objValue = ensureObjSlot(value)
val typeObj = ensureObjSlot(typeValue) val typeObj = ensureObjSlot(typeValue)
val sourceSlot = allocSlot()
builder.emit(Opcode.MOVE_OBJ, objValue.slot, sourceSlot)
updateSlotType(sourceSlot, SlotType.OBJ)
if (!ref.castIsNullable()) { if (!ref.castIsNullable()) {
builder.emit(Opcode.ASSERT_IS, sourceSlot, typeObj.slot) builder.emit(Opcode.ASSERT_IS, objValue.slot, typeObj.slot)
val resultSlot = allocSlot() val resultSlot = allocSlot()
builder.emit(Opcode.MAKE_QUALIFIED_VIEW, sourceSlot, typeObj.slot, resultSlot) builder.emit(Opcode.MAKE_QUALIFIED_VIEW, objValue.slot, typeObj.slot, resultSlot)
updateSlotType(resultSlot, SlotType.OBJ) updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ) return CompiledValue(resultSlot, SlotType.OBJ)
} }
val checkSlot = allocSlot() val checkSlot = allocSlot()
builder.emit(Opcode.CHECK_IS, sourceSlot, typeObj.slot, checkSlot) builder.emit(Opcode.CHECK_IS, objValue.slot, typeObj.slot, checkSlot)
updateSlotType(checkSlot, SlotType.BOOL) updateSlotType(checkSlot, SlotType.BOOL)
val resultSlot = allocSlot() val resultSlot = allocSlot()
val nullSlot = allocSlot() val nullSlot = allocSlot()
@ -1059,7 +1056,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, nullSlot, resultSlot) builder.emit(Opcode.MOVE_OBJ, nullSlot, resultSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(okLabel) builder.mark(okLabel)
builder.emit(Opcode.MAKE_QUALIFIED_VIEW, sourceSlot, typeObj.slot, resultSlot) builder.emit(Opcode.MAKE_QUALIFIED_VIEW, objValue.slot, typeObj.slot, resultSlot)
builder.mark(endLabel) builder.mark(endLabel)
updateSlotType(resultSlot, SlotType.OBJ) updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ) return CompiledValue(resultSlot, SlotType.OBJ)
@ -7820,9 +7817,7 @@ class BytecodeCompiler(
scopeSlotRefPosByKey[scopeKey] = ref.pos() scopeSlotRefPosByKey[scopeKey] = ref.pos()
} }
} }
if (resolved != null) return resolved return resolved
localSlotIndexByName[ref.name]?.let { return scopeSlotCount + it }
return null
} }
private fun resolveLocalSlotByRefOrName(ref: LocalSlotRef): Int? { private fun resolveLocalSlotByRefOrName(ref: LocalSlotRef): Int? {
@ -8674,11 +8669,6 @@ class BytecodeCompiler(
collectLoopVarNamesRef(ref.targetRef) collectLoopVarNamesRef(ref.targetRef)
collectLoopVarNamesRef(ref.indexRef) collectLoopVarNamesRef(ref.indexRef)
} }
is RangeRef -> {
ref.left?.let { collectLoopVarNamesRef(it) }
ref.right?.let { collectLoopVarNamesRef(it) }
ref.step?.let { collectLoopVarNamesRef(it) }
}
else -> {} else -> {}
} }
} }
@ -8805,11 +8795,6 @@ class BytecodeCompiler(
collectScopeSlotsRef(ref.targetRef) collectScopeSlotsRef(ref.targetRef)
collectScopeSlotsRef(ref.indexRef) collectScopeSlotsRef(ref.indexRef)
} }
is RangeRef -> {
ref.left?.let { collectScopeSlotsRef(it) }
ref.right?.let { collectScopeSlotsRef(it) }
ref.step?.let { collectScopeSlotsRef(it) }
}
is ClassOperatorRef -> { is ClassOperatorRef -> {
collectScopeSlotsRef(ref.target) collectScopeSlotsRef(ref.target)
} }

View File

@ -3783,48 +3783,11 @@ class BytecodeLambdaCallable(
private val returnLabels: Set<String>, private val returnLabels: Set<String>,
override val pos: Pos, override val pos: Pos,
) : Statement(), BytecodeCallable { ) : Statement(), BytecodeCallable {
private fun freezeRecord(record: ObjRecord): ObjRecord {
val frozenValue = when (val raw = record.value) {
is net.sergeych.lyng.FrameSlotRef -> raw.read()
is net.sergeych.lyng.RecordSlotRef -> raw.read()
is net.sergeych.lyng.ScopeSlotRef -> raw.read()
else -> raw
}
return record.copy(value = frozenValue)
}
private fun resolveCaptureRecords(base: Scope): List<ObjRecord>? {
if (captureNames.isEmpty()) return null
return captureNames.map { name ->
base.chainLookupIgnoreClosure(
name,
followClosure = true,
caller = base.currentClassCtx
) ?: base.raiseSymbolNotFound("symbol $name not found")
}
}
fun rebindClosure(newClosureScope: Scope): BytecodeLambdaCallable { fun rebindClosure(newClosureScope: Scope): BytecodeLambdaCallable {
return BytecodeLambdaCallable( return BytecodeLambdaCallable(
fn = fn, fn = fn,
closureScope = newClosureScope, closureScope = newClosureScope,
captureRecords = resolveCaptureRecords(newClosureScope) ?: captureRecords, captureRecords = captureRecords,
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
preferredThisType = preferredThisType,
returnLabels = returnLabels,
pos = pos
)
}
fun freezeForLaunch(newClosureScope: Scope): BytecodeLambdaCallable {
val frozenCaptures = captureRecords?.map(::freezeRecord)
?: resolveCaptureRecords(newClosureScope)?.map(::freezeRecord)
return BytecodeLambdaCallable(
fn = fn,
closureScope = newClosureScope,
captureRecords = frozenCaptures,
captureNames = captureNames, captureNames = captureNames,
paramSlotPlan = paramSlotPlan, paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration, argsDeclaration = argsDeclaration,

View File

@ -151,16 +151,8 @@ class ImportManager(
fun copy(): ImportManager = fun copy(): ImportManager =
op.withLock { op.withLock {
ImportManager(rootScope, securityManager).apply { ImportManager(rootScope, securityManager).apply {
for ((name, entry) in this@ImportManager.imports) { imports.putAll(this@ImportManager.imports)
imports[name] = Entry(entry.packageName, entry.builder, entry.cachedScope)
}
} }
} }
fun invalidatePackageCache(name: String) { }
op.withLock {
imports[name]?.cachedScope = null
}
}
}

View File

@ -120,104 +120,6 @@ class TestCoroutines {
) )
} }
@Test
fun testJoinAll() = runTest {
eval(
"""
val replies = (1..6).map { n ->
launch {
delay((7 - n) * 5)
"done:${'$'}n"
}
}.joinAll()
assertEquals(
["done:1", "done:2", "done:3", "done:4", "done:5", "done:6"],
replies
)
""".trimIndent()
)
}
@Test
fun testLaunchCapturesDistinctLoopValues() = runTest {
eval(
"""
val jobs = []
for( i in 0..<8 ) {
val x = i
jobs += launch {
delay(5)
x
}
}
assertEquals([0,1,2,3,4,5,6,7], jobs.joinAll())
""".trimIndent()
)
}
@Test
fun testNestedLaunchCapturesDistinctLoopValues() = runTest {
eval(
"""
val outer = launch {
val jobs = []
for( i in 0..<8 ) {
val x = i
jobs += launch {
delay(5)
x
}
}
jobs.joinAll()
}
assertEquals([0,1,2,3,4,5,6,7], outer.await())
""".trimIndent()
)
}
@Test
fun testLaunchCapturesDistinctObjectValues() = runTest {
eval(
"""
val jobs = []
for( i in 0..<8 ) {
val box = ["item:${'$'}i"]
jobs += launch {
delay(5)
box[0]
}
}
assertEquals(
["item:0", "item:1", "item:2", "item:3", "item:4", "item:5", "item:6", "item:7"],
jobs.joinAll()
)
""".trimIndent()
)
}
@Test
fun testLaunchCanUseCapturedRangeBoundInForLoop() = runTest {
eval(
"""
val count = 8
val outer = launch {
val jobs = []
for( i in 0..<count ) {
val x = i
jobs += launch { x }
}
jobs.joinAll()
}
assertEquals([0,1,2,3,4,5,6,7], outer.await())
""".trimIndent()
)
}
@Test @Test
fun testFlows() = runTest { fun testFlows() = runTest {
eval(""" eval("""

View File

@ -29,16 +29,6 @@ extern class Deferred {
val isCancelled: Bool val isCancelled: Bool
} }
/* Await every task in order and return collected results. */
fun Iterable<Deferred>.joinAll(): List<Object> {
var results: List<Object> = List()
for( task in this ) {
val deferred = task as Deferred
results += deferred.await()
}
results
}
/* A deferred result that can be completed manually. */ /* A deferred result that can be completed manually. */
extern class CompletableDeferred : Deferred { extern class CompletableDeferred : Deferred {
fun complete(value: Object): void fun complete(value: Object): void