added complext number support
This commit is contained in:
parent
418b1ae2b6
commit
cd007050a8
82
docs/Complex.md
Normal file
82
docs/Complex.md
Normal 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.
|
||||
@ -56,6 +56,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
## 5. Additional Built-in Modules (import explicitly)
|
||||
- `import lyng.observable`
|
||||
- `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`
|
||||
- `Buffer`, `MutableBuffer`.
|
||||
- `import lyng.serialization`
|
||||
|
||||
@ -149,6 +149,9 @@ abstract class GenerateLyngStdlib : DefaultTask() {
|
||||
val outBase = outputDir.get().asFile
|
||||
val targetDir = outBase.resolve(pkgPath)
|
||||
targetDir.mkdirs()
|
||||
targetDir.listFiles()
|
||||
?.filter { it.isFile && it.name.endsWith(".generated.kt") }
|
||||
?.forEach { it.delete() }
|
||||
|
||||
val srcDir = sourceDir.get().asFile
|
||||
val files = srcDir.walkTopDown()
|
||||
@ -156,34 +159,38 @@ abstract class GenerateLyngStdlib : DefaultTask() {
|
||||
.sortedBy { it.name }
|
||||
.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 {
|
||||
for (ch in s) when (ch) {
|
||||
'\\' -> append("\\\\")
|
||||
'"' -> append("\\\"")
|
||||
'$' -> append("\\$")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> {}
|
||||
'\t' -> append("\\t")
|
||||
else -> append(ch)
|
||||
}
|
||||
}
|
||||
val body = escapeForQuoted(content)
|
||||
|
||||
fun constantName(baseName: String): String {
|
||||
val parts = baseName.split(Regex("[^A-Za-z0-9]+")).filter { it.isNotEmpty() }
|
||||
if (parts.isEmpty()) return "moduleLyng"
|
||||
val head = parts.first().replaceFirstChar { it.lowercase() }
|
||||
val tail = parts.drop(1).joinToString("") { part ->
|
||||
part.replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
return "${head}${tail}Lyng"
|
||||
}
|
||||
|
||||
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 rootLyng = \"")
|
||||
sb.append("internal val ").append(constantName(file.nameWithoutExtension)).append(" = \"")
|
||||
sb.append(body)
|
||||
sb.append("\"\n")
|
||||
|
||||
targetDir.resolve("root_lyng.generated.kt").writeText(sb.toString())
|
||||
targetDir.resolve("${file.nameWithoutExtension}_lyng.generated.kt").writeText(sb.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import net.sergeych.lyng.bytecode.CmdVm
|
||||
import net.sergeych.lyng.miniast.*
|
||||
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.observableLyng
|
||||
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
||||
@ -615,12 +616,91 @@ class Script(
|
||||
type = type("lyng.Real")
|
||||
)
|
||||
getOrCreateNamespace("Math").apply {
|
||||
fun ensureFn(name: String, fn: suspend ScopeFacade.() -> Obj) {
|
||||
if (members.containsKey(name)) return
|
||||
addFn(name, code = fn)
|
||||
}
|
||||
addConstDoc(
|
||||
name = "PI",
|
||||
value = pi,
|
||||
doc = "The mathematical constant pi (π) in the Math namespace.",
|
||||
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))
|
||||
ObjBigDecimalSupport.bindTo(module)
|
||||
}
|
||||
addPackage("lyng.complex") { module ->
|
||||
module.eval(Source("lyng.complex", complexLyng))
|
||||
}
|
||||
addPackage("lyng.buffer") {
|
||||
it.addConstDoc(
|
||||
name = "Buffer",
|
||||
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
158
lynglib/stdlib/lyng/complex.lyng
Normal file
158
lynglib/stdlib/lyng/complex.lyng
Normal 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 }
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user