From cd007050a8392c3d84f5699f58aa41f08bdf55ed Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 28 Mar 2026 17:44:43 +0300 Subject: [PATCH] added complext number support --- docs/Complex.md | 82 +++++++++ docs/ai_stdlib_reference.md | 2 + lynglib/build.gradle.kts | 39 +++-- .../kotlin/net/sergeych/lyng/Script.kt | 83 +++++++++ .../net/sergeych/lyng/ComplexModuleTest.kt | 76 +++++++++ lynglib/stdlib/lyng/complex.lyng | 158 ++++++++++++++++++ 6 files changed, 424 insertions(+), 16 deletions(-) create mode 100644 docs/Complex.md create mode 100644 lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt create mode 100644 lynglib/stdlib/lyng/complex.lyng diff --git a/docs/Complex.md b/docs/Complex.md new file mode 100644 index 0000000..7a6d7c8 --- /dev/null +++ b/docs/Complex.md @@ -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`. + +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` can be revisited on firmer ground. diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index 93974aa..2dc7dff 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -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` diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 0d68792..4751f04 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -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) - val sb = StringBuilder() - sb.append("package ").append(targetPkg).append("\n\n") - sb.append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n") - sb.append("internal val rootLyng = \"") - sb.append(body) - sb.append("\"\n") + 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" + } - 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()) + } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 2d5b973..7def164 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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", diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt new file mode 100644 index 0000000..4d7e1ab --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/ComplexModuleTest.kt @@ -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() + ) + } +} diff --git a/lynglib/stdlib/lyng/complex.lyng b/lynglib/stdlib/lyng/complex.lyng new file mode 100644 index 0000000..e5afae8 --- /dev/null +++ b/lynglib/stdlib/lyng/complex.lyng @@ -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 } +)