added complext number support

This commit is contained in:
Sergey Chernov 2026-03-28 17:44:43 +03:00
parent 418b1ae2b6
commit cd007050a8
6 changed files with 424 additions and 16 deletions

82
docs/Complex.md Normal file
View File

@ -0,0 +1,82 @@
# Complex Numbers (`lyng.complex`)
`lyng.complex` adds a pure-Lyng `Complex` type backed by `Real` components.
Import it when you want ordinary complex arithmetic:
```lyng
import lyng.complex
```
## Construction
Use any of these:
```lyng
import lyng.complex
val a = Complex(1.0, 2.0)
val b = complex(1.0, 2.0)
val c = 2.i
val d = 3.re
assertEquals(Complex(1.0, 2.0), 1 + 2.i)
```
Convenience extensions:
- `Int.re`, `Real.re`: embed a real value into the complex plane
- `Int.i`, `Real.i`: create a pure imaginary value
- `cis(angle)`: shorthand for `cos(angle) + i sin(angle)`
## Core Operations
`Complex` supports:
- `+`
- `-`
- `*`
- `/`
- unary `-`
- `conjugate`
- `magnitude`
- `phase`
Mixed arithmetic with `Int` and `Real` is enabled through `lyng.operators`, so both sides work naturally:
```lyng
import lyng.complex
assertEquals(Complex(1.0, 2.0), 1 + 2.i)
assertEquals(Complex(1.5, 2.0), 1.5 + 2.i)
assertEquals(Complex(2.0, 2.0), 2.i + 2)
```
Mixed equality with built-in numeric types is intentionally not promised yet. Keep equality checks in the `Complex` domain for now.
## Transcendental Functions
For now, use member-style calls:
```lyng
import lyng.complex
val z = 1 + π.i
val w = z.exp()
val s = z.sin()
val r = z.sqrt()
```
This is deliberate. Lyng already has built-in top-level real-valued functions such as `exp(x)` and `sin(x)`, and imported modules do not currently replace those root bindings. So plain `exp(z)` is not yet the right extension mechanism for complex math.
## Design Scope
This module intentionally uses `Complex` with `Real` parts, not `Complex<T>`.
Reasons:
- the existing math runtime is `Real`-centric
- the operator interop registry works with concrete runtime classes
- transcendental functions (`exp`, `sin`, `ln`, `sqrt`) are defined over the `Real` math backend here
If Lyng later gets a more general numeric-trait or callable-overload registry, a generic algebraic `Complex<T>` can be revisited on firmer ground.

View File

@ -56,6 +56,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
## 5. Additional Built-in Modules (import explicitly) ## 5. Additional Built-in Modules (import explicitly)
- `import lyng.observable` - `import lyng.observable`
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`. - `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.buffer` - `import lyng.buffer`
- `Buffer`, `MutableBuffer`. - `Buffer`, `MutableBuffer`.
- `import lyng.serialization` - `import lyng.serialization`

View File

@ -149,6 +149,9 @@ abstract class GenerateLyngStdlib : DefaultTask() {
val outBase = outputDir.get().asFile val outBase = outputDir.get().asFile
val targetDir = outBase.resolve(pkgPath) val targetDir = outBase.resolve(pkgPath)
targetDir.mkdirs() targetDir.mkdirs()
targetDir.listFiles()
?.filter { it.isFile && it.name.endsWith(".generated.kt") }
?.forEach { it.delete() }
val srcDir = sourceDir.get().asFile val srcDir = sourceDir.get().asFile
val files = srcDir.walkTopDown() val files = srcDir.walkTopDown()
@ -156,34 +159,38 @@ abstract class GenerateLyngStdlib : DefaultTask() {
.sortedBy { it.name } .sortedBy { it.name }
.toList() .toList()
val content = if (files.isEmpty()) "" else buildString {
files.forEachIndexed { idx, f ->
val text = f.readText()
if (idx > 0) append("\n\n")
append(text)
}
}
fun escapeForQuoted(s: String): String = buildString { fun escapeForQuoted(s: String): String = buildString {
for (ch in s) when (ch) { for (ch in s) when (ch) {
'\\' -> append("\\\\") '\\' -> append("\\\\")
'"' -> append("\\\"") '"' -> append("\\\"")
'$' -> append("\\$")
'\n' -> append("\\n") '\n' -> append("\\n")
'\r' -> {} '\r' -> {}
'\t' -> append("\\t") '\t' -> append("\\t")
else -> append(ch) else -> append(ch)
} }
} }
val body = escapeForQuoted(content)
val sb = StringBuilder() fun constantName(baseName: String): String {
sb.append("package ").append(targetPkg).append("\n\n") val parts = baseName.split(Regex("[^A-Za-z0-9]+")).filter { it.isNotEmpty() }
sb.append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n") if (parts.isEmpty()) return "moduleLyng"
sb.append("internal val rootLyng = \"") val head = parts.first().replaceFirstChar { it.lowercase() }
sb.append(body) val tail = parts.drop(1).joinToString("") { part ->
sb.append("\"\n") part.replaceFirstChar { it.uppercase() }
}
return "${head}${tail}Lyng"
}
targetDir.resolve("root_lyng.generated.kt").writeText(sb.toString()) for (file in files) {
val body = escapeForQuoted(file.readText())
val sb = StringBuilder()
sb.append("package ").append(targetPkg).append("\n\n")
sb.append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n")
sb.append("internal val ").append(constantName(file.nameWithoutExtension)).append(" = \"")
sb.append(body)
sb.append("\"\n")
targetDir.resolve("${file.nameWithoutExtension}_lyng.generated.kt").writeText(sb.toString())
}
} }
} }

View File

@ -27,6 +27,7 @@ import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.stdlib_included.complexLyng
import net.sergeych.lyng.stdlib_included.decimalLyng import net.sergeych.lyng.stdlib_included.decimalLyng
import net.sergeych.lyng.stdlib_included.observableLyng import net.sergeych.lyng.stdlib_included.observableLyng
import net.sergeych.lyng.stdlib_included.operatorsLyng import net.sergeych.lyng.stdlib_included.operatorsLyng
@ -615,12 +616,91 @@ class Script(
type = type("lyng.Real") type = type("lyng.Real")
) )
getOrCreateNamespace("Math").apply { getOrCreateNamespace("Math").apply {
fun ensureFn(name: String, fn: suspend ScopeFacade.() -> Obj) {
if (members.containsKey(name)) return
addFn(name, code = fn)
}
addConstDoc( addConstDoc(
name = "PI", name = "PI",
value = pi, value = pi,
doc = "The mathematical constant pi (π) in the Math namespace.", doc = "The mathematical constant pi (π) in the Math namespace.",
type = type("lyng.Real") type = type("lyng.Real")
) )
ensureFn("floor") {
val x = args.firstAndOnly()
if (x is ObjInt) x else ObjReal(floor(x.toDouble()))
}
ensureFn("ceil") {
val x = args.firstAndOnly()
if (x is ObjInt) x else ObjReal(ceil(x.toDouble()))
}
ensureFn("round") {
val x = args.firstAndOnly()
if (x is ObjInt) x else ObjReal(round(x.toDouble()))
}
ensureFn("sin") {
ObjReal(sin(args.firstAndOnly().toDouble()))
}
ensureFn("cos") {
ObjReal(cos(args.firstAndOnly().toDouble()))
}
ensureFn("tan") {
ObjReal(tan(args.firstAndOnly().toDouble()))
}
ensureFn("asin") {
ObjReal(asin(args.firstAndOnly().toDouble()))
}
ensureFn("acos") {
ObjReal(acos(args.firstAndOnly().toDouble()))
}
ensureFn("atan") {
ObjReal(atan(args.firstAndOnly().toDouble()))
}
ensureFn("sinh") {
ObjReal(sinh(args.firstAndOnly().toDouble()))
}
ensureFn("cosh") {
ObjReal(cosh(args.firstAndOnly().toDouble()))
}
ensureFn("tanh") {
ObjReal(tanh(args.firstAndOnly().toDouble()))
}
ensureFn("asinh") {
ObjReal(asinh(args.firstAndOnly().toDouble()))
}
ensureFn("acosh") {
ObjReal(acosh(args.firstAndOnly().toDouble()))
}
ensureFn("atanh") {
ObjReal(atanh(args.firstAndOnly().toDouble()))
}
ensureFn("exp") {
ObjReal(exp(args.firstAndOnly().toDouble()))
}
ensureFn("ln") {
ObjReal(ln(args.firstAndOnly().toDouble()))
}
ensureFn("log10") {
ObjReal(log10(args.firstAndOnly().toDouble()))
}
ensureFn("log2") {
ObjReal(log2(args.firstAndOnly().toDouble()))
}
ensureFn("pow") {
requireExactCount(2)
ObjReal(
(args[0].toDouble()).pow(args[1].toDouble())
)
}
ensureFn("sqrt") {
ObjReal(
sqrt(args.firstAndOnly().toDouble())
)
}
ensureFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
}
} }
} }
@ -759,6 +839,9 @@ class Script(
module.eval(Source("lyng.decimal", decimalLyng)) module.eval(Source("lyng.decimal", decimalLyng))
ObjBigDecimalSupport.bindTo(module) ObjBigDecimalSupport.bindTo(module)
} }
addPackage("lyng.complex") { module ->
module.eval(Source("lyng.complex", complexLyng))
}
addPackage("lyng.buffer") { addPackage("lyng.buffer") {
it.addConstDoc( it.addConstDoc(
name = "Buffer", name = "Buffer",

View File

@ -0,0 +1,76 @@
/*
* 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 ComplexModuleTest {
@Test
fun testComplexArithmeticAndInterop() = runTest {
val scope = Script.newScope()
scope.eval(
"""
import lyng.complex
assertEquals(Complex(0.0, 2.0), 2.i)
assertEquals(Complex(2.0, 0.0), 2.re)
assertEquals(Complex(1.0, 2.0), 1 + 2.i)
assertEquals(Complex(1.5, 2.0), 1.5 + 2.i)
assertEquals(Complex(3.0, 2.0), 1 + Complex(2.0, 2.0))
val product: Complex = Complex(1.0, 2.0) * Complex(3.0, -1.0)
assertEquals(5.0, product.re)
assertEquals(5.0, product.im)
val quotient: Complex = Complex(5.0, 5.0) / Complex(3.0, -1.0)
assertEquals(1.0, quotient.re)
assertEquals(2.0, quotient.im)
assertEquals(Complex(1.0, -2.0), Complex(1.0, 2.0).conjugate)
""".trimIndent()
)
}
@Test
fun testComplexMemberMathFunctions() = runTest {
val scope = Script.newScope()
scope.eval(
"""
import lyng.complex
val eps = 1e-9
val eipi = Complex.imaginary(π).exp()
assert(abs(eipi.re + 1.0) < eps)
assert(abs(eipi.im) < eps)
val iy: Complex = 2.i
val sinIy = iy.sin()
assert(abs(sinIy.re) < eps)
assert(abs(sinIy.im - sinh(2.0)) < eps)
val minusOne: Complex = (-1).re
val root = minusOne.sqrt()
assert(abs(root.re) < eps)
assert(abs(root.im - 1.0) < eps)
val unitLeft: Complex = cis(π)
assert(abs(unitLeft.re + 1.0) < eps)
assert(abs(unitLeft.im) < eps)
""".trimIndent()
)
}
}

View File

@ -0,0 +1,158 @@
package lyng.complex
import lyng.operators
/*
Complex number backed by `Real` components.
This module intentionally keeps the storage model simple:
- `re`: real part
- `im`: imaginary part
The algebra is implemented in pure Lyng and interoperates with `Int` and `Real`
through `lyng.operators`, so expressions like `1 + 2.i` and `0.5 * (1 + 2.i)` work.
For transcendental functions, use the member-style API for now:
- `z.exp()`
- `z.ln()`
- `z.sin()`
- `z.cos()`
- `z.tan()`
- `z.sqrt()`
This avoids collisions with the built-in real-valued top-level math functions
such as `exp(x)` and `sin(x)`.
*/
class Complex(val real: Real, val imag: Real = 0.0) {
val re get() = real
val im get() = imag
fun plus(other: Complex): Complex {
val ar = real
val ai = imag
val br = other.real
val bi = other.imag
val realPart = ar + br
val imagPart = ai + bi
Complex(realPart, imagPart)
}
fun minus(other: Complex): Complex {
val ar = real
val ai = imag
val br = other.real
val bi = other.imag
val realPart = ar - br
val imagPart = ai - bi
Complex(realPart, imagPart)
}
fun mul(other: Complex): Complex {
val ar = real
val ai = imag
val br = other.real
val bi = other.imag
val realPart = ar * br - ai * bi
val imagPart = ar * bi + ai * br
Complex(realPart, imagPart)
}
fun div(other: Complex): Complex {
val ar = real
val ai = imag
val br = other.real
val bi = other.imag
val denominator = br * br + bi * bi
val realPart = (ar * br + ai * bi) / denominator
val imagPart = (ai * br - ar * bi) / denominator
Complex(realPart, imagPart)
}
fun negate(): Complex = Complex(-real, -imag)
val conjugate get() = Complex(real, -imag)
val magnitude2 get() = real * real + imag * imag
val magnitude get() = Math.sqrt(magnitude2)
val phase: Real
get() = if (real > 0.0) {
Math.atan(imag / real)
} else if (real < 0.0 && imag >= 0.0) {
Math.atan(imag / real) + π
} else if (real < 0.0 && imag < 0.0) {
Math.atan(imag / real) - π
} else if (real == 0.0 && imag > 0.0) {
π / 2.0
} else if (real == 0.0 && imag < 0.0) {
-π / 2.0
} else {
0.0
}
fun exp(): Complex {
val scale = Math.exp(real)
Complex(scale * Math.cos(imag), scale * Math.sin(imag))
}
fun ln(): Complex = Complex(Math.ln(magnitude), phase)
fun sin(): Complex =
Complex(
Math.sin(real) * Math.cosh(imag),
Math.cos(real) * Math.sinh(imag)
)
fun cos(): Complex =
Complex(
Math.cos(real) * Math.cosh(imag),
-Math.sin(real) * Math.sinh(imag)
)
fun tan(): Complex = sin() / cos()
fun sqrt(): Complex = if (imag == 0.0 && real >= 0.0) {
Complex(Math.sqrt(real), 0.0)
} else {
val radius = magnitude
val realPart = Math.sqrt((radius + real) / 2.0)
val imagPart = Math.sqrt((radius - real) / 2.0)
Complex(realPart, if (imag < 0.0) -imagPart else imagPart)
}
override fun toString() =
real.toString() +
(if (imag < 0.0) imag.toString() else "+" + imag.toString()) +
"i"
static fun fromInt(value: Int): Complex = Complex(value + 0.0, 0.0)
static fun fromReal(value: Real): Complex = Complex(value, 0.0)
static fun imaginary(value: Real): Complex = Complex(0.0, value)
static fun fromPolar(radius: Real, angle: Real): Complex =
Complex(radius * Math.cos(angle), radius * Math.sin(angle))
}
fun complex(re: Real, im: Real = 0.0): Complex = Complex(re, im)
fun cis(angle: Real): Complex = Complex.fromPolar(1.0, angle)
val Int.re: Complex get() = Complex.fromInt(this)
val Real.re: Complex get() = Complex.fromReal(this)
val Int.i: Complex get() = Complex.imaginary(this + 0.0)
val Real.i: Complex get() = Complex.imaginary(this)
OperatorInterop.register(
Int,
Complex,
Complex,
[BinaryOperator.Plus, BinaryOperator.Minus, BinaryOperator.Mul, BinaryOperator.Div],
{ x: Int -> Complex.fromInt(x) },
{ x: Complex -> x }
)
OperatorInterop.register(
Real,
Complex,
Complex,
[BinaryOperator.Plus, BinaryOperator.Minus, BinaryOperator.Mul, BinaryOperator.Div],
{ x: Real -> Complex.fromReal(x) },
{ x: Complex -> x }
)