diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md deleted file mode 100644 index f768eab..0000000 --- a/bytecode_migration_plan.md +++ /dev/null @@ -1,7 +0,0 @@ -# Bytecode Migration Plan (Archived) - -Status: completed. - -Historical reference: -- `notes/archive/bytecode_migration_plan.md` (full plan) -- `notes/archive/bytecode_migration_plan_completed.md` (summary) diff --git a/docs/Array.md b/docs/Array.md index f043f57..9480b55 100644 --- a/docs/Array.md +++ b/docs/Array.md @@ -4,6 +4,13 @@ It's an interface if the [Collection] that provides indexing access, like `array Array therefore implements [Iterable] too. Well known implementations of `Array` are [List] and [ImmutableList]. +The language-level bracket syntax supports one or more selectors: + +- `value[i]` +- `value[i, j]` + +Concrete array-like types decide what selectors they accept. Built-in list-like arrays use one selector at a time; custom types such as matrices may interpret multiple selectors. + Array adds the following methods: ## Binary search diff --git a/docs/List.md b/docs/List.md index d109fe6..017797a 100644 --- a/docs/List.md +++ b/docs/List.md @@ -30,6 +30,13 @@ There is a shortcut for the last: __Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too. +The language also allows multi-selector indexing syntax such as `value[i, j]`, but `List` itself uses a single selector only: + +- `list[index]` for one element +- `list[range]` for a slice copy + +Multi-selector indexing is intended for custom indexers such as `Matrix`. + ## Concatenation You can concatenate lists or iterable objects: diff --git a/docs/Matrix.md b/docs/Matrix.md new file mode 100644 index 0000000..8cbb0e6 --- /dev/null +++ b/docs/Matrix.md @@ -0,0 +1,192 @@ +# Matrix (`lyng.matrix`) + +`lyng.matrix` adds dense immutable `Matrix` and `Vector` types for linear algebra. + +Import it when you need matrix or vector arithmetic: + +```lyng +import lyng.matrix +``` + +## Construction + +Create vectors from a flat list and matrices from nested row lists: + +```lyng +import lyng.matrix + +val v: Vector = vector([1, 2, 3]) +val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + +assertEquals([1.0, 2.0, 3.0], v.toList()) +assertEquals([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], m.toList()) +``` + +Factory methods are also available: + +```lyng +import lyng.matrix + +val z: Vector = Vector.zeros(3) +val i: Matrix = Matrix.identity(3) +val m: Matrix = Matrix.zeros(2, 4) +``` + +All elements are standard double-precision numeric values internally. + +## Shapes + +Matrices may have any rectangular geometry: + +```lyng +import lyng.matrix + +val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + +assertEquals(2, a.rows) +assertEquals(3, a.cols) +assertEquals([2, 3], a.shape) +assertEquals(false, a.isSquare) +``` + +Vectors expose: + +- `size` +- `length` as an alias of `size` + +## Matrix Operations + +Supported matrix operations: + +- `+` and `-` for element-wise matrix arithmetic +- `*` for matrix-matrix product +- `*` and `/` by a scalar +- `transpose()` +- `trace()` +- `rank()` +- `determinant()` +- `inverse()` +- `solve(rhs)` for `Vector` or `Matrix` right-hand sides + +Example: + +```lyng +import lyng.matrix + +val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) +val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]]) +val product: Matrix = a * b +assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList()) +assertEquals([[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]], a.transpose().toList()) +``` + +Inverse and solve: + +```lyng +import lyng.matrix + +val a: Matrix = matrix([[4, 7], [2, 6]]) +val rhs: Vector = vector([1, 0]) + +val inv: Matrix = a.inverse() +val x: Vector = a.solve(rhs) + +assert(abs(a.determinant() - 10.0) < 1e-9) +assert(abs(inv.get(0, 0) - 0.6) < 1e-9) +assert(abs(x.get(0) - 0.6) < 1e-9) +``` + +## Vector Operations + +Supported vector operations: + +- `+` and `-` +- scalar `*` and `/` +- `dot(other)` +- `norm()` +- `normalize()` +- `cross(other)` for 3D vectors +- `outer(other)` producing a matrix + +```lyng +import lyng.matrix + +val a: Vector = vector([1, 2, 3]) +val b: Vector = vector([2, 0, 0]) + +assertEquals(2.0, a.dot(b)) +assertEquals([0.2672612419124244, 0.5345224838248488, 0.8017837257372732], a.normalize().toList()) +``` + +## Indexing and Slicing + +`Matrix` supports both method-style indexing and bracket syntax. + +Scalar access: + +```lyng +import lyng.matrix + +val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + +assertEquals(6.0, m.get(1, 2)) +assertEquals(6.0, m[1, 2]) +``` + +Bracket indexing accepts two selectors: `[row, col]`. +Each selector may be either: + +- an `Int` +- a `Range` + +Examples: + +```lyng +import lyng.matrix + +val m: Matrix = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + +assertEquals(7.0, m[1, 2]) +val columnSlice: Matrix = m[0..2, 2] +val topLeft: Matrix = m[0..1, 0..1] +val tail: Matrix = m[1.., 1..] +assertEquals([[3.0], [7.0], [11.0]], columnSlice.toList()) +assertEquals([[1.0, 2.0], [5.0, 6.0]], topLeft.toList()) +assertEquals([[6.0, 7.0, 8.0], [10.0, 11.0, 12.0]], tail.toList()) +``` + +Shape rules: + +- `m[Int, Int]` returns a `Real` +- `m[Range, Int]` returns an `Nx1` `Matrix` +- `m[Int, Range]` returns a `1xM` `Matrix` +- `m[Range, Range]` returns a submatrix + +Open-ended ranges are supported: + +- `m[..1, ..1]` +- `m[1.., 1..]` +- `m[.., 2]` + +Stepped ranges are not supported in matrix slicing. + +Slices currently return new matrices, not views. + +## Rows and Columns + +If you want plain lists instead of a sliced matrix: + +```lyng +import lyng.matrix + +val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + +assertEquals([4.0, 5.0, 6.0], a.row(1)) +assertEquals([2.0, 5.0], a.column(1)) +``` + +## Backend Notes + +The matrix module uses a platform-specific backend where available and falls back to pure Kotlin where needed. + +The public Lyng API stays the same across platforms. diff --git a/docs/OOP.md b/docs/OOP.md index 1215712..8fb7078 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -1183,8 +1183,24 @@ collection's sugar won't work with it: assertEquals("buzz", x[0]) >>> void -If you want dynamic to function like an array, create a [feature -request](https://gitea.sergeych.net/SergeychWorks/lyng/issues). +Multiple selectors are packed into one list index object: + + val x = dynamic { + get { + if( it == [1, 2] ) "hit" + else null + } + } + assertEquals("hit", x[1, 2]) + >>> void + +So: + +- `x[i]` passes `i` +- `x[i, j]` passes `[i, j]` +- `x[i, j, k]` passes `[i, j, k]` + +This is the same rule used by Kotlin-backed `getAt` / `putAt` indexers in embedding. # Theory diff --git a/docs/ai_language_reference.md b/docs/ai_language_reference.md index 1fcff8f..79c77ed 100644 --- a/docs/ai_language_reference.md +++ b/docs/ai_language_reference.md @@ -82,11 +82,16 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T - Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`. - Null-safe family: - member access: `?.` - - safe index: `?[i]` + - safe index: `?[i]`, `?[i, j]` - safe invoke: `?(...)` - safe block invoke: `?{ ... }` - elvis: `?:` and `??`. - Increment/decrement: prefix and postfix `++`, `--`. +- Indexing syntax: + - single selector: `a[i]` + - multiple selectors: `a[i, j, k]` + - language-level indexing with multiple selectors is passed to `getAt`/`putAt` as one list-like index object, not as multiple method arguments. + - example: `m[0..2, 2]`. ## 5. Declarations - Variables: diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index 2dc7dff..0119d2d 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -58,6 +58,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s - `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`. - `import lyng.complex` - `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`. +- `import lyng.matrix` + - `Matrix`, `Vector`, `matrix(rows)`, `vector(values)`, dense linear algebra, inversion, solving, and matrix slicing with `m[row, col]`. - `import lyng.buffer` - `Buffer`, `MutableBuffer`. - `import lyng.serialization` diff --git a/docs/embedding.md b/docs/embedding.md index 3c1cea5..f319d89 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -183,6 +183,72 @@ Scope-backed Kotlin lambdas receive a `ScopeFacade` (not a full `Scope`). For mi If you truly need the full `Scope` (e.g., for low-level interop), use `requireScope()` explicitly. +### 4.5) Indexers from Kotlin: `getAt` and `putAt` + +Lyng bracket syntax is dispatched through `getAt` and `putAt`. + +That means: + +- `x[i]` calls `getAt(index)` +- `x[i] = value` calls `putAt(index, value)` or `setAt(index, value)` +- field-like `x["name"]` also uses the same index path unless you expose a real field/property + +For Kotlin-backed classes, bind indexers as ordinary methods named `getAt` and `putAt`: + +```kotlin +moduleScope.eval(""" + extern class Grid { + override fun getAt(index: List): Int + override fun putAt(index: List, value: Int): void + } +""".trimIndent()) + +moduleScope.bind("Grid") { + init { _ -> data = IntArray(4) } + + addFun("getAt") { + val index = args.requiredArg(0) + val row = (index.list[0] as ObjInt).value.toInt() + val col = (index.list[1] as ObjInt).value.toInt() + val data = (thisObj as ObjInstance).data as IntArray + ObjInt.of(data[row * 2 + col].toLong()) + } + + addFun("putAt") { + val index = args.requiredArg(0) + val value = args.requiredArg(1).value.toInt() + val row = (index.list[0] as ObjInt).value.toInt() + val col = (index.list[1] as ObjInt).value.toInt() + val data = (thisObj as ObjInstance).data as IntArray + data[row * 2 + col] = value + ObjVoid + } +} +``` + +Usage from Lyng: + +```lyng +val g = Grid() +g[0, 1] = 42 +assertEquals(42, g[0, 1]) +``` + +Important rule: multiple selectors inside brackets are packed into one index object. +So: + +- `x[i]` passes `i` +- `x[i, j]` passes a `List` containing `[i, j]` +- `x[i, j, k]` passes `[i, j, k]` + +This applies equally to: + +- Kotlin-backed classes +- Lyng classes overriding `getAt` +- `dynamic { get { ... } set { ... } }` + +If you want multi-axis slicing semantics, decode that list yourself in `getAt`. + ### 5) Add Kotlin‑backed fields If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances. diff --git a/docs/math.md b/docs/math.md index 760d874..7d4f7b9 100644 --- a/docs/math.md +++ b/docs/math.md @@ -110,6 +110,56 @@ For example: assert( 5.clamp(0..10) == 5 ) >>> void +## Linear algebra: `lyng.matrix` + +For vectors and dense matrices, import `lyng.matrix`: + +```lyng +import lyng.matrix +``` + +It provides: + +- `Vector` +- `Matrix` +- `vector(values)` +- `matrix(rows)` + +Core operations include: + +- matrix addition and subtraction +- matrix-matrix multiplication +- matrix-vector multiplication +- transpose +- determinant +- inverse +- linear solve +- vector dot, norm, normalize, cross, outer product + +Example: + +```lyng +import lyng.matrix + +val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) +val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]]) +val product: Matrix = a * b +assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList()) +``` + +Matrices also support two-axis bracket indexing and slicing: + +```lyng +import lyng.matrix + +val m: Matrix = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) +assertEquals(6.0, m[1, 2]) +val sub: Matrix = m[0..1, 1..2] +assertEquals([[2.0, 3.0], [5.0, 6.0]], sub.toList()) +``` + +See [Matrix](Matrix.md) for the full API. + ## Random values Lyng stdlib provides a global random singleton and deterministic seeded generators: diff --git a/docs/tutorial.md b/docs/tutorial.md index 4fd6ba8..281f1b4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -375,6 +375,18 @@ It is rather simple, like everywhere else: See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed. +For linear algebra, import `lyng.matrix`: + + import lyng.matrix + + val a: Matrix = matrix([[1, 2], [3, 4]]) + val i: Matrix = Matrix.identity(2) + val sum: Matrix = a + i + assertEquals([[2.0, 2.0], [3.0, 5.0]], sum.toList()) + >>> void + +See [Matrix](Matrix.md) for vectors, matrix multiplication, inversion, and slicing such as `m[0..2, 1]`. + Logical operation could be used the same var x = 10 @@ -811,6 +823,14 @@ Lists can contain any type of objects, lists too: Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md). +In general, bracket indexing may contain more than one selector: + + value[i] + value[i, j] + +For built-in lists, strings, maps, and buffers, the selector is usually a single value such as an `Int`, `Range`, or `Regex`. +For types with custom indexers, multiple selectors are packed into one list-like index object and passed to `getAt` / `putAt`. + When you want to "flatten" it to single array, you can use splat syntax: [1, ...[2,3], 4] @@ -1699,6 +1719,14 @@ Open-ended ranges could be used to get start and end too: assertEquals( "pult", "catapult"[ 4.. ]) >>> void +The same bracket syntax is also used by imported numeric modules such as `lyng.matrix`, where indexing can be multi-axis: + + import lyng.matrix + + val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + assertEquals(6.0, m[1, 2]) + >>> void + ### String operations Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also diff --git a/docs/whats_new.md b/docs/whats_new.md index 4f02a94..9fd98ab 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -5,6 +5,53 @@ For a programmer-focused migration summary, see `docs/whats_new_1_5.md`. ## Language Features +### Matrix and Vector Module (`lyng.matrix`) +Lyng now ships a dense linear algebra module with immutable double-precision `Matrix` and `Vector` types. + +It provides: + +- `matrix([[...]])` and `vector([...])` +- matrix multiplication +- matrix inversion +- determinant, trace, rank +- solving `A * x = b` +- vector operations such as `dot`, `normalize`, `cross`, and `outer` + +```lyng +import lyng.matrix + +val a: Matrix = matrix([[4, 7], [2, 6]]) +val inv: Matrix = a.inverse() +assert(abs(inv.get(0, 0) - 0.6) < 1e-9) +``` + +Matrices also support Lyng-style slicing: + +```lyng +import lyng.matrix + +val m: Matrix = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) +assertEquals(6.0, m[1, 2]) +val column: Matrix = m[0..2, 2] +val tail: Matrix = m[1.., 1..] +assertEquals([[3.0], [6.0], [9.0]], column.toList()) +assertEquals([[5.0, 6.0], [8.0, 9.0]], tail.toList()) +``` + +See [Matrix](Matrix.md). + +### Multiple Selectors in Bracket Indexing +Bracket indexing now accepts more than one selector: + +```lyng +value[i] +value[i, j] +value[i, j, k] +``` + +For custom indexers, multiple selectors are packed into one list-like index object and dispatched through `getAt` / `putAt`. +This is the rule used by `lyng.matrix` and by embedding APIs for Kotlin-backed indexers. + ### Decimal Arithmetic Module (`lyng.decimal`) Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aaa2c20..7b4c687 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,13 +2,14 @@ agp = "8.5.2" clikt = "5.0.3" mordant = "3.0.2" -kotlin = "2.3.0" +kotlin = "2.3.20" android-minSdk = "24" android-compileSdk = "34" kotlinx-coroutines = "1.10.2" kotlinx-datetime = "0.6.1" mp_bintools = "0.3.2" ionspin-bignum = "0.3.10" +multik = "0.3.0" firebaseCrashlyticsBuildtools = "3.0.3" okioVersion = "3.10.2" compiler = "3.2.0-alpha11" @@ -24,6 +25,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" } ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" } +multik-default = { module = "org.jetbrains.kotlinx:multik-default", version.ref = "multik" } firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" } okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt index 3cb1ac5..cd79746 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt @@ -23,6 +23,8 @@ import com.intellij.execution.ui.ConsoleViewContentType import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowAnchor @@ -36,9 +38,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import net.sergeych.lyng.idea.LyngIcons +import java.io.File class RunLyngScriptAction : AnAction(LyngIcons.FILE) { - private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private fun getPsiFile(e: AnActionEvent): PsiFile? { val project = e.project ?: return null @@ -48,36 +51,99 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) { } } + private fun getRunnableFile(e: AnActionEvent): PsiFile? { + val psiFile = getPsiFile(e) ?: return null + val virtualFile = psiFile.virtualFile ?: return null + if (!virtualFile.isInLocalFileSystem) return null + if (!psiFile.name.endsWith(".lyng")) return null + return psiFile + } + override fun update(e: AnActionEvent) { - val psiFile = getPsiFile(e) - val isLyng = psiFile?.name?.endsWith(".lyng") == true - e.presentation.isEnabledAndVisible = isLyng - if (isLyng) { - e.presentation.isEnabled = false - e.presentation.text = "Run '${psiFile.name}' (disabled)" - e.presentation.description = "Running scripts from the IDE is disabled; use the CLI." + val psiFile = getRunnableFile(e) + val isRunnable = psiFile != null + e.presentation.isEnabledAndVisible = isRunnable + if (isRunnable) { + e.presentation.text = "Run '${psiFile.name}'" + e.presentation.description = "Run the current Lyng script using the Lyng CLI" } else { e.presentation.text = "Run Lyng Script" + e.presentation.description = "Run the current Lyng script" } } override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return - val psiFile = getPsiFile(e) ?: return - val fileName = psiFile.name + val psiFile = getRunnableFile(e) ?: return + val virtualFile = psiFile.virtualFile ?: return + FileDocumentManager.getInstance().getDocument(virtualFile)?.let { document -> + FileDocumentManager.getInstance().saveDocument(document) + } + val filePath = virtualFile.path + val workingDir = virtualFile.parent?.path ?: project.basePath ?: File(filePath).parent val (console, toolWindow) = getConsoleAndToolWindow(project) console.clear() toolWindow.show { scope.launch { - console.print("--- Run is disabled ---\n", ConsoleViewContentType.SYSTEM_OUTPUT) - console.print("Lyng now runs in bytecode-only mode; the IDE no longer evaluates scripts.\n", ConsoleViewContentType.NORMAL_OUTPUT) - console.print("Use the CLI to run scripts, e.g. `lyng run $fileName`.\n", ConsoleViewContentType.NORMAL_OUTPUT) + val command = startLyngProcess(filePath, workingDir) + if (command == null) { + printToConsole(console, "Unable to start Lyng CLI.\n", ConsoleViewContentType.ERROR_OUTPUT) + printToConsole(console, "Tried commands: lyng, jlyng.\n", ConsoleViewContentType.ERROR_OUTPUT) + printToConsole(console, "Install `lyng` or `jlyng` and make sure it is available on PATH.\n", ConsoleViewContentType.NORMAL_OUTPUT) + return@launch + } + + printToConsole( + console, + "Running ${command.commandLine} in ${command.workingDir}\n", + ConsoleViewContentType.SYSTEM_OUTPUT + ) + streamProcess(command.process, console) + val exitCode = command.process.waitFor() + val outputType = if (exitCode == 0) ConsoleViewContentType.SYSTEM_OUTPUT else ConsoleViewContentType.ERROR_OUTPUT + printToConsole(console, "\nProcess finished with exit code $exitCode\n", outputType) } } } + private suspend fun streamProcess(process: Process, console: ConsoleView) { + val stdout = scope.launch { + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { printToConsole(console, "$it\n", ConsoleViewContentType.NORMAL_OUTPUT) } + } + } + val stderr = scope.launch { + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { printToConsole(console, "$it\n", ConsoleViewContentType.ERROR_OUTPUT) } + } + } + stdout.join() + stderr.join() + } + + private fun printToConsole(console: ConsoleView, text: String, type: ConsoleViewContentType) { + ApplicationManager.getApplication().invokeLater { + console.print(text, type) + } + } + + private fun startLyngProcess(filePath: String, workingDir: String?): StartedProcess? { + val candidates = listOf("lyng", "jlyng") + for (candidate in candidates) { + try { + val process = ProcessBuilder(candidate, filePath) + .directory(workingDir?.let(::File)) + .start() + return StartedProcess(process, "$candidate $filePath", workingDir ?: File(filePath).parent.orEmpty()) + } catch (_: java.io.IOException) { + // Try the next candidate when the command is not available. + } + } + return null + } + private fun getConsoleAndToolWindow(project: Project): Pair { val toolWindowManager = ToolWindowManager.getInstance(project) var toolWindow = toolWindowManager.getToolWindow(ToolWindowId.RUN) @@ -106,4 +172,10 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) { contentManager.setSelectedContent(content) return console to actualToolWindow } + + private data class StartedProcess( + val process: Process, + val commandLine: String, + val workingDir: String + ) } diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 4751f04..d0367d2 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -29,7 +29,7 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) // alias(libs.plugins.vanniktech.mavenPublish) - kotlin("plugin.serialization") version "2.2.21" + kotlin("plugin.serialization") version "2.3.20" id("com.codingfeline.buildkonfig") version "0.17.1" `maven-publish` } @@ -110,6 +110,27 @@ kotlin { implementation(libs.kotlinx.coroutines.test) } } + val matrixMultikMain by creating { + dependsOn(commonMain) + dependencies { + implementation(libs.multik.default) + } + } + val matrixPureMain by creating { + dependsOn(commonMain) + } + val jvmMain by getting { dependsOn(matrixMultikMain) } + val androidMain by getting { dependsOn(matrixPureMain) } + val jsMain by getting { dependsOn(matrixMultikMain) } + val wasmJsMain by getting { dependsOn(matrixMultikMain) } + val iosX64Main by getting { dependsOn(matrixMultikMain) } + val iosArm64Main by getting { dependsOn(matrixMultikMain) } + val iosSimulatorArm64Main by getting { dependsOn(matrixMultikMain) } + val macosX64Main by getting { dependsOn(matrixMultikMain) } + val macosArm64Main by getting { dependsOn(matrixMultikMain) } + val mingwX64Main by getting { dependsOn(matrixMultikMain) } + val linuxX64Main by getting { dependsOn(matrixMultikMain) } + val linuxArm64Main by getting { dependsOn(matrixPureMain) } val jvmTest by getting { dependencies { // Allow tests to load external docs like lyng.io.fs via registrar diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 404b262..9deb84d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2915,9 +2915,9 @@ class Compiler( operand?.let { left -> // array access via ObjRef val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX - val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression") + val index = parseIndexExpression() ?: throw ScriptError(t.pos, "Expecting index expression") cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal") - operand = IndexRef(left, StatementRef(index), isOptional) + operand = IndexRef(left, index, isOptional) } ?: run { // array literal val entries = parseArrayLiteral() @@ -3412,6 +3412,20 @@ class Compiler( } } + private suspend fun parseIndexExpression(): ObjRef? { + val first = parseExpressionLevel() ?: return null + if (!cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)) { + return first + } + + val entries = mutableListOf(ListEntry.Element(first)) + do { + val next = parseExpressionLevel() ?: throw ScriptError(cc.currentPos(), "Expecting index expression") + entries += ListEntry.Element(next) + } while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)) + return ListLiteralRef(entries) + } + private suspend fun parseDestructuringPattern(): List { // it should be called after Token.Type.LBRACKET is consumed val entries = mutableListOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 7def164..163d3b1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.stdlib_included.complexLyng import net.sergeych.lyng.stdlib_included.decimalLyng +import net.sergeych.lyng.stdlib_included.matrixLyng import net.sergeych.lyng.stdlib_included.observableLyng import net.sergeych.lyng.stdlib_included.operatorsLyng import net.sergeych.lyng.stdlib_included.rootLyng @@ -839,6 +840,10 @@ class Script( module.eval(Source("lyng.decimal", decimalLyng)) ObjBigDecimalSupport.bindTo(module) } + addPackage("lyng.matrix") { module -> + module.eval(Source("lyng.matrix", matrixLyng)) + ObjMatrixSupport.bindTo(module) + } addPackage("lyng.complex") { module -> module.eval(Source("lyng.complex", complexLyng)) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/MatrixData.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/MatrixData.kt new file mode 100644 index 0000000..7e8e95f --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/MatrixData.kt @@ -0,0 +1,152 @@ +/* + * 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.matrix + +internal data class MatrixData( + val rows: Int, + val cols: Int, + val values: DoubleArray +) { + init { + require(rows > 0) { "matrix must have at least one row" } + require(cols > 0) { "matrix must have at least one column" } + require(values.size == rows * cols) { + "matrix data size ${values.size} does not match shape ${rows}x${cols}" + } + } + + val isSquare: Boolean get() = rows == cols + + fun at(row: Int, col: Int): Double { + require(row in 0 until rows) { "row index $row out of bounds for $rows rows" } + require(col in 0 until cols) { "column index $col out of bounds for $cols columns" } + return values[row * cols + col] + } + + fun rowValues(row: Int): DoubleArray { + require(row in 0 until rows) { "row index $row out of bounds for $rows rows" } + val start = row * cols + return values.copyOfRange(start, start + cols) + } + + fun columnValues(col: Int): DoubleArray { + require(col in 0 until cols) { "column index $col out of bounds for $cols columns" } + return DoubleArray(rows) { row -> values[row * cols + col] } + } + + fun slice(rowIndices: IntArray, colIndices: IntArray): MatrixData { + require(rowIndices.isNotEmpty()) { "matrix slice must include at least one row" } + require(colIndices.isNotEmpty()) { "matrix slice must include at least one column" } + val out = DoubleArray(rowIndices.size * colIndices.size) + var offset = 0 + for (row in rowIndices) { + require(row in 0 until rows) { "row index $row out of bounds for $rows rows" } + val rowOffset = row * cols + for (col in colIndices) { + require(col in 0 until cols) { "column index $col out of bounds for $cols columns" } + out[offset++] = values[rowOffset + col] + } + } + return MatrixData(rowIndices.size, colIndices.size, out) + } + + fun plus(other: MatrixData): MatrixData { + requireSameShape(other) + return MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] + other.values[index] }) + } + + fun minus(other: MatrixData): MatrixData { + requireSameShape(other) + return MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] - other.values[index] }) + } + + fun scale(factor: Double): MatrixData = + MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] * factor }) + + fun divide(divisor: Double): MatrixData { + require(divisor != 0.0) { "matrix division by zero" } + return MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] / divisor }) + } + + fun transpose(): MatrixData { + val out = DoubleArray(values.size) + for (row in 0 until rows) { + for (col in 0 until cols) { + out[col * rows + row] = values[row * cols + col] + } + } + return MatrixData(cols, rows, out) + } + + fun multiply(other: MatrixData): MatrixData = PlatformMatrixBackend.multiply(this, other) + + fun multiply(other: VectorData): VectorData = PureMatrixAlgorithms.multiply(this, other) + + fun solve(other: VectorData): VectorData = PureMatrixAlgorithms.solve(this, other) + + fun solve(other: MatrixData): MatrixData = PureMatrixAlgorithms.solve(this, other) + + fun determinant(): Double = PureMatrixAlgorithms.determinant(this) + + fun inverse(): MatrixData = PlatformMatrixBackend.inverse(this) + + fun trace(): Double = PureMatrixAlgorithms.trace(this) + + fun rank(): Int = PureMatrixAlgorithms.rank(this) + + fun toNestedLists(): List> = + List(rows) { row -> + List(cols) { col -> values[row * cols + col] } + } + + fun render(): String = buildString { + append("Matrix(") + append(rows) + append("x") + append(cols) + append(", [") + for (row in 0 until rows) { + if (row > 0) append(", ") + append("[") + for (col in 0 until cols) { + if (col > 0) append(", ") + append(formatMatrixValue(values[row * cols + col])) + } + append("]") + } + append("])") + } + + fun compareTo(other: MatrixData): Int { + val rowCmp = rows.compareTo(other.rows) + if (rowCmp != 0) return rowCmp + val colCmp = cols.compareTo(other.cols) + if (colCmp != 0) return colCmp + for (index in values.indices) { + val cmp = values[index].compareTo(other.values[index]) + if (cmp != 0) return cmp + } + return 0 + } + + private fun requireSameShape(other: MatrixData) { + require(rows == other.rows && cols == other.cols) { + "matrix shape mismatch: ${rows}x${cols} vs ${other.rows}x${other.cols}" + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/MatrixFormatting.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/MatrixFormatting.kt new file mode 100644 index 0000000..e535a26 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/MatrixFormatting.kt @@ -0,0 +1,30 @@ +/* + * 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.matrix + +import net.sergeych.lyng.obj.ObjReal + +internal fun formatMatrixValue(value: Double): String { + if (value.isFinite()) { + val asLong = value.toLong() + if (asLong.toDouble() == value) { + return asLong.toString() + } + } + return ObjReal.of(value).toString() +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.kt new file mode 100644 index 0000000..336b4d4 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.kt @@ -0,0 +1,23 @@ +/* + * 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.matrix + +internal expect object PlatformMatrixBackend { + fun multiply(left: MatrixData, right: MatrixData): MatrixData + fun inverse(matrix: MatrixData): MatrixData +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/PureMatrixAlgorithms.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/PureMatrixAlgorithms.kt new file mode 100644 index 0000000..876cf89 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/PureMatrixAlgorithms.kt @@ -0,0 +1,286 @@ +/* + * 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.matrix + +internal object PureMatrixAlgorithms { + private const val singularEpsilon = 1e-12 + + fun multiply(left: MatrixData, right: MatrixData): MatrixData { + require(left.cols == right.rows) { + "matrix multiplication shape mismatch: ${left.rows}x${left.cols} cannot multiply ${right.rows}x${right.cols}" + } + val out = DoubleArray(left.rows * right.cols) + for (row in 0 until left.rows) { + val leftRowOffset = row * left.cols + val outRowOffset = row * right.cols + for (pivot in 0 until left.cols) { + val leftValue = left.values[leftRowOffset + pivot] + val rightRowOffset = pivot * right.cols + for (col in 0 until right.cols) { + out[outRowOffset + col] += leftValue * right.values[rightRowOffset + col] + } + } + } + return MatrixData(left.rows, right.cols, out) + } + + fun multiply(left: MatrixData, right: VectorData): VectorData { + require(left.cols == right.size) { + "matrix-vector multiplication shape mismatch: ${left.rows}x${left.cols} cannot multiply length ${right.size}" + } + val out = DoubleArray(left.rows) + for (row in 0 until left.rows) { + var sum = 0.0 + val rowOffset = row * left.cols + for (col in 0 until left.cols) { + sum += left.values[rowOffset + col] * right.values[col] + } + out[row] = sum + } + return VectorData(out) + } + + fun inverse(matrix: MatrixData): MatrixData { + require(matrix.isSquare) { "matrix inverse requires a square matrix, got ${matrix.rows}x${matrix.cols}" } + val n = matrix.rows + val width = n * 2 + val augmented = DoubleArray(n * width) + + for (row in 0 until n) { + for (col in 0 until n) { + augmented[row * width + col] = matrix.values[row * n + col] + } + augmented[row * width + (n + row)] = 1.0 + } + + for (pivotCol in 0 until n) { + var pivotRow = pivotCol + var pivotAbs = kotlin.math.abs(augmented[pivotRow * width + pivotCol]) + for (candidate in pivotCol + 1 until n) { + val candidateAbs = kotlin.math.abs(augmented[candidate * width + pivotCol]) + if (candidateAbs > pivotAbs) { + pivotAbs = candidateAbs + pivotRow = candidate + } + } + require(pivotAbs > singularEpsilon) { "matrix is singular and cannot be inverted" } + if (pivotRow != pivotCol) { + swapRows(augmented, width, pivotRow, pivotCol) + } + + val pivotValue = augmented[pivotCol * width + pivotCol] + val pivotOffset = pivotCol * width + for (col in 0 until width) { + augmented[pivotOffset + col] /= pivotValue + } + + for (row in 0 until n) { + if (row == pivotCol) continue + val factor = augmented[row * width + pivotCol] + if (factor == 0.0) continue + val rowOffset = row * width + for (col in 0 until width) { + augmented[rowOffset + col] -= factor * augmented[pivotOffset + col] + } + } + } + + val inverse = DoubleArray(n * n) + for (row in 0 until n) { + for (col in 0 until n) { + inverse[row * n + col] = augmented[row * width + n + col] + } + } + return MatrixData(n, n, inverse) + } + + fun determinant(matrix: MatrixData): Double { + require(matrix.isSquare) { "matrix determinant requires a square matrix, got ${matrix.rows}x${matrix.cols}" } + val n = matrix.rows + val work = matrix.values.copyOf() + var sign = 1.0 + var determinant = 1.0 + + for (pivotCol in 0 until n) { + var pivotRow = pivotCol + var pivotAbs = kotlin.math.abs(work[pivotRow * n + pivotCol]) + for (candidate in pivotCol + 1 until n) { + val candidateAbs = kotlin.math.abs(work[candidate * n + pivotCol]) + if (candidateAbs > pivotAbs) { + pivotAbs = candidateAbs + pivotRow = candidate + } + } + if (pivotAbs <= singularEpsilon) { + return 0.0 + } + if (pivotRow != pivotCol) { + swapRows(work, n, pivotRow, pivotCol) + sign = -sign + } + + val pivotValue = work[pivotCol * n + pivotCol] + determinant *= pivotValue + + for (row in pivotCol + 1 until n) { + val rowOffset = row * n + val factor = work[rowOffset + pivotCol] / pivotValue + if (factor == 0.0) continue + for (col in pivotCol + 1 until n) { + work[rowOffset + col] -= factor * work[pivotCol * n + col] + } + } + } + + return determinant * sign + } + + fun trace(matrix: MatrixData): Double { + require(matrix.isSquare) { "matrix trace requires a square matrix, got ${matrix.rows}x${matrix.cols}" } + var sum = 0.0 + for (index in 0 until matrix.rows) { + sum += matrix.values[index * matrix.cols + index] + } + return sum + } + + fun rank(matrix: MatrixData): Int { + val work = matrix.values.copyOf() + var rank = 0 + var pivotRow = 0 + val rows = matrix.rows + val cols = matrix.cols + + for (pivotCol in 0 until cols) { + var bestRow = -1 + var bestAbs = singularEpsilon + for (candidate in pivotRow until rows) { + val absValue = kotlin.math.abs(work[candidate * cols + pivotCol]) + if (absValue > bestAbs) { + bestAbs = absValue + bestRow = candidate + } + } + if (bestRow == -1) continue + if (bestRow != pivotRow) { + swapRows(work, cols, bestRow, pivotRow) + } + val pivotValue = work[pivotRow * cols + pivotCol] + for (row in pivotRow + 1 until rows) { + val factor = work[row * cols + pivotCol] / pivotValue + if (kotlin.math.abs(factor) <= singularEpsilon) continue + val rowOffset = row * cols + val pivotOffset = pivotRow * cols + for (col in pivotCol until cols) { + work[rowOffset + col] -= factor * work[pivotOffset + col] + } + } + rank += 1 + pivotRow += 1 + if (pivotRow == rows) break + } + return rank + } + + fun solve(matrix: MatrixData, rhs: VectorData): VectorData { + require(matrix.isSquare) { "matrix solve requires a square matrix, got ${matrix.rows}x${matrix.cols}" } + require(matrix.rows == rhs.size) { + "matrix solve shape mismatch: ${matrix.rows}x${matrix.cols} cannot solve length ${rhs.size}" + } + val solution = solveAugmented(matrix.rows, matrix.cols, 1, matrix.values, rhs.values) + return VectorData(solution) + } + + fun solve(matrix: MatrixData, rhs: MatrixData): MatrixData { + require(matrix.isSquare) { "matrix solve requires a square matrix, got ${matrix.rows}x${matrix.cols}" } + require(matrix.rows == rhs.rows) { + "matrix solve shape mismatch: ${matrix.rows}x${matrix.cols} cannot solve ${rhs.rows}x${rhs.cols}" + } + val solution = solveAugmented(matrix.rows, matrix.cols, rhs.cols, matrix.values, rhs.values) + return MatrixData(matrix.rows, rhs.cols, solution) + } + + private fun solveAugmented(rows: Int, cols: Int, rhsCols: Int, matrixValues: DoubleArray, rhsValues: DoubleArray): DoubleArray { + val width = cols + rhsCols + val augmented = DoubleArray(rows * width) + + for (row in 0 until rows) { + val matrixOffset = row * cols + val augmentedOffset = row * width + for (col in 0 until cols) { + augmented[augmentedOffset + col] = matrixValues[matrixOffset + col] + } + val rhsOffset = row * rhsCols + for (col in 0 until rhsCols) { + augmented[augmentedOffset + cols + col] = rhsValues[rhsOffset + col] + } + } + + for (pivotCol in 0 until cols) { + var pivotRow = pivotCol + var pivotAbs = kotlin.math.abs(augmented[pivotRow * width + pivotCol]) + for (candidate in pivotCol + 1 until rows) { + val candidateAbs = kotlin.math.abs(augmented[candidate * width + pivotCol]) + if (candidateAbs > pivotAbs) { + pivotAbs = candidateAbs + pivotRow = candidate + } + } + require(pivotAbs > singularEpsilon) { "matrix is singular and cannot be solved" } + if (pivotRow != pivotCol) { + swapRows(augmented, width, pivotRow, pivotCol) + } + + val pivotOffset = pivotCol * width + val pivotValue = augmented[pivotOffset + pivotCol] + for (col in pivotCol until width) { + augmented[pivotOffset + col] /= pivotValue + } + + for (row in 0 until rows) { + if (row == pivotCol) continue + val factor = augmented[row * width + pivotCol] + if (kotlin.math.abs(factor) <= singularEpsilon) continue + val rowOffset = row * width + for (col in pivotCol until width) { + augmented[rowOffset + col] -= factor * augmented[pivotOffset + col] + } + } + } + + val solution = DoubleArray(rows * rhsCols) + for (row in 0 until rows) { + val augmentedOffset = row * width + cols + val solutionOffset = row * rhsCols + for (col in 0 until rhsCols) { + solution[solutionOffset + col] = augmented[augmentedOffset + col] + } + } + return solution + } + + private fun swapRows(data: DoubleArray, stride: Int, firstRow: Int, secondRow: Int) { + val firstOffset = firstRow * stride + val secondOffset = secondRow * stride + for (col in 0 until stride) { + val tmp = data[firstOffset + col] + data[firstOffset + col] = data[secondOffset + col] + data[secondOffset + col] = tmp + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/VectorData.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/VectorData.kt new file mode 100644 index 0000000..9c1c84d --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/matrix/VectorData.kt @@ -0,0 +1,113 @@ +/* + * 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.matrix + +import kotlin.math.sqrt + +internal data class VectorData( + val values: DoubleArray +) { + init { + require(values.isNotEmpty()) { "vector must have at least one element" } + } + + val size: Int get() = values.size + + fun at(index: Int): Double { + require(index in values.indices) { "vector index $index out of bounds for length $size" } + return values[index] + } + + fun plus(other: VectorData): VectorData { + require(size == other.size) { "vector size mismatch: $size vs ${other.size}" } + return VectorData(DoubleArray(size) { index -> values[index] + other.values[index] }) + } + + fun minus(other: VectorData): VectorData { + require(size == other.size) { "vector size mismatch: $size vs ${other.size}" } + return VectorData(DoubleArray(size) { index -> values[index] - other.values[index] }) + } + + fun scale(factor: Double): VectorData = + VectorData(DoubleArray(size) { index -> values[index] * factor }) + + fun divide(divisor: Double): VectorData { + require(divisor != 0.0) { "vector division by zero" } + return VectorData(DoubleArray(size) { index -> values[index] / divisor }) + } + + fun dot(other: VectorData): Double { + require(size == other.size) { "vector size mismatch: $size vs ${other.size}" } + var sum = 0.0 + for (index in values.indices) { + sum += values[index] * other.values[index] + } + return sum + } + + fun norm(): Double = sqrt(dot(this)) + + fun normalize(): VectorData { + val length = norm() + require(length != 0.0) { "cannot normalize a zero vector" } + return divide(length) + } + + fun cross(other: VectorData): VectorData { + require(size == 3 && other.size == 3) { "cross product requires two 3D vectors" } + return VectorData( + doubleArrayOf( + values[1] * other.values[2] - values[2] * other.values[1], + values[2] * other.values[0] - values[0] * other.values[2], + values[0] * other.values[1] - values[1] * other.values[0] + ) + ) + } + + fun outer(other: VectorData): MatrixData { + val out = DoubleArray(size * other.size) + for (row in values.indices) { + val rowOffset = row * other.size + for (col in other.values.indices) { + out[rowOffset + col] = values[row] * other.values[col] + } + } + return MatrixData(size, other.size, out) + } + + fun toList(): List = values.asList() + + fun render(): String = buildString { + append("Vector([") + for (index in values.indices) { + if (index > 0) append(", ") + append(formatMatrixValue(values[index])) + } + append("])") + } + + fun compareTo(other: VectorData): Int { + val sizeCmp = size.compareTo(other.size) + if (sizeCmp != 0) return sizeCmp + for (index in values.indices) { + val cmp = values[index].compareTo(other.values[index]) + if (cmp != 0) return cmp + } + return 0 + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt index 5fa93df..7a1ec0f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt @@ -17,22 +17,14 @@ package net.sergeych.lyng.obj -import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal import com.ionspin.kotlin.bignum.decimal.DecimalMode import com.ionspin.kotlin.bignum.decimal.RoundingMode import com.ionspin.kotlin.bignum.integer.BigInteger -import net.sergeych.lyng.Arguments -import net.sergeych.lyng.FrameSlotRef -import net.sergeych.lyng.InteropOperator -import net.sergeych.lyng.ModuleScope -import net.sergeych.lyng.OperatorInteropRegistry -import net.sergeych.lyng.RecordSlotRef -import net.sergeych.lyng.Scope -import net.sergeych.lyng.ScopeFacade -import net.sergeych.lyng.TypeDecl +import net.sergeych.lyng.* import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type import net.sergeych.lyng.requiredArg +import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal object ObjBigDecimalSupport { private const val decimalContextVar = "__lyng_decimal_context__" @@ -88,6 +80,9 @@ object ObjBigDecimalSupport { decimalClass.addFn("toReal") { ObjReal.of(valueOf(thisObj).doubleValue(false)) } + decimalClass.addFn("toString") { + ObjString(valueOf(thisObj).toStringExpanded()) + } decimalClass.addFn("toStringExpanded") { ObjString(valueOf(thisObj).toStringExpanded()) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMatrixSupport.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMatrixSupport.kt new file mode 100644 index 0000000..6413b6b --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMatrixSupport.kt @@ -0,0 +1,385 @@ +/* + * 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.obj + +import net.sergeych.lyng.ModuleScope +import net.sergeych.lyng.Scope +import net.sergeych.lyng.ScopeFacade +import net.sergeych.lyng.matrix.MatrixData +import net.sergeych.lyng.matrix.VectorData +import net.sergeych.lyng.requiredArg + +object ObjMatrixSupport { + private sealed interface MatrixAxisIndex { + data class Single(val value: Int) : MatrixAxisIndex + data class Slice(val values: IntArray) : MatrixAxisIndex + } + + private object BoundMarker + private val defaultMatrix = MatrixData(1, 1, doubleArrayOf(0.0)) + private val defaultVector = VectorData(doubleArrayOf(0.0)) + + suspend fun bindTo(module: ModuleScope) { + val matrixClass = module.requireClass("Matrix") + val vectorClass = module.requireClass("Vector") + if (matrixClass.kotlinClassData === BoundMarker && vectorClass.kotlinClassData === BoundMarker) return + + bindVectorClass(vectorClass, matrixClass) + bindMatrixClass(matrixClass, vectorClass) + } + + private fun bindVectorClass(vectorClass: ObjClass, matrixClass: ObjClass) { + if (vectorClass.kotlinClassData === BoundMarker) return + vectorClass.kotlinClassData = BoundMarker + vectorClass.isAbstract = false + + val hooks = vectorClass.bridgeInitHooks ?: mutableListOf Unit>().also { + vectorClass.bridgeInitHooks = it + } + hooks += { _, instance -> instance.kotlinInstanceData = defaultVector } + + vectorClass.addProperty("size", getter = { + ObjInt.of(vectorOf(thisObj).size.toLong()) + }) + vectorClass.addProperty("length", getter = { + ObjInt.of(vectorOf(thisObj).size.toLong()) + }) + vectorClass.addFn("toList") { + Obj.from(vectorOf(thisObj).toList()) + } + vectorClass.addFn("get") { + ObjReal.of(vectorOf(thisObj).at(requiredArg(0).value.toInt())) + } + vectorClass.addFn("plus") { + newVector(vectorClass, vectorOf(thisObj).plus(coerceVectorArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("minus") { + newVector(vectorClass, vectorOf(thisObj).minus(coerceVectorArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("mul") { + newVector(vectorClass, vectorOf(thisObj).scale(coerceScalarArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("div") { + newVector(vectorClass, vectorOf(thisObj).divide(coerceScalarArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("dot") { + ObjReal.of(vectorOf(thisObj).dot(coerceVectorArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("norm") { + ObjReal.of(vectorOf(thisObj).norm()) + } + vectorClass.addFn("normalize") { + newVector(vectorClass, vectorOf(thisObj).normalize()) + } + vectorClass.addFn("cross") { + newVector(vectorClass, vectorOf(thisObj).cross(coerceVectorArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("outer") { + newMatrix(matrixClass, vectorOf(thisObj).outer(coerceVectorArg(requireScope(), args.firstAndOnly()))) + } + vectorClass.addFn("toString") { + ObjString(vectorOf(thisObj).render()) + } + vectorClass.addFn("compareTo") { + ObjInt.of(vectorOf(thisObj).compareTo(coerceVectorArg(requireScope(), args.firstAndOnly())).toLong()) + } + + vectorClass.addClassFn("fromList") { + newVector(vectorClass, parseVector(requireScope(), requiredArg(0))) + } + vectorClass.addClassFn("zeros") { + val size = requiredArg(0).value.toInt() + if (size <= 0) requireScope().raiseIllegalArgument("vector size must be positive") + newVector(vectorClass, VectorData(DoubleArray(size))) + } + } + + private fun bindMatrixClass(matrixClass: ObjClass, vectorClass: ObjClass) { + if (matrixClass.kotlinClassData === BoundMarker) return + matrixClass.kotlinClassData = BoundMarker + matrixClass.isAbstract = false + + val hooks = matrixClass.bridgeInitHooks ?: mutableListOf Unit>().also { + matrixClass.bridgeInitHooks = it + } + hooks += { _, instance -> instance.kotlinInstanceData = defaultMatrix } + + matrixClass.addProperty("rows", getter = { + ObjInt.of(matrixOf(thisObj).rows.toLong()) + }) + matrixClass.addProperty("cols", getter = { + ObjInt.of(matrixOf(thisObj).cols.toLong()) + }) + matrixClass.addProperty("shape", getter = { + ObjList( + mutableListOf( + ObjInt.of(matrixOf(thisObj).rows.toLong()), + ObjInt.of(matrixOf(thisObj).cols.toLong()) + ) + ) + }) + matrixClass.addProperty("isSquare", getter = { + matrixOf(thisObj).isSquare.toObj() + }) + + matrixClass.addFn("plus") { + newMatrix(matrixClass, matrixOf(thisObj).plus(coerceMatrixArg(requireScope(), args.firstAndOnly()))) + } + matrixClass.addFn("minus") { + newMatrix(matrixClass, matrixOf(thisObj).minus(coerceMatrixArg(requireScope(), args.firstAndOnly()))) + } + matrixClass.addFn("mul") { + when (val other = args.firstAndOnly()) { + is ObjInstance -> when (other.objClass.className) { + "Matrix" -> newMatrix(matrixClass, matrixOf(thisObj).multiply(matrixOf(other))) + "Vector" -> newVector(vectorClass, matrixOf(thisObj).multiply(vectorOf(other))) + else -> newMatrix(matrixClass, matrixOf(thisObj).scale(coerceScalarArg(requireScope(), other))) + } + else -> newMatrix(matrixClass, matrixOf(thisObj).scale(coerceScalarArg(requireScope(), other))) + } + } + matrixClass.addFn("div") { + newMatrix(matrixClass, matrixOf(thisObj).divide(coerceScalarArg(requireScope(), args.firstAndOnly()))) + } + matrixClass.addFn("transpose") { + newMatrix(matrixClass, matrixOf(thisObj).transpose()) + } + matrixClass.addFn("trace") { + ObjReal.of(matrixOf(thisObj).trace()) + } + matrixClass.addFn("rank") { + ObjInt.of(matrixOf(thisObj).rank().toLong()) + } + matrixClass.addFn("determinant") { + ObjReal.of(matrixOf(thisObj).determinant()) + } + matrixClass.addFn("inverse") { + newMatrix(matrixClass, matrixOf(thisObj).inverse()) + } + matrixClass.addFn("solve") { + when (val rhs = args.firstAndOnly()) { + is ObjInstance -> when (rhs.objClass.className) { + "Vector" -> newVector(vectorClass, matrixOf(thisObj).solve(vectorOf(rhs))) + "Matrix" -> newMatrix(matrixClass, matrixOf(thisObj).solve(matrixOf(rhs))) + else -> requireScope().raiseClassCastError("Matrix.solve expects Vector or Matrix") + } + else -> requireScope().raiseClassCastError("Matrix.solve expects Vector or Matrix") + } + } + matrixClass.addFn("get") { + ObjReal.of( + matrixOf(thisObj).at( + requiredArg(0).value.toInt(), + requiredArg(1).value.toInt() + ) + ) + } + matrixClass.addFn("getAt") { + resolveMatrixIndex(matrixClass, matrixOf(thisObj), args.firstAndOnly(), thisObj) + } + matrixClass.addFn("row") { + doubleArrayToObjList(matrixOf(thisObj).rowValues(requiredArg(0).value.toInt())) + } + matrixClass.addFn("column") { + doubleArrayToObjList(matrixOf(thisObj).columnValues(requiredArg(0).value.toInt())) + } + matrixClass.addFn("toList") { + Obj.from(matrixOf(thisObj).toNestedLists()) + } + matrixClass.addFn("toString") { + ObjString(matrixOf(thisObj).render()) + } + matrixClass.addFn("compareTo") { + ObjInt.of(matrixOf(thisObj).compareTo(coerceMatrixArg(requireScope(), args.firstAndOnly())).toLong()) + } + + matrixClass.addClassFn("fromRows") { + newMatrix(matrixClass, parseRows(requireScope(), requiredArg(0))) + } + matrixClass.addClassFn("zeros") { + val rows = requiredArg(0).value.toInt() + val cols = requiredArg(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.addClassFn("identity") { + val size = requiredArg(0).value.toInt() + if (size <= 0) requireScope().raiseIllegalArgument("identity matrix size must be positive") + val values = DoubleArray(size * size) + for (index in 0 until size) { + values[index * size + index] = 1.0 + } + newMatrix(matrixClass, MatrixData(size, size, values)) + } + } + + private fun matrixOf(obj: Obj): MatrixData { + val instance = obj as? ObjInstance ?: error("Matrix receiver must be an object instance") + return instance.kotlinInstanceData as? MatrixData ?: defaultMatrix + } + + private fun vectorOf(obj: Obj): VectorData { + val instance = obj as? ObjInstance ?: error("Vector receiver must be an object instance") + return instance.kotlinInstanceData as? VectorData ?: defaultVector + } + + private suspend fun ScopeFacade.newMatrix(matrixClass: ObjClass, value: MatrixData): ObjInstance { + val instance = call(matrixClass) as? ObjInstance + ?: raiseIllegalState("Matrix() did not return an object instance") + instance.kotlinInstanceData = value + return instance + } + + private suspend fun ScopeFacade.newVector(vectorClass: ObjClass, value: VectorData): ObjInstance { + val instance = call(vectorClass) as? ObjInstance + ?: raiseIllegalState("Vector() did not return an object instance") + instance.kotlinInstanceData = value + return instance + } + + private fun coerceMatrixArg(scope: Scope, value: Obj): MatrixData { + val instance = value as? ObjInstance + ?: scope.raiseClassCastError("expected Matrix, got ${value.objClass.className}") + if (instance.objClass.className != "Matrix") { + scope.raiseClassCastError("expected Matrix, got ${instance.objClass.className}") + } + return instance.kotlinInstanceData as? MatrixData ?: defaultMatrix + } + + private fun coerceVectorArg(scope: Scope, value: Obj): VectorData { + val instance = value as? ObjInstance + ?: scope.raiseClassCastError("expected Vector, got ${value.objClass.className}") + if (instance.objClass.className != "Vector") { + scope.raiseClassCastError("expected Vector, got ${instance.objClass.className}") + } + return instance.kotlinInstanceData as? VectorData ?: defaultVector + } + + private fun coerceScalarArg(scope: Scope, value: Obj): Double = try { + value.toDouble() + } catch (_: IllegalArgumentException) { + scope.raiseClassCastError("expected matrix scalar (Int or Real), got ${value.objClass.className}") + } + + private suspend fun parseRows(scope: Scope, rowsObj: Obj): MatrixData { + val outer = asObjList(scope, rowsObj, "Matrix.fromRows expects a list of rows") + if (outer.list.isEmpty()) scope.raiseIllegalArgument("matrix must have at least one row") + + val rows = outer.list.size + var cols = -1 + val values = mutableListOf() + + for (rowObj in outer.list) { + val row = asObjList(scope, rowObj, "Matrix rows must be lists") + if (cols == -1) { + cols = row.list.size + if (cols <= 0) scope.raiseIllegalArgument("matrix must have at least one column") + } else if (row.list.size != cols) { + scope.raiseIllegalArgument("matrix rows must all have the same length") + } + for (cell in row.list) { + values += coerceScalarArg(scope, cell) + } + } + return MatrixData(rows, cols, values.toDoubleArray()) + } + + private suspend fun parseVector(scope: Scope, valuesObj: Obj): VectorData { + val list = asObjList(scope, valuesObj, "Vector.fromList expects a list") + if (list.list.isEmpty()) scope.raiseIllegalArgument("vector must have at least one element") + return VectorData(list.list.map { coerceScalarArg(scope, it) }.toDoubleArray()) + } + + private suspend fun ScopeFacade.resolveMatrixIndex( + matrixClass: ObjClass, + matrix: MatrixData, + index: Obj, + receiver: Obj + ): Obj { + val tuple = asObjList(requireScope(), index, "Matrix index must be [row, col]") + if (tuple.list.size != 2) { + raiseIllegalArgument("Matrix index must contain exactly two selectors: [row, col]") + } + + val rowIndex = decodeAxisIndex(requireScope(), tuple.list[0], matrix.rows, "row") + val colIndex = decodeAxisIndex(requireScope(), tuple.list[1], matrix.cols, "column") + + return when { + rowIndex is MatrixAxisIndex.Single && colIndex is MatrixAxisIndex.Single -> + ObjReal.of(matrix.at(rowIndex.value, colIndex.value)) + + rowIndex is MatrixAxisIndex.Single && colIndex is MatrixAxisIndex.Slice -> + newMatrix(matrixClass, matrix.slice(intArrayOf(rowIndex.value), colIndex.values)) + + rowIndex is MatrixAxisIndex.Slice && colIndex is MatrixAxisIndex.Single -> + newMatrix(matrixClass, matrix.slice(rowIndex.values, intArrayOf(colIndex.value))) + + rowIndex is MatrixAxisIndex.Slice && colIndex is MatrixAxisIndex.Slice -> + newMatrix(matrixClass, matrix.slice(rowIndex.values, colIndex.values)) + + else -> requireScope().raiseIllegalState("unreachable matrix index state for ${receiver.objClass.className}") + } + } + + private suspend fun decodeAxisIndex(scope: Scope, index: Obj, size: Int, axisName: String): MatrixAxisIndex = + when (index) { + is ObjInt -> { + val value = index.value.toInt() + if (value !in 0 until size) { + scope.raiseIllegalArgument("$axisName index $value out of bounds for length $size") + } + MatrixAxisIndex.Single(value) + } + + is ObjRange -> { + if (index.hasExplicitStep) { + scope.raiseIllegalArgument("Matrix slicing does not support stepped $axisName ranges") + } + val start = index.startInt(scope) + val endExclusive = index.exclusiveIntEnd(scope) ?: size + if (start !in 0..size) { + scope.raiseIllegalArgument("$axisName slice start $start out of bounds for length $size") + } + if (endExclusive !in 0..size) { + scope.raiseIllegalArgument("$axisName slice end $endExclusive out of bounds for length $size") + } + if (start > endExclusive) { + scope.raiseIllegalArgument("$axisName slice start $start is after end $endExclusive") + } + if (start == endExclusive) { + scope.raiseIllegalArgument("Matrix slice must include at least one $axisName") + } + MatrixAxisIndex.Slice(IntArray(endExclusive - start) { start + it }) + } + + else -> scope.raiseClassCastError("Matrix $axisName selector must be Int or Range, got ${index.objClass.className}") + } + + private suspend fun asObjList(scope: Scope, value: Obj, message: String): ObjList = when (value) { + is ObjList -> value + else -> if (value.isInstanceOf(ObjIterable)) { + value.callMethod(scope, "toList") + } else { + scope.raiseClassCastError(message) + } + } + + private fun doubleArrayToObjList(values: DoubleArray): ObjList = + ObjList(values.map { ObjReal.of(it) }.toMutableList()) +} diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt index 5539fa9..3719a8b 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt @@ -135,4 +135,15 @@ class BigDecimalModuleTest { """.trimIndent() ) } + + @Test + fun testDefaultToString() = runTest { + eval(""" + import lyng.decimal + + var s0 = "0.1".d + "0.1".d + assertEquals("0.2", s0.toStringExpanded()) + assertEquals("0.2", s0.toString()) + """.trimIndent()) + } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt new file mode 100644 index 0000000..189544f --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt @@ -0,0 +1,155 @@ +/* + * 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 + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class MatrixModuleTest { + @Test + fun testMatrixConstructionAndShape() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.matrix + + val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + val v: Vector = vector([1, 2, 3]) + assertEquals(2, a.rows) + assertEquals(3, a.cols) + assertEquals([2, 3], a.shape) + assertEquals(false, a.isSquare) + assertEquals([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], a.toList()) + assertEquals("Matrix(2x3, [[1, 2, 3], [4, 5, 6]])", a.toString()) + assertEquals(3, v.size) + assertEquals([1.0, 2.0, 3.0], v.toList()) + assertEquals("Vector([1, 2, 3])", v.toString()) + assertEquals([0.2672612419124244, 0.5345224838248488, 0.8017837257372732], v.normalize().toList()) + """.trimIndent() + ) + } + + @Test + fun testMatrixArithmeticAndTranspose() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.matrix + + val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]]) + val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]]) + val product: Matrix = a * b + assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList()) + + val scaled: Matrix = product * 0.5 + assertEquals([[29.0, 32.0], [69.5, 77.0]], scaled.toList()) + + val sum: Matrix = matrix([[1, 2], [3, 4]]) + Matrix.identity(2) + assertEquals([[2.0, 2.0], [3.0, 5.0]], sum.toList()) + + val transposed: Matrix = a.transpose() + assertEquals([[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]], transposed.toList()) + assertEquals([4.0, 5.0, 6.0], a.row(1)) + assertEquals([2.0, 5.0], a.column(1)) + + val x: Vector = vector([1, 0.5, -1]) + val y: Vector = a * x + assertEquals([-1.0, 0.5], y.toList()) + + val shifted: Vector = x + vector([2, 2, 2]) + assertEquals([3.0, 2.5, 1.0], shifted.toList()) + val d0: Vector = vector([1, 2, 3]) + val d1: Vector = vector([2, 0, 0]) + assertEquals(2.0, d0.dot(d1)) + val cx0: Vector = vector([1, 0, 0]) + val cx1: Vector = vector([0, 1, 0]) + assertEquals([0.0, 0.0, 1.0], cx0.cross(cx1).toList()) + val o0: Vector = vector([1.5, 3.0]) + val o1: Vector = vector([2, 2.6666666666666665]) + assertEquals([[3.0, 4.0], [6.0, 8.0]], o0.outer(o1).toList()) + """.trimIndent() + ) + } + + @Test + fun testMatrixDeterminantAndInverse() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.matrix + + val eps = 1e-9 + val a: Matrix = matrix([[4, 7], [2, 6]]) + assert(abs(a.determinant() - 10.0) < eps) + assertEquals(10.0, a.trace()) + assertEquals(2, a.rank()) + + val inv: Matrix = a.inverse() + assert(abs(inv.get(0, 0) - 0.6) < eps) + assert(abs(inv.get(0, 1) + 0.7) < eps) + assert(abs(inv.get(1, 0) + 0.2) < eps) + assert(abs(inv.get(1, 1) - 0.4) < eps) + + val identity: Matrix = a * inv + assert(abs(identity.get(0, 0) - 1.0) < eps) + assert(abs(identity.get(0, 1)) < eps) + assert(abs(identity.get(1, 0)) < eps) + assert(abs(identity.get(1, 1) - 1.0) < eps) + + val rhs: Vector = vector([1, 0]) + val solution: Vector = a.solve(rhs) + assert(abs(solution.get(0) - 0.6) < eps) + assert(abs(solution.get(1) + 0.2) < eps) + + val rhsMatrix: Matrix = Matrix.identity(2) + val solvedMatrix: Matrix = a.solve(rhsMatrix) + assert(abs(solvedMatrix.get(0, 0) - 0.6) < eps) + assert(abs(solvedMatrix.get(1, 1) - 0.4) < eps) + + val lowRank: Matrix = matrix([[1, 2], [2, 4]]) + assertEquals(1, lowRank.rank()) + """.trimIndent() + ) + } + + @Test + fun testMatrixBracketIndexingAndSlices() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.matrix + + val m: Matrix = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + + assertEquals(7.0, m[1, 2]) + + val columnSlice: Matrix = m[0..2, 2] + assertEquals([[3.0], [7.0], [11.0]], columnSlice.toList()) + + val topLeft: Matrix = m[0..1, 0..1] + assertEquals([[1.0, 2.0], [5.0, 6.0]], topLeft.toList()) + + val tail: Matrix = m[1.., 1..] + assertEquals([[6.0, 7.0, 8.0], [10.0, 11.0, 12.0]], tail.toList()) + + val rowSlice: Matrix = m[1, 1..2] + assertEquals([[6.0, 7.0]], rowSlice.toList()) + """.trimIndent() + ) + } +} diff --git a/lynglib/src/matrixMultikMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.multik.kt b/lynglib/src/matrixMultikMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.multik.kt new file mode 100644 index 0000000..7624493 --- /dev/null +++ b/lynglib/src/matrixMultikMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.multik.kt @@ -0,0 +1,53 @@ +/* + * 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.matrix + +import org.jetbrains.kotlinx.multik.api.mk +import org.jetbrains.kotlinx.multik.api.ndarray +import org.jetbrains.kotlinx.multik.api.linalg.dot +import org.jetbrains.kotlinx.multik.api.linalg.inv +import org.jetbrains.kotlinx.multik.ndarray.data.get + +internal actual object PlatformMatrixBackend { + actual fun multiply(left: MatrixData, right: MatrixData): MatrixData { + require(left.cols == right.rows) { + "matrix multiplication shape mismatch: ${left.rows}x${left.cols} cannot multiply ${right.rows}x${right.cols}" + } + val leftArray = mk.ndarray(left.values, left.rows, left.cols) + val rightArray = mk.ndarray(right.values, right.rows, right.cols) + val product = leftArray dot rightArray + return MatrixData(left.rows, right.cols, extract(product, left.rows, right.cols)) + } + + actual fun inverse(matrix: MatrixData): MatrixData { + require(matrix.isSquare) { "matrix inverse requires a square matrix, got ${matrix.rows}x${matrix.cols}" } + val source = mk.ndarray(matrix.values, matrix.rows, matrix.cols) + val inverse = mk.linalg.inv(source) + return MatrixData(matrix.rows, matrix.cols, extract(inverse, matrix.rows, matrix.cols)) + } + + private fun extract(array: org.jetbrains.kotlinx.multik.ndarray.data.D2Array, rows: Int, cols: Int): DoubleArray { + val out = DoubleArray(rows * cols) + for (row in 0 until rows) { + for (col in 0 until cols) { + out[row * cols + col] = array[row, col] + } + } + return out + } +} diff --git a/lynglib/src/matrixPureMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.pure.kt b/lynglib/src/matrixPureMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.pure.kt new file mode 100644 index 0000000..33d2381 --- /dev/null +++ b/lynglib/src/matrixPureMain/kotlin/net/sergeych/lyng/matrix/PlatformMatrixBackend.pure.kt @@ -0,0 +1,26 @@ +/* + * 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.matrix + +internal actual object PlatformMatrixBackend { + actual fun multiply(left: MatrixData, right: MatrixData): MatrixData = + PureMatrixAlgorithms.multiply(left, right) + + actual fun inverse(matrix: MatrixData): MatrixData = + PureMatrixAlgorithms.inverse(matrix) +} diff --git a/lynglib/stdlib/lyng/matrix.lyng b/lynglib/stdlib/lyng/matrix.lyng new file mode 100644 index 0000000..5710110 --- /dev/null +++ b/lynglib/stdlib/lyng/matrix.lyng @@ -0,0 +1,157 @@ +package lyng.matrix + +type MatrixScalar = Real | Int + +/** Dense immutable vector backed by double-precision elements. */ +extern class Vector() { + /** Number of elements. */ + val size: Int + + /** Alias to `size`. */ + val length: Int + + /** Convert to a plain list. */ + extern fun toList(): List + + /** Get one element by zero-based index. */ + extern fun get(index: Int): Real + + /** Element-wise addition. */ + extern fun plus(other: Vector): Vector + + /** Element-wise subtraction. */ + extern fun minus(other: Vector): Vector + + /** Scale every element. */ + extern fun mul(other: MatrixScalar): Vector + + /** Divide every element by a scalar. */ + extern fun div(other: MatrixScalar): Vector + + /** Dot product. */ + extern fun dot(other: Vector): Real + + /** Euclidean norm. */ + extern fun norm(): Real + + /** Return a unit-length vector in the same direction. */ + extern fun normalize(): Vector + + /** 3D cross product. Both vectors must have length 3. */ + extern fun cross(other: Vector): Vector + + /** Outer product, returning a matrix of shape `(this.size, other.size)`. */ + extern fun outer(other: Vector): Matrix + + /** Build a vector from numeric values. */ + static extern fun fromList(values: List): Vector + + /** Zero-filled vector. */ + static extern fun zeros(size: Int): Vector +} + +/** + * Dense immutable matrix backed by double-precision elements. + * + * `Matrix` supports arbitrary rectangular geometry (`rows x cols`), matrix arithmetic, + * transpose, determinant, and inversion. + * + * Use this type for numeric linear algebra where contiguous dense storage is appropriate. + */ +extern class Matrix() { + /** Number of rows. */ + val rows: Int + + /** Number of columns. */ + val cols: Int + + /** Two-element shape `[rows, cols]`. */ + val shape: List + + /** Whether `rows == cols`. */ + val isSquare: Bool + + /** Element-wise addition. Shapes must match. */ + extern fun plus(other: Matrix): Matrix + + /** Element-wise subtraction. Shapes must match. */ + extern fun minus(other: Matrix): Matrix + + /** + * Multiply by another matrix, by a vector, or by a scalar. + * + * - `Matrix * Matrix`: matrix product (`A.cols == B.rows`) + * - `Matrix * Vector`: matrix-vector product (`A.cols == x.size`) + * - `Matrix * MatrixScalar`: scale every element + */ + extern fun mul(other: Matrix | Vector | MatrixScalar): Matrix | Vector + + /** Divide every element by a scalar. */ + extern fun div(other: MatrixScalar): Matrix + + /** Matrix transpose. */ + extern fun transpose(): Matrix + + /** Sum of diagonal elements of a square matrix. */ + extern fun trace(): Real + + /** Row rank computed numerically via row-echelon reduction. */ + extern fun rank(): Int + + /** + * Determinant of a square matrix. + * + * Raises an error for non-square matrices. + */ + extern fun determinant(): Real + + /** + * Inverse of a square matrix. + * + * Raises an error for non-square or singular matrices. + */ + extern fun inverse(): Matrix + + /** + * Solve `A * x = rhs`. + * + * - with a `Vector` right-hand side, returns a `Vector` + * - with a `Matrix` right-hand side, returns a `Matrix` + */ + extern fun solve(rhs: Vector | Matrix): Vector | Matrix + + /** + * Bracket indexing by `[row, col]`. + * + * Each selector may be an `Int` or a `Range`: + * - `m[1, 2]` returns one scalar + * - `m[0..2, 2]` returns a `3x1` matrix + * - `m[0..2, 0..2]` returns a sub-matrix + * - `m[1.., 1..]` returns the bottom-right tail + */ + override extern fun getAt(index: List): Real | Matrix + + /** Get one element by zero-based row and column indices. */ + extern fun get(row: Int, col: Int): Real + + /** Return one row as a `List`. */ + extern fun row(index: Int): List + + /** Return one column as a `List`. */ + extern fun column(index: Int): List + + /** Convert to nested row lists. */ + extern fun toList(): List> + + /** Build a matrix from nested row lists. All rows must have the same length. */ + static extern fun fromRows(rows: List>): Matrix + + /** Create a zero-filled matrix. */ + static extern fun zeros(rows: Int, cols: Int): Matrix + + /** Create an identity matrix of shape `(size, size)`. */ + static extern fun identity(size: Int): Matrix +} + +fun vector(values: List): Vector = Vector.fromList(values) +fun matrix(rows: List>): Matrix = Matrix.fromRows(rows) diff --git a/lyngweb/build.gradle.kts b/lyngweb/build.gradle.kts index 08db5f4..8ed5c23 100644 --- a/lyngweb/build.gradle.kts +++ b/lyngweb/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -21,7 +21,7 @@ plugins { alias(libs.plugins.kotlinMultiplatform) - id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" + id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" id("org.jetbrains.compose") version "1.9.3" `maven-publish` } diff --git a/site/build.gradle.kts b/site/build.gradle.kts index 0bf3dc9..86ffbb4 100644 --- a/site/build.gradle.kts +++ b/site/build.gradle.kts @@ -21,8 +21,8 @@ plugins { alias(libs.plugins.kotlinMultiplatform) - // Compose compiler plugin for Kotlin 2.2.21 (matches version catalog) - id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" + // Compose compiler plugin aligned with the project Kotlin version. + id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" // Compose Multiplatform plugin for convenient dependencies (compose.html.core, etc.) id("org.jetbrains.compose") version "1.9.3" } diff --git a/tmp/test.lyng b/tmp/test.lyng new file mode 100644 index 0000000..3e2cdce --- /dev/null +++ b/tmp/test.lyng @@ -0,0 +1,18 @@ +#!/binenv lyng + +import lyng.decimal + +val x = "0.1".d +val y = 0.1 +var s0 = 0.d +var s1 = 0.0 + +for( i in 1..100 ) { + s0 += x + s1 += y +} + +println("$s0") +println("$s1") + +println(":: ${sin(3.14)}")