Added Vectors and Matrics operations, slicing, docs.

This commit is contained in:
Sergey Chernov 2026-03-28 22:25:13 +03:00
parent a72991d1b7
commit 83d8c8b71f
31 changed files with 1972 additions and 41 deletions

View File

@ -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)

View File

@ -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

View File

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

192
docs/Matrix.md Normal file
View File

@ -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.

View File

@ -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

View File

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

View File

@ -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`

View File

@ -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>): Int
override fun putAt(index: List<Int>, value: Int): void
}
""".trimIndent())
moduleScope.bind("Grid") {
init { _ -> data = IntArray(4) }
addFun("getAt") {
val index = args.requiredArg<ObjList>(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<ObjList>(0)
val value = args.requiredArg<ObjInt>(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.

View File

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

View File

@ -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

View File

@ -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.

View File

@ -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" }

View File

@ -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<ConsoleView, ToolWindow> {
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
)
}

View File

@ -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

View File

@ -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>(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<ListEntry> {
// it should be called after Token.Type.LBRACKET is consumed
val entries = mutableListOf<ListEntry>()

View File

@ -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))
}

View File

@ -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<Double>> =
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}"
}
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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<Double> = 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
}
}

View File

@ -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())
}

View File

@ -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<suspend (ScopeFacade, ObjInstance) -> 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<ObjInt>(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<ObjInt>(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<suspend (ScopeFacade, ObjInstance) -> 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<ObjInt>(0).value.toInt(),
requiredArg<ObjInt>(1).value.toInt()
)
)
}
matrixClass.addFn("getAt") {
resolveMatrixIndex(matrixClass, matrixOf(thisObj), args.firstAndOnly(), thisObj)
}
matrixClass.addFn("row") {
doubleArrayToObjList(matrixOf(thisObj).rowValues(requiredArg<ObjInt>(0).value.toInt()))
}
matrixClass.addFn("column") {
doubleArrayToObjList(matrixOf(thisObj).columnValues(requiredArg<ObjInt>(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<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.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)
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<Double>()
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<ObjList>(scope, "toList")
} else {
scope.raiseClassCastError(message)
}
}
private fun doubleArrayToObjList(values: DoubleArray): ObjList =
ObjList(values.map { ObjReal.of(it) }.toMutableList())
}

View File

@ -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())
}
}

View File

@ -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()
)
}
}

View File

@ -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<Double>, 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
}
}

View File

@ -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)
}

View File

@ -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<Real>
/** 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<MatrixScalar>): 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<Int>
/** 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<Int | Range>): 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<Real>`. */
extern fun row(index: Int): List<Real>
/** Return one column as a `List<Real>`. */
extern fun column(index: Int): List<Real>
/** Convert to nested row lists. */
extern fun toList(): List<List<Real>>
/** Build a matrix from nested row lists. All rows must have the same length. */
static extern fun fromRows(rows: List<List<MatrixScalar>>): 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<MatrixScalar>): Vector = Vector.fromList(values)
fun matrix(rows: List<List<MatrixScalar>>): Matrix = Matrix.fromRows(rows)

View File

@ -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`
}

View File

@ -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"
}

18
tmp/test.lyng Normal file
View File

@ -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)}")