Compare commits
No commits in common. "49fc7002333b5db79a4764286d637bcb9ef77aff" and "bb2119b1d139badaf4f9f79e32aa652d557e410f" have entirely different histories.
49fc700233
...
bb2119b1d1
@ -20,7 +20,7 @@
|
||||
set -e
|
||||
echo "publishing all artifacts"
|
||||
echo
|
||||
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel --no-configuration-cache
|
||||
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel #--no-configuration-cache
|
||||
|
||||
#echo
|
||||
#echo "Creating plugin"
|
||||
|
||||
@ -205,8 +205,7 @@ assertThrows(RollbackException) {
|
||||
- `columns` — positional `SqlColumn` metadata, available before iteration.
|
||||
- `size()` — result row count.
|
||||
- `isEmpty()` — fast emptiness check where possible.
|
||||
- `iterator()` — normal row iteration while the transaction is active.
|
||||
- `toList()` — materialize detached `SqlRow` snapshots that may be used after the transaction ends.
|
||||
- `iterator()` / `toList()` — normal row iteration.
|
||||
|
||||
##### `SqlRow`
|
||||
|
||||
@ -371,24 +370,14 @@ PostgreSQL-specific notes:
|
||||
|
||||
#### Lifetime rules
|
||||
|
||||
`ResultSet` is valid only while its owning transaction is active.
|
||||
|
||||
`SqlRow` values are detached snapshots once materialized, so this pattern is valid:
|
||||
|
||||
```lyng
|
||||
val rows = db.transaction { tx ->
|
||||
tx.select("select name from person order by id").toList()
|
||||
}
|
||||
|
||||
assertEquals("Ada", rows[0]["name"])
|
||||
```
|
||||
Result sets and rows are valid only while their owning transaction is active.
|
||||
|
||||
This means:
|
||||
|
||||
- do not keep `ResultSet` objects after the transaction block returns
|
||||
- materialize rows with `toList()` inside the transaction when they must outlive it
|
||||
- do not keep `ResultSet` or `SqlRow` objects after the transaction block returns
|
||||
- copy the values you need into ordinary Lyng objects inside the transaction
|
||||
|
||||
The same rule applies to generated keys from `ExecutionResult.getGeneratedKeys()`: the `ResultSet` is transaction-scoped, but rows returned by `toList()` are detached.
|
||||
The same lifetime rule applies to generated keys returned by `ExecutionResult.getGeneratedKeys()`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -65,7 +65,7 @@ db.transaction { tx ->
|
||||
println(" #" + row["ID"] + " " + row["title"] + " done=" + row["done"] + " due=" + row["due_date"])
|
||||
}
|
||||
|
||||
// toList() materializes detached rows that stay usable after transaction close.
|
||||
// If values need to survive after the transaction closes, copy them now.
|
||||
val snapshot = tx.select("select title, due_date from task order by id").toList()
|
||||
assertEquals("Write a SQLite example", snapshot[0]["title"])
|
||||
assertEquals(Date(2026, 4, 16), snapshot[1]["due_date"])
|
||||
|
||||
@ -151,13 +151,8 @@ kotlin {
|
||||
|
||||
tasks.named<KotlinNativeTest>("linuxX64Test") {
|
||||
dependsOn(tasks.named("linkDebugExecutableLinuxX64"))
|
||||
dependsOn(tasks.named("linkReleaseExecutableLinuxX64"))
|
||||
environment(
|
||||
"LYNG_CLI_NATIVE_BIN",
|
||||
layout.buildDirectory.file("bin/linuxX64/debugExecutable/lyng.kexe").get().asFile.absolutePath
|
||||
)
|
||||
environment(
|
||||
"LYNG_CLI_NATIVE_RELEASE_BIN",
|
||||
layout.buildDirectory.file("bin/linuxX64/releaseExecutable/lyng.kexe").get().asFile.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
@ -119,9 +119,8 @@ internal class CliExecutionRuntime(
|
||||
facade.call(handler)
|
||||
}
|
||||
}
|
||||
session.cancel()
|
||||
session.cancelAndJoin()
|
||||
shutdownSystemNetEngine()
|
||||
session.join()
|
||||
}
|
||||
|
||||
fun shutdownBlocking() {
|
||||
|
||||
@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toPath
|
||||
import platform.posix.O_CREAT
|
||||
import platform.posix.O_TRUNC
|
||||
import platform.posix.O_WRONLY
|
||||
import platform.posix.SIGKILL
|
||||
import platform.posix.SIGSEGV
|
||||
import platform.posix._exit
|
||||
import platform.posix.close
|
||||
import platform.posix.dup2
|
||||
import platform.posix.execvp
|
||||
import platform.posix.fork
|
||||
import platform.posix.getenv
|
||||
import platform.posix.getpid
|
||||
import platform.posix.kill
|
||||
import platform.posix.open
|
||||
import platform.posix.usleep
|
||||
import platform.posix.waitpid
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
class CliWebSocketNativeRegressionTest {
|
||||
@Test
|
||||
fun releaseCliDoesNotSegfaultOnConcurrentWebSocketClients() {
|
||||
val executable = getenv("LYNG_CLI_NATIVE_RELEASE_BIN")?.toKString()
|
||||
?: error("LYNG_CLI_NATIVE_RELEASE_BIN is not set")
|
||||
val fs = FileSystem.SYSTEM
|
||||
val repoRoot = ascend(executable.toPath(), 6)
|
||||
val scriptPath = repoRoot / "bugs" / "ws-segfault.lyng"
|
||||
check(fs.exists(scriptPath)) { "bug repro script not found at $scriptPath" }
|
||||
|
||||
val tempDir = "/tmp/lyng_ws_native_${getpid()}_${kotlin.random.Random.nextInt()}".toPath()
|
||||
val stdoutPath = tempDir / "stdout.txt"
|
||||
val stderrPath = tempDir / "stderr.txt"
|
||||
|
||||
fs.createDirectories(tempDir)
|
||||
try {
|
||||
val pid = launchCli(executable, scriptPath, stdoutPath, stderrPath)
|
||||
usleep(5_000_000u)
|
||||
|
||||
if (kill(pid, 0) == 0) {
|
||||
kill(pid, SIGKILL)
|
||||
}
|
||||
|
||||
val status = waitForPid(pid)
|
||||
val termSignal = status and 0x7f
|
||||
val stdout = readUtf8IfExists(fs, stdoutPath)
|
||||
val stderr = readUtf8IfExists(fs, stderrPath)
|
||||
val allOutput = "$stdout\n$stderr"
|
||||
|
||||
assertFalse(termSignal == SIGSEGV, "native CLI crashed with SIGSEGV. Output:\n$allOutput")
|
||||
assertTrue(
|
||||
stdout.lineSequence().count { it == "test send to ws://127.0.0.1:9998... OK" } == 2,
|
||||
"expected both websocket clients to finish. Output:\n$allOutput"
|
||||
)
|
||||
assertFalse(allOutput.contains("Segmentation fault"), "process output reported a segmentation fault:\n$allOutput")
|
||||
} finally {
|
||||
fs.deleteRecursively(tempDir, mustExist = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ascend(path: Path, levels: Int): Path {
|
||||
var current = path
|
||||
repeat(levels) {
|
||||
current = current.parent ?: error("cannot ascend $levels levels from $path")
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private fun readUtf8IfExists(fs: FileSystem, path: Path): String {
|
||||
return if (fs.exists(path)) fs.read(path) { readUtf8() } else ""
|
||||
}
|
||||
|
||||
private fun waitForPid(pid: Int): Int = memScoped {
|
||||
val status = alloc<IntVar>()
|
||||
val waited = waitpid(pid, status.ptr, 0)
|
||||
check(waited == pid) { "waitpid failed for $pid" }
|
||||
status.value
|
||||
}
|
||||
|
||||
private fun launchCli(
|
||||
executable: String,
|
||||
scriptPath: Path,
|
||||
stdoutPath: Path,
|
||||
stderrPath: Path,
|
||||
): Int = memScoped {
|
||||
val pid = fork()
|
||||
check(pid >= 0) { "fork failed" }
|
||||
if (pid == 0) {
|
||||
val stdoutFd = open(stdoutPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
val stderrFd = open(stderrPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
if (stdoutFd < 0 || stderrFd < 0) {
|
||||
_exit(2)
|
||||
}
|
||||
dup2(stdoutFd, 1)
|
||||
dup2(stderrFd, 2)
|
||||
close(stdoutFd)
|
||||
close(stderrFd)
|
||||
|
||||
val argv = allocArray<CPointerVar<ByteVar>>(3)
|
||||
argv[0] = executable.cstr.ptr
|
||||
argv[1] = scriptPath.toString().cstr.ptr
|
||||
argv[2] = null
|
||||
execvp(executable, argv)
|
||||
_exit(127)
|
||||
}
|
||||
pid
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
@ -233,10 +234,12 @@ internal class SqlRuntimeTypes private constructor(
|
||||
|
||||
rowClass.addProperty("size", getter = {
|
||||
val self = thisAs<SqlRowObj>()
|
||||
self.lifetime.ensureActive(this)
|
||||
ObjInt.of(self.values.size.toLong())
|
||||
})
|
||||
rowClass.addProperty("values", getter = {
|
||||
val self = thisAs<SqlRowObj>()
|
||||
self.lifetime.ensureActive(this)
|
||||
ObjImmutableList(self.values)
|
||||
})
|
||||
|
||||
@ -299,13 +302,14 @@ internal class SqlResultSetObj(
|
||||
data: SqlResultSetData,
|
||||
) : Obj() {
|
||||
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
||||
val rows: List<Obj> = buildRows(types, data)
|
||||
val rows: List<Obj> = buildRows(types, lifetime, data)
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = types.resultSetClass
|
||||
|
||||
private fun buildRows(
|
||||
types: SqlRuntimeTypes,
|
||||
lifetime: SqlTransactionLifetime,
|
||||
data: SqlResultSetData,
|
||||
): List<Obj> {
|
||||
val indexByName = linkedMapOf<String, MutableList<Int>>()
|
||||
@ -313,13 +317,14 @@ internal class SqlResultSetObj(
|
||||
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
||||
}
|
||||
return data.rows.map { rowValues ->
|
||||
SqlRowObj(types, rowValues, indexByName)
|
||||
SqlRowObj(types, lifetime, rowValues, indexByName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SqlRowObj(
|
||||
val types: SqlRuntimeTypes,
|
||||
val lifetime: SqlTransactionLifetime,
|
||||
val values: List<Obj>,
|
||||
private val indexByName: Map<String, List<Int>>,
|
||||
) : Obj() {
|
||||
@ -327,6 +332,7 @@ internal class SqlRowObj(
|
||||
get() = types.rowClass
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
lifetime.ensureActive(scope.asFacade())
|
||||
return when (index) {
|
||||
is ObjInt -> {
|
||||
val idx = index.value.toInt()
|
||||
|
||||
@ -17,46 +17,35 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
internal fun createKtorWsEngine(
|
||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||
shareClient: Boolean = true,
|
||||
): LyngWsEngine = KtorLyngWsEngine(engineFactory, shareClient)
|
||||
): LyngWsEngine = KtorLyngWsEngine(engineFactory)
|
||||
|
||||
private class KtorLyngWsEngine(
|
||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||
private val shareClient: Boolean,
|
||||
) : LyngWsEngine {
|
||||
private val clientFactory: () -> HttpClient = {
|
||||
private val clientResult = runCatching {
|
||||
HttpClient(engineFactory) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
private val sharedClientResult = if (shareClient) runCatching { clientFactory() } else null
|
||||
private val supportProbe = if (!shareClient) runCatching { clientFactory().close() } else null
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = sharedClientResult?.isSuccess ?: supportProbe?.isSuccess ?: true
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = (sharedClientResult?.getOrElse {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
} ?: runCatching { clientFactory() }.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
})
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return KtorLyngWsSession(
|
||||
targetUrl = url,
|
||||
session = session,
|
||||
ownedClient = client.takeUnless { shareClient },
|
||||
)
|
||||
return KtorLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class KtorLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
private val ownedClient: HttpClient? = null,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
|
||||
@ -66,22 +55,12 @@ private class KtorLyngWsSession(
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
try {
|
||||
session.send(text)
|
||||
} catch (e: Throwable) {
|
||||
release()
|
||||
throw e
|
||||
}
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
try {
|
||||
session.send(data)
|
||||
} catch (e: Throwable) {
|
||||
release()
|
||||
throw e
|
||||
}
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
@ -89,17 +68,14 @@ private class KtorLyngWsSession(
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
release()
|
||||
closed = true
|
||||
return null
|
||||
} catch (e: Throwable) {
|
||||
release()
|
||||
throw e
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
release()
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
@ -108,20 +84,11 @@ private class KtorLyngWsSession(
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
try {
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
ownedClient?.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,392 +0,0 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.URLProtocol
|
||||
import net.sergeych.lyngio.net.LyngNetEngine
|
||||
import net.sergeych.lyngio.net.LyngTcpSocket
|
||||
import net.sergeych.lyngio.net.getSystemNetEngine
|
||||
import net.sergeych.mp_tools.encodeToBase64
|
||||
import kotlin.random.Random
|
||||
|
||||
internal fun createSocketWsEngine(
|
||||
secureFallback: LyngWsEngine = UnsupportedLyngWsEngine,
|
||||
): LyngWsEngine = SocketLyngWsEngine(getSystemNetEngine(), secureFallback)
|
||||
|
||||
private class SocketLyngWsEngine(
|
||||
private val netEngine: LyngNetEngine,
|
||||
private val secureFallback: LyngWsEngine,
|
||||
) : LyngWsEngine {
|
||||
override val isSupported: Boolean
|
||||
get() = (netEngine.isSupported && netEngine.isTcpAvailable) || secureFallback.isSupported
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val parsedUrl = Url(url)
|
||||
return when (parsedUrl.protocol.name.lowercase()) {
|
||||
URLProtocol.WS.name.lowercase() -> connectPlain(parsedUrl, headers)
|
||||
URLProtocol.WSS.name.lowercase() -> secureFallback.connect(url, headers)
|
||||
else -> throw UnsupportedOperationException("Unsupported websocket scheme: ${parsedUrl.protocol.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun connectPlain(url: Url, headers: Map<String, String>): LyngWsSession {
|
||||
if (!netEngine.isSupported || !netEngine.isTcpAvailable) {
|
||||
throw UnsupportedOperationException("WebSocket client is not supported on this runtime")
|
||||
}
|
||||
val socket = netEngine.tcpConnect(url.host, url.port, timeoutMillis = null, noDelay = true)
|
||||
try {
|
||||
val key = randomBytes(16).encodeToBase64()
|
||||
val requestPath = buildRequestPath(url)
|
||||
val hostHeader = buildHostHeader(url)
|
||||
val request = buildString {
|
||||
append("GET ").append(requestPath).append(" HTTP/1.1\r\n")
|
||||
append("Host: ").append(hostHeader).append("\r\n")
|
||||
append("Upgrade: websocket\r\n")
|
||||
append("Connection: Upgrade\r\n")
|
||||
append("Sec-WebSocket-Key: ").append(key).append("\r\n")
|
||||
append("Sec-WebSocket-Version: 13\r\n")
|
||||
headers.forEach { (name, value) ->
|
||||
append(name).append(": ").append(value).append("\r\n")
|
||||
}
|
||||
append("\r\n")
|
||||
}
|
||||
socket.writeUtf8(request)
|
||||
socket.flush()
|
||||
validateHandshake(socket, key)
|
||||
return SocketLyngWsSession(url.toString(), socket)
|
||||
} catch (e: Throwable) {
|
||||
socket.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SocketLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val socket: LyngTcpSocket,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
private var closeSent = false
|
||||
private var pending = ByteArray(0)
|
||||
private var fragmentedOpcode: Int? = null
|
||||
private var fragmentedPayload = ByteArray(0)
|
||||
|
||||
override fun isOpen(): Boolean = !closed && socket.isOpen()
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
sendFrame(OPCODE_TEXT, text.encodeToByteArray())
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
sendFrame(OPCODE_BINARY, data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
while (true) {
|
||||
val frame = readFrame() ?: run {
|
||||
release()
|
||||
return null
|
||||
}
|
||||
when (frame.opcode) {
|
||||
OPCODE_CONTINUATION -> {
|
||||
val opcode = fragmentedOpcode ?: throw IllegalStateException("unexpected websocket continuation frame")
|
||||
fragmentedPayload += frame.payload
|
||||
if (frame.fin) {
|
||||
val payload = fragmentedPayload
|
||||
fragmentedOpcode = null
|
||||
fragmentedPayload = ByteArray(0)
|
||||
return payload.toMessage(opcode)
|
||||
}
|
||||
}
|
||||
OPCODE_TEXT, OPCODE_BINARY -> {
|
||||
if (frame.fin) return frame.payload.toMessage(frame.opcode)
|
||||
fragmentedOpcode = frame.opcode
|
||||
fragmentedPayload = frame.payload
|
||||
}
|
||||
OPCODE_CLOSE -> {
|
||||
if (!closeSent) {
|
||||
sendFrame(OPCODE_CLOSE, frame.payload)
|
||||
closeSent = true
|
||||
}
|
||||
release()
|
||||
return null
|
||||
}
|
||||
OPCODE_PING -> sendFrame(OPCODE_PONG, frame.payload)
|
||||
OPCODE_PONG -> Unit
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
val reasonBytes = reason.encodeToByteArray()
|
||||
val payload = ByteArray(reasonBytes.size + 2)
|
||||
payload[0] = (code shr 8).toByte()
|
||||
payload[1] = code.toByte()
|
||||
reasonBytes.copyInto(payload, destinationOffset = 2)
|
||||
try {
|
||||
if (!closeSent) {
|
||||
sendFrame(OPCODE_CLOSE, payload)
|
||||
closeSent = true
|
||||
}
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendFrame(opcode: Int, payload: ByteArray) {
|
||||
ensureOpen()
|
||||
val header = buildFrameHeader(opcode, payload.size, masked = true)
|
||||
val mask = randomBytes(4)
|
||||
val maskedPayload = payload.copyOf()
|
||||
maskedPayload.indices.forEach { index ->
|
||||
maskedPayload[index] = (maskedPayload[index].toInt() xor mask[index % mask.size].toInt()).toByte()
|
||||
}
|
||||
socket.write(header + mask + maskedPayload)
|
||||
socket.flush()
|
||||
}
|
||||
|
||||
private suspend fun readFrame(): WsFrame? {
|
||||
val head = readExact(2) ?: return null
|
||||
val fin = (head[0].toInt() and 0x80) != 0
|
||||
val opcode = head[0].toInt() and 0x0f
|
||||
val masked = (head[1].toInt() and 0x80) != 0
|
||||
val payloadLength = when (val lengthCode = head[1].toInt() and 0x7f) {
|
||||
126 -> {
|
||||
val extended = readExact(2) ?: return null
|
||||
((extended[0].toInt() and 0xff) shl 8) or (extended[1].toInt() and 0xff)
|
||||
}
|
||||
127 -> {
|
||||
val extended = readExact(8) ?: return null
|
||||
var acc = 0L
|
||||
extended.forEach { byte ->
|
||||
acc = (acc shl 8) or (byte.toInt() and 0xff).toLong()
|
||||
}
|
||||
require(acc <= Int.MAX_VALUE.toLong()) { "websocket frame is too large" }
|
||||
acc.toInt()
|
||||
}
|
||||
else -> lengthCode
|
||||
}
|
||||
val mask = if (masked) readExact(4) ?: return null else null
|
||||
val payload = if (payloadLength > 0) readExact(payloadLength) ?: return null else ByteArray(0)
|
||||
if (mask != null) {
|
||||
payload.indices.forEach { index ->
|
||||
payload[index] = (payload[index].toInt() xor mask[index % mask.size].toInt()).toByte()
|
||||
}
|
||||
}
|
||||
return WsFrame(fin = fin, opcode = opcode, payload = payload)
|
||||
}
|
||||
|
||||
private suspend fun readExact(byteCount: Int): ByteArray? {
|
||||
while (pending.size < byteCount) {
|
||||
val chunk = socket.read(maxOf(4096, byteCount - pending.size)) ?: break
|
||||
if (chunk.isEmpty()) break
|
||||
pending += chunk
|
||||
}
|
||||
if (pending.size < byteCount) return null
|
||||
val result = pending.copyOfRange(0, byteCount)
|
||||
pending = pending.copyOfRange(byteCount, pending.size)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
socket.close()
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed || !socket.isOpen()) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
|
||||
private data class WsFrame(
|
||||
val fin: Boolean,
|
||||
val opcode: Int,
|
||||
val payload: ByteArray,
|
||||
)
|
||||
|
||||
private suspend fun validateHandshake(socket: LyngTcpSocket, key: String) {
|
||||
val statusLine = socket.readLine() ?: error("websocket handshake failed: missing response status")
|
||||
require(statusLine.startsWith("HTTP/1.1 101") || statusLine.startsWith("HTTP/1.0 101")) {
|
||||
"websocket handshake failed: $statusLine"
|
||||
}
|
||||
val headers = linkedMapOf<String, String>()
|
||||
while (true) {
|
||||
val line = socket.readLine() ?: error("websocket handshake failed: unexpected EOF")
|
||||
if (line.isBlank()) break
|
||||
val colonAt = line.indexOf(':')
|
||||
require(colonAt > 0) { "invalid websocket header: $line" }
|
||||
val name = line.substring(0, colonAt).trim().lowercase()
|
||||
val value = line.substring(colonAt + 1).trim()
|
||||
headers[name] = value
|
||||
}
|
||||
require(headers["upgrade"]?.lowercase() == "websocket") { "websocket handshake failed: missing Upgrade header" }
|
||||
require(headers["connection"]?.lowercase()?.contains("upgrade") == true) {
|
||||
"websocket handshake failed: missing Connection header"
|
||||
}
|
||||
require(headers["sec-websocket-accept"] == websocketAcceptKey(key)) {
|
||||
"websocket handshake failed: invalid Sec-WebSocket-Accept"
|
||||
}
|
||||
}
|
||||
|
||||
private fun websocketAcceptKey(key: String): String =
|
||||
sha1((key + WS_GUID).encodeToByteArray()).encodeToBase64()
|
||||
|
||||
private fun buildRequestPath(url: Url): String {
|
||||
val path = url.encodedPath.ifEmpty { "/" }
|
||||
val query = url.encodedQuery
|
||||
return if (query.isBlank()) path else "$path?$query"
|
||||
}
|
||||
|
||||
private fun buildHostHeader(url: Url): String {
|
||||
val host = if (':' in url.host && !url.host.startsWith("[")) "[${url.host}]" else url.host
|
||||
return if (url.port == url.protocol.defaultPort) host else "$host:${url.port}"
|
||||
}
|
||||
|
||||
private fun buildFrameHeader(opcode: Int, payloadSize: Int, masked: Boolean): ByteArray {
|
||||
require(payloadSize >= 0) { "payload size must be non-negative" }
|
||||
val firstByte = (0x80 or (opcode and 0x0f)).toByte()
|
||||
val maskBit = if (masked) 0x80 else 0
|
||||
return when {
|
||||
payloadSize <= 125 -> byteArrayOf(firstByte, (maskBit or payloadSize).toByte())
|
||||
payloadSize <= 0xffff -> byteArrayOf(
|
||||
firstByte,
|
||||
(maskBit or 126).toByte(),
|
||||
((payloadSize ushr 8) and 0xff).toByte(),
|
||||
(payloadSize and 0xff).toByte(),
|
||||
)
|
||||
else -> byteArrayOf(
|
||||
firstByte,
|
||||
(maskBit or 127).toByte(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
((payloadSize ushr 24) and 0xff).toByte(),
|
||||
((payloadSize ushr 16) and 0xff).toByte(),
|
||||
((payloadSize ushr 8) and 0xff).toByte(),
|
||||
(payloadSize and 0xff).toByte(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toMessage(opcode: Int): LyngWsMessage = when (opcode) {
|
||||
OPCODE_TEXT -> LyngWsMessage(isText = true, text = decodeToString())
|
||||
OPCODE_BINARY -> LyngWsMessage(isText = false, data = copyOf())
|
||||
else -> throw IllegalStateException("unsupported websocket opcode: $opcode")
|
||||
}
|
||||
|
||||
private fun randomBytes(size: Int): ByteArray = ByteArray(size).also(Random.Default::nextBytes)
|
||||
|
||||
private fun sha1(input: ByteArray): ByteArray {
|
||||
var h0 = 0x67452301
|
||||
var h1 = 0xEFCDAB89.toInt()
|
||||
var h2 = 0x98BADCFE.toInt()
|
||||
var h3 = 0x10325476
|
||||
var h4 = 0xC3D2E1F0.toInt()
|
||||
|
||||
val msgLen = input.size
|
||||
val bitLen = msgLen.toLong() * 8L
|
||||
val totalLen = ((msgLen + 1 + 8 + 63) / 64) * 64
|
||||
val padded = ByteArray(totalLen).also { buf ->
|
||||
input.copyInto(buf)
|
||||
buf[msgLen] = 0x80.toByte()
|
||||
for (i in 0..7) {
|
||||
buf[totalLen - 8 + i] = ((bitLen ushr (56 - i * 8)) and 0xff).toByte()
|
||||
}
|
||||
}
|
||||
|
||||
val words = IntArray(80)
|
||||
var blockStart = 0
|
||||
while (blockStart < padded.size) {
|
||||
for (i in 0..15) {
|
||||
val off = blockStart + i * 4
|
||||
words[i] = ((padded[off].toInt() and 0xff) shl 24) or
|
||||
((padded[off + 1].toInt() and 0xff) shl 16) or
|
||||
((padded[off + 2].toInt() and 0xff) shl 8) or
|
||||
(padded[off + 3].toInt() and 0xff)
|
||||
}
|
||||
for (i in 16..79) {
|
||||
val mixed = words[i - 3] xor words[i - 8] xor words[i - 14] xor words[i - 16]
|
||||
words[i] = (mixed shl 1) or (mixed ushr 31)
|
||||
}
|
||||
|
||||
var a = h0
|
||||
var b = h1
|
||||
var c = h2
|
||||
var d = h3
|
||||
var e = h4
|
||||
|
||||
for (i in 0..19) {
|
||||
val f = (b and c) or (b.inv() and d)
|
||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x5A827999 + words[i]
|
||||
e = d
|
||||
d = c
|
||||
c = (b shl 30) or (b ushr 2)
|
||||
b = a
|
||||
a = temp
|
||||
}
|
||||
for (i in 20..39) {
|
||||
val f = b xor c xor d
|
||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x6ED9EBA1 + words[i]
|
||||
e = d
|
||||
d = c
|
||||
c = (b shl 30) or (b ushr 2)
|
||||
b = a
|
||||
a = temp
|
||||
}
|
||||
for (i in 40..59) {
|
||||
val f = (b and c) or (b and d) or (c and d)
|
||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x8F1BBCDC.toInt() + words[i]
|
||||
e = d
|
||||
d = c
|
||||
c = (b shl 30) or (b ushr 2)
|
||||
b = a
|
||||
a = temp
|
||||
}
|
||||
for (i in 60..79) {
|
||||
val f = b xor c xor d
|
||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0xCA62C1D6.toInt() + words[i]
|
||||
e = d
|
||||
d = c
|
||||
c = (b shl 30) or (b ushr 2)
|
||||
b = a
|
||||
a = temp
|
||||
}
|
||||
|
||||
h0 += a
|
||||
h1 += b
|
||||
h2 += c
|
||||
h3 += d
|
||||
h4 += e
|
||||
blockStart += 64
|
||||
}
|
||||
|
||||
return ByteArray(20).also { out ->
|
||||
fun putInt(offset: Int, value: Int) {
|
||||
out[offset] = (value ushr 24).toByte()
|
||||
out[offset + 1] = (value ushr 16).toByte()
|
||||
out[offset + 2] = (value ushr 8).toByte()
|
||||
out[offset + 3] = value.toByte()
|
||||
}
|
||||
putInt(0, h0)
|
||||
putInt(4, h1)
|
||||
putInt(8, h2)
|
||||
putInt(12, h3)
|
||||
putInt(16, h4)
|
||||
}
|
||||
}
|
||||
|
||||
private const val WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
private const val OPCODE_CONTINUATION = 0x0
|
||||
private const val OPCODE_TEXT = 0x1
|
||||
private const val OPCODE_BINARY = 0x2
|
||||
private const val OPCODE_CLOSE = 0x8
|
||||
private const val OPCODE_PING = 0x9
|
||||
private const val OPCODE_PONG = 0xA
|
||||
@ -112,34 +112,6 @@ class LyngJdbcModuleTest {
|
||||
assertEquals(3L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaterializedRowsListCanBeReturnedFromTransaction() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createJdbcModule(scope.importManager)
|
||||
val jdbcModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.jdbc")
|
||||
val db = jdbcModule.callFn("openH2", ObjString("mem:rows_return_${System.nanoTime()};DB_CLOSE_DELAY=-1"))
|
||||
|
||||
val rows = db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table person(id bigint auto_increment primary key, name varchar(120) not null)")
|
||||
)
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into person(name) values(?)"), ObjString("Ada"))
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into person(name) values(?)"), ObjString("Linus"))
|
||||
tx.invokeInstanceMethod(requireScope(), "select", ObjString("select name from person order by id"))
|
||||
.invokeInstanceMethod(requireScope(), "toList")
|
||||
}
|
||||
)
|
||||
|
||||
assertEquals("Ada", (rows.getAt(scope, ObjInt.Zero).getAt(scope, ObjString("name")) as ObjString).value)
|
||||
assertEquals("Linus", (rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name")) as ObjString).value)
|
||||
}
|
||||
|
||||
private suspend fun scalarSelect(scope: net.sergeych.lyng.Scope, db: Obj, sql: String): Long {
|
||||
val result = db.invokeInstanceMethod(
|
||||
scope,
|
||||
|
||||
@ -250,7 +250,7 @@ class LyngSqliteModuleTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaterializedRowSurvivesAfterTransactionEnds() = runTest {
|
||||
fun testRowFailsAfterTransactionEnds() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val db = openMemoryDb(scope)
|
||||
var leakedRow: Obj = ObjNull
|
||||
@ -265,34 +265,12 @@ class LyngSqliteModuleTest {
|
||||
}
|
||||
)
|
||||
|
||||
val answer = leakedRow.getAt(scope, ObjString("answer")) as ObjInt
|
||||
assertEquals(42L, answer.value)
|
||||
}
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
leakedRow.getAt(scope, ObjString("answer"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaterializedRowsListCanBeReturnedFromTransaction() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val db = openMemoryDb(scope)
|
||||
|
||||
val rows = db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table items(id integer primary key autoincrement, name text not null)")
|
||||
)
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("alpha"))
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("beta"))
|
||||
tx.invokeInstanceMethod(requireScope(), "select", ObjString("select name from items order by id"))
|
||||
.invokeInstanceMethod(requireScope(), "toList")
|
||||
}
|
||||
)
|
||||
|
||||
assertEquals("alpha", stringValue(scope, rows.getAt(scope, ObjInt.Zero).getAt(scope, ObjString("name"))))
|
||||
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -2,5 +2,4 @@ package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.engine.curl.Curl
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine =
|
||||
createSocketWsEngine(secureFallback = createKtorWsEngine(Curl, shareClient = false))
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Curl)
|
||||
|
||||
@ -186,7 +186,7 @@ class LyngSqliteModuleNativeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaterializedRowSurvivesAfterTransactionEnds() = runTest {
|
||||
fun testRowFailsAfterTransactionEnds() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val db = openMemoryDb(scope)
|
||||
var leakedRow: Obj = ObjNull
|
||||
@ -201,34 +201,12 @@ class LyngSqliteModuleNativeTest {
|
||||
}
|
||||
)
|
||||
|
||||
val answer = leakedRow.getAt(scope, ObjString("answer")) as ObjInt
|
||||
assertEquals(42L, answer.value)
|
||||
}
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
leakedRow.getAt(scope, ObjString("answer"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaterializedRowsListCanBeReturnedFromTransaction() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val db = openMemoryDb(scope)
|
||||
|
||||
val rows = db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table items(id integer primary key autoincrement, name text not null)")
|
||||
)
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("alpha"))
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("beta"))
|
||||
tx.invokeInstanceMethod(requireScope(), "select", ObjString("select name from items order by id"))
|
||||
.invokeInstanceMethod(requireScope(), "toList")
|
||||
}
|
||||
)
|
||||
|
||||
assertEquals("alpha", stringValue(scope, rows.getAt(scope, ObjInt.Zero).getAt(scope, ObjString("name"))))
|
||||
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -44,11 +44,11 @@ extern class SqlRow {
|
||||
- iteration to the end or canceled iteration should close the underlying
|
||||
resources automatically
|
||||
- using the result set after its transaction ends is invalid
|
||||
- rows obtained from the result set stay usable after the owning
|
||||
transaction ends once they have been materialized
|
||||
- rows obtained from the result set are also invalid after the owning
|
||||
transaction ends, even if the implementation had already buffered them
|
||||
|
||||
Calling `toList()` while the transaction is active is the normal way to
|
||||
detach rows for use after the transaction block returns.
|
||||
If user code wants row data to survive independently, it should copy the
|
||||
values it needs into ordinary Lyng objects while the transaction is active.
|
||||
*/
|
||||
extern class ResultSet : Iterable<SqlRow> {
|
||||
/*
|
||||
@ -84,8 +84,7 @@ extern class ExecutionResult {
|
||||
other result set.
|
||||
|
||||
If the statement produced no generated values, the returned result set
|
||||
is empty. Call `toList()` if generated-key rows must outlive the
|
||||
transaction.
|
||||
is empty.
|
||||
*/
|
||||
fun getGeneratedKeys(): ResultSet
|
||||
}
|
||||
|
||||
@ -10679,12 +10679,8 @@ class Compiler(
|
||||
)
|
||||
}
|
||||
if (getter != null || setter != null) {
|
||||
val prop = if (actualExtern) {
|
||||
ObjProperty(name, null, null)
|
||||
} else {
|
||||
ObjProperty(name, getter, setter)
|
||||
}
|
||||
val initStmt = if (!isAbstract && !actualExtern) {
|
||||
val prop = ObjProperty(name, getter, setter)
|
||||
val initStmt = if (!isAbstract) {
|
||||
val initStatement = InstancePropertyInitStatement(
|
||||
storageName = storageName,
|
||||
isMutable = isMutable,
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.requiredArg
|
||||
|
||||
internal fun ObjClass.bindClassFn(name: String, code: suspend ScopeFacade.() -> Obj) {
|
||||
val callable = ObjExternCallable.fromBridge { code() }
|
||||
val memberRecord = members[name]
|
||||
val classScopeRecord = classScope?.objects?.get(name)
|
||||
if (memberRecord != null) {
|
||||
val methodId = ensureMethodIdForBridge(name, memberRecord)
|
||||
val newRecord = memberRecord.copy(
|
||||
value = callable,
|
||||
type = ObjRecord.Type.Fun,
|
||||
methodId = methodId,
|
||||
isAbstract = false,
|
||||
)
|
||||
replaceMemberForBridge(name, newRecord)
|
||||
if (classScopeRecord != null) {
|
||||
replaceClassScopeMemberForBridge(name, newRecord)
|
||||
}
|
||||
} else if (classScopeRecord != null) {
|
||||
val methodId = ensureMethodIdForBridge(name, classScopeRecord)
|
||||
replaceClassScopeMemberForBridge(
|
||||
name,
|
||||
classScopeRecord.copy(
|
||||
value = callable,
|
||||
type = ObjRecord.Type.Fun,
|
||||
methodId = methodId,
|
||||
isAbstract = false,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addClassFn(name, code = code)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ObjClass.bindProperty(
|
||||
name: String,
|
||||
getter: (suspend ScopeFacade.() -> Obj)? = null,
|
||||
setter: (suspend ScopeFacade.(Obj) -> Unit)? = null,
|
||||
) {
|
||||
val g = getter?.let { ObjExternCallable.fromBridge { it() } }
|
||||
val s = setter?.let { ObjExternCallable.fromBridge { it(requiredArg(0)); ObjVoid } }
|
||||
val prop = ObjProperty(name, g, s)
|
||||
val existing = members[name]
|
||||
if (existing != null) {
|
||||
val newRecord = existing.copy(
|
||||
value = prop,
|
||||
type = ObjRecord.Type.Property,
|
||||
methodId = ensureMethodIdForBridge(name, existing),
|
||||
fieldId = null,
|
||||
isAbstract = false,
|
||||
)
|
||||
replaceMemberForBridge(name, newRecord)
|
||||
if (classScope?.objects?.containsKey(name) == true) {
|
||||
replaceClassScopeMemberForBridge(name, newRecord)
|
||||
}
|
||||
} else {
|
||||
addProperty(name, getter = getter, setter = setter)
|
||||
}
|
||||
}
|
||||
@ -18,10 +18,13 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
import net.sergeych.lyng.requiredArg
|
||||
|
||||
object ObjComplexSupport {
|
||||
private object BoundMarker
|
||||
private val complexTypeDecl = TypeDecl.Simple("lyng.complex.Complex", false)
|
||||
|
||||
suspend fun bindTo(module: ModuleScope) {
|
||||
val complexClass = module.requireClass("Complex")
|
||||
@ -30,20 +33,36 @@ object ObjComplexSupport {
|
||||
|
||||
val decimalModule = module.currentImportProvider.createModuleScope(module.pos, "lyng.decimal")
|
||||
val decimalClass = decimalModule.requireClass("Decimal")
|
||||
decimalClass.bindProperty("re", getter = {
|
||||
newComplex(
|
||||
complexClass,
|
||||
decimalToReal(thisObj),
|
||||
0.0
|
||||
)
|
||||
})
|
||||
decimalClass.bindProperty("i", getter = {
|
||||
newComplex(
|
||||
complexClass,
|
||||
0.0,
|
||||
decimalToReal(thisObj)
|
||||
)
|
||||
})
|
||||
|
||||
decimalClass.addPropertyDoc(
|
||||
name = "re",
|
||||
doc = "Convert this Decimal to a Complex with zero imaginary part.",
|
||||
type = type("lyng.complex.Complex"),
|
||||
moduleName = "lyng.complex",
|
||||
getter = {
|
||||
newComplex(
|
||||
complexClass,
|
||||
decimalToReal(thisObj),
|
||||
0.0
|
||||
)
|
||||
}
|
||||
)
|
||||
decimalClass.members["re"] = decimalClass.members.getValue("re").copy(typeDecl = complexTypeDecl)
|
||||
|
||||
decimalClass.addPropertyDoc(
|
||||
name = "i",
|
||||
doc = "Convert this Decimal to a pure imaginary Complex after rounding to Real.",
|
||||
type = type("lyng.complex.Complex"),
|
||||
moduleName = "lyng.complex",
|
||||
getter = {
|
||||
newComplex(
|
||||
complexClass,
|
||||
0.0,
|
||||
decimalToReal(thisObj)
|
||||
)
|
||||
}
|
||||
)
|
||||
decimalClass.members["i"] = decimalClass.members.getValue("i").copy(typeDecl = complexTypeDecl)
|
||||
|
||||
OperatorInteropRegistry.register(
|
||||
leftClass = decimalClass,
|
||||
|
||||
@ -106,15 +106,15 @@ object ObjDecimalSupport {
|
||||
decimalClass.addFn("toStringExpanded") {
|
||||
ObjString(valueOf(thisObj).toStringExpanded())
|
||||
}
|
||||
decimalClass.bindClassFn("fromInt") {
|
||||
decimalClass.addClassFn("fromInt") {
|
||||
val value = requiredArg<ObjInt>(0).value
|
||||
newInstance(decimalClass, IonBigDecimal.fromLong(value))
|
||||
}
|
||||
decimalClass.bindClassFn("fromReal") {
|
||||
decimalClass.addClassFn("fromReal") {
|
||||
val value = requiredArg<ObjReal>(0).value
|
||||
newInstanceFromFiniteReal(decimalClass, value)
|
||||
}
|
||||
decimalClass.bindClassFn("fromString") {
|
||||
decimalClass.addClassFn("fromString") {
|
||||
val value = requiredArg<ObjString>(0).value
|
||||
try {
|
||||
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
||||
|
||||
@ -53,10 +53,10 @@ object ObjMatrixSupport {
|
||||
}
|
||||
hooks += { _, instance -> instance.kotlinInstanceData = defaultVector }
|
||||
|
||||
vectorClass.bindProperty("size", getter = {
|
||||
vectorClass.addProperty("size", getter = {
|
||||
ObjInt.of(vectorOf(thisObj).size.toLong())
|
||||
})
|
||||
vectorClass.bindProperty("length", getter = {
|
||||
vectorClass.addProperty("length", getter = {
|
||||
ObjInt.of(vectorOf(thisObj).size.toLong())
|
||||
})
|
||||
vectorClass.addFn("toList") {
|
||||
@ -99,10 +99,10 @@ object ObjMatrixSupport {
|
||||
ObjInt.of(vectorOf(thisObj).compareTo(coerceVectorArg(requireScope(), args.firstAndOnly())).toLong())
|
||||
}
|
||||
|
||||
vectorClass.bindClassFn("fromList") {
|
||||
vectorClass.addClassFn("fromList") {
|
||||
newVector(vectorClass, parseVector(requireScope(), requiredArg(0)))
|
||||
}
|
||||
vectorClass.bindClassFn("zeros") {
|
||||
vectorClass.addClassFn("zeros") {
|
||||
val size = requiredArg<ObjInt>(0).value.toInt()
|
||||
if (size <= 0) requireScope().raiseIllegalArgument("vector size must be positive")
|
||||
newVector(vectorClass, VectorData(DoubleArray(size)))
|
||||
@ -119,13 +119,13 @@ object ObjMatrixSupport {
|
||||
}
|
||||
hooks += { _, instance -> instance.kotlinInstanceData = defaultMatrix }
|
||||
|
||||
matrixClass.bindProperty("rows", getter = {
|
||||
matrixClass.addProperty("rows", getter = {
|
||||
ObjInt.of(matrixOf(thisObj).rows.toLong())
|
||||
})
|
||||
matrixClass.bindProperty("cols", getter = {
|
||||
matrixClass.addProperty("cols", getter = {
|
||||
ObjInt.of(matrixOf(thisObj).cols.toLong())
|
||||
})
|
||||
matrixClass.bindProperty("shape", getter = {
|
||||
matrixClass.addProperty("shape", getter = {
|
||||
ObjList(
|
||||
mutableListOf(
|
||||
ObjInt.of(matrixOf(thisObj).rows.toLong()),
|
||||
@ -133,7 +133,7 @@ object ObjMatrixSupport {
|
||||
)
|
||||
)
|
||||
})
|
||||
matrixClass.bindProperty("isSquare", getter = {
|
||||
matrixClass.addProperty("isSquare", getter = {
|
||||
matrixOf(thisObj).isSquare.toObj()
|
||||
})
|
||||
|
||||
@ -208,17 +208,17 @@ object ObjMatrixSupport {
|
||||
ObjInt.of(matrixOf(thisObj).compareTo(coerceMatrixArg(requireScope(), args.firstAndOnly())).toLong())
|
||||
}
|
||||
|
||||
matrixClass.bindClassFn("fromRows") {
|
||||
matrixClass.addClassFn("fromRows") {
|
||||
newMatrix(matrixClass, parseRows(requireScope(), requiredArg(0)))
|
||||
}
|
||||
matrixClass.bindClassFn("zeros") {
|
||||
matrixClass.addClassFn("zeros") {
|
||||
val rows = requiredArg<ObjInt>(0).value.toInt()
|
||||
val cols = requiredArg<ObjInt>(1).value.toInt()
|
||||
if (rows <= 0) requireScope().raiseIllegalArgument("matrix must have at least one row")
|
||||
if (cols <= 0) requireScope().raiseIllegalArgument("matrix must have at least one column")
|
||||
newMatrix(matrixClass, MatrixData(rows, cols, DoubleArray(rows * cols)))
|
||||
}
|
||||
matrixClass.bindClassFn("identity") {
|
||||
matrixClass.addClassFn("identity") {
|
||||
val size = requiredArg<ObjInt>(0).value.toInt()
|
||||
if (size <= 0) requireScope().raiseIllegalArgument("identity matrix size must be positive")
|
||||
val values = DoubleArray(size * size)
|
||||
|
||||
@ -201,7 +201,14 @@ extern class Decimal() {
|
||||
*
|
||||
* Contexts are dynamic and block-local. After the block finishes, the previous context is restored.
|
||||
*/
|
||||
extern fun withDecimalContext<T>(context: Object, block: ()->T): T
|
||||
extern fun withDecimalContext<T>(context: DecimalContext, block: ()->T): T
|
||||
|
||||
/**
|
||||
* Convenience overload for changing only precision.
|
||||
*
|
||||
* Equivalent to `withDecimalContext(DecimalContext(precision, DecimalRounding.HalfEven), block)`.
|
||||
*/
|
||||
extern fun withDecimalContext<T>(precision: Int, block: ()->T): T
|
||||
|
||||
/**
|
||||
* Convenience overload for changing precision and rounding explicitly.
|
||||
|
||||
@ -205,8 +205,10 @@ class DecimalModuleTest {
|
||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||
assertEquals("0.3333333333", withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
||||
assertEquals("0.666667", withDecimalContext(6) { ("2".d / 3.d).toStringExpanded() })
|
||||
assertEquals("0.666667", withDecimalContext(DecimalContext(6)) { ("2".d / 3.d).toStringExpanded() })
|
||||
assertEquals("0.12", withDecimalContext(2) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("0.13", withDecimalContext(DecimalContext(2, DecimalRounding.HalfAwayFromZero)) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
@ -23,7 +23,7 @@ actual object PerfDefaults {
|
||||
|
||||
actual val ARG_BUILDER: Boolean = true
|
||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
||||
actual val SCOPE_POOL: Boolean = false
|
||||
actual val SCOPE_POOL: Boolean = true
|
||||
|
||||
actual val FIELD_PIC: Boolean = true
|
||||
actual val METHOD_PIC: Boolean = true
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package lyng.complex
|
||||
|
||||
import lyng.decimal
|
||||
import lyng.operators
|
||||
|
||||
/*
|
||||
@ -113,10 +112,8 @@ fun cis(angle: Real): Complex = Complex.fromPolar(1.0, angle)
|
||||
|
||||
val Int.re: Complex get() = Complex.fromInt(this)
|
||||
val Real.re: Complex get() = Complex.fromReal(this)
|
||||
val Decimal.re: Complex get() = Complex(this.toReal(), 0.0)
|
||||
val Int.i: Complex get() = Complex.imaginary(this + 0.0)
|
||||
val Real.i: Complex get() = Complex.imaginary(this)
|
||||
val Decimal.i: Complex get() = Complex(0.0, this.toReal())
|
||||
|
||||
OperatorInterop.register(
|
||||
Int,
|
||||
|
||||
@ -6,11 +6,9 @@ type MatrixScalar = Real | Int
|
||||
extern class Vector() {
|
||||
/** Number of elements. */
|
||||
val size: Int
|
||||
get() = 0
|
||||
|
||||
/** Alias to `size`. */
|
||||
val length: Int
|
||||
get() = 0
|
||||
|
||||
/** Convert to a plain list. */
|
||||
extern fun toList(): List<Real>
|
||||
@ -63,19 +61,15 @@ extern class Vector() {
|
||||
extern class Matrix() {
|
||||
/** Number of rows. */
|
||||
val rows: Int
|
||||
get() = 0
|
||||
|
||||
/** Number of columns. */
|
||||
val cols: Int
|
||||
get() = 0
|
||||
|
||||
/** Two-element shape `[rows, cols]`. */
|
||||
val shape: List<Int>
|
||||
get() = []
|
||||
|
||||
/** Whether `rows == cols`. */
|
||||
val isSquare: Bool
|
||||
get() = false
|
||||
|
||||
/** Element-wise addition. Shapes must match. */
|
||||
extern fun plus(other: Matrix): Matrix
|
||||
|
||||
@ -61,10 +61,10 @@ Notes:
|
||||
rather than silently degrading to some other visible type.
|
||||
- `ResultSet` should stay iterable, but also expose `isEmpty()` for cheap
|
||||
emptiness checks where possible and `size()` as a separate operation.
|
||||
- `ResultSet` is valid only while the owning transaction is active.
|
||||
- Materialized `SqlRow` values should be detached snapshots, so
|
||||
`transaction { tx.select(...).toList() }` is a valid pattern and the rows
|
||||
remain usable after transaction end.
|
||||
- `ResultSet` and all `SqlRow` instances obtained from it are valid only while
|
||||
the owning transaction is active. After transaction end, any further row or
|
||||
result-set access should fail with `SqlUsageException`, even if the provider
|
||||
had buffered data internally.
|
||||
- Portable SQL parameter values should match the row conversion set: `null`,
|
||||
`Bool`, `Int`, `Double`, `Decimal`, `String`, `Buffer`,
|
||||
`Date`, `DateTime`, and `Instant`.
|
||||
|
||||
@ -42,11 +42,11 @@ class SqlRow(
|
||||
- iteration to the end or canceled iteration should close the underlying
|
||||
resources automatically
|
||||
- using the result set after its transaction ends is invalid
|
||||
- rows obtained from the result set stay usable after the owning
|
||||
transaction ends once they have been materialized
|
||||
- rows obtained from the result set are also invalid after the owning
|
||||
transaction ends, even if the implementation had already buffered them
|
||||
|
||||
Calling [toList] while the transaction is active is the normal way to
|
||||
detach rows for use after the transaction block returns.
|
||||
If user code wants row data to survive independently, it should copy the
|
||||
values it needs into ordinary Lyng objects while the transaction is active.
|
||||
*/
|
||||
interface ResultSet : Iterable<SqlRow> {
|
||||
/*
|
||||
|
||||
@ -96,8 +96,8 @@ row-id behavior are all connection-local.
|
||||
The provider may stream rows or buffer them, but it must preserve the core
|
||||
contract:
|
||||
- result sets are valid only while the owning transaction is active
|
||||
- rows obtained from a result set should stay usable after the owning
|
||||
transaction ends once they were materialized, e.g. with `toList()`
|
||||
- rows obtained from a result set are also invalid after the owning
|
||||
transaction ends, even if they were already buffered
|
||||
- iteration closes underlying resources when finished or canceled
|
||||
- `isEmpty()` should be cheap where possible
|
||||
- `size()` may consume or buffer the full result
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user