Compare commits

..

No commits in common. "cd007050a8392c3d84f5699f58aa41f08bdf55ed" and "86e8b2e2bcfbf9cb542844a39cbc99ddee4d038f" have entirely different histories.

15 changed files with 70 additions and 720 deletions

1
.gitignore vendored
View File

@ -27,4 +27,3 @@ debug.log
/compile_jvm_output.txt
/compile_metadata_output.txt
test_output*.txt
/site/src/version-template/lyng-version.js

View File

@ -1,82 +0,0 @@
# 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,8 +56,6 @@ 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`

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.5.3-SNAPSHOT"
version = "1.5.2"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@ -149,9 +149,6 @@ 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()
@ -159,38 +156,34 @@ 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"
}
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")
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())
}
targetDir.resolve("root_lyng.generated.kt").writeText(sb.toString())
}
}

View File

@ -8253,7 +8253,7 @@ class Compiler(
?: context.parent?.get(localName)
?: context.get(localName)
?: continue
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
context.resolve(record, localName)
} else {
record.value

View File

@ -167,19 +167,6 @@ class RecordSlotRef(
}
}
suspend fun read(scope: Scope, name: String?): Obj {
val direct = record.value
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) {
return scope.resolve(record, name)
}
return when (direct) {
is FrameSlotRef -> direct.read()
is RecordSlotRef -> direct.read(scope, name)
is ScopeSlotRef -> direct.read()
else -> direct
}
}
override suspend fun callOn(scope: Scope): Obj {
val resolved = read()
if (resolved === this) {
@ -206,18 +193,4 @@ class RecordSlotRef(
record.value = value
}
}
suspend fun write(scope: Scope, name: String?, value: Obj): Boolean {
val direct = record.value
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) {
scope.assign(record, name, value)
return true
}
when (direct) {
is ScopeSlotRef -> direct.write(value)
is RecordSlotRef -> if (direct.write(scope, name, value)) return true else direct.write(value)
else -> record.value = value
}
return false
}
}

View File

@ -27,7 +27,6 @@ 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
@ -616,91 +615,12 @@ 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)
}
}
}
@ -839,9 +759,6 @@ 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",

View File

@ -2396,8 +2396,7 @@ class BytecodeCompiler(
builder.emit(Opcode.THROW, posId, msgSlot)
return value
}
val slot = resolveCapturedOwnerScopeSlot(localTarget)
?: resolveSlot(localTarget)
val slot = resolveSlot(localTarget)
?: resolveAssignableSlotByName(localTarget.name)?.first
?: return null
if (slot < scopeSlotCount && value.type != SlotType.UNKNOWN) {
@ -2703,7 +2702,7 @@ class BytecodeCompiler(
return CompiledValue(result, SlotType.OBJ)
}
if (localTarget.isDelegated) return compileEvalRef(ref)
val slot = resolveCapturedOwnerScopeSlot(localTarget) ?: resolveSlot(localTarget) ?: return null
val slot = resolveSlot(localTarget) ?: return null
val targetType = slotTypes[slot] ?: SlotType.OBJ
if (!localTarget.isMutable) {
if (targetType != SlotType.OBJ && targetType != SlotType.UNKNOWN) return compileEvalRef(ref)
@ -7631,13 +7630,6 @@ class BytecodeCompiler(
return scopeSlotMap[scopeKey]
}
private fun resolveCapturedOwnerScopeSlot(ref: LocalSlotRef): Int? {
val ownerScopeId = ref.captureOwnerScopeId ?: return null
val ownerSlot = ref.captureOwnerSlot ?: return null
val key = ScopeSlotKey(ownerScopeId, ownerSlot)
return scopeSlotMap[key]
}
private fun updateSlotType(slot: Int, type: SlotType) {
if (forcedObjSlots.contains(slot) && type != SlotType.OBJ) return
if (type == SlotType.UNKNOWN) {

View File

@ -50,7 +50,7 @@ class BytecodeStatement private constructor(
?: scope.parent?.get(name)
?: scope.get(name)
?: continue
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is net.sergeych.lyng.obj.ObjProperty) {
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
scope.resolve(record, name)
} else {
record.value

View File

@ -85,9 +85,6 @@ class CmdNop : Cmd() {
class CmdMoveObj(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val value = frame.slotToObj(src)
if (frame.writeThroughPropertyLikeSlot(dst, value)) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) {
frame.setObjUnchecked(dst, value)
} else {
@ -100,9 +97,6 @@ class CmdMoveObj(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdMoveInt(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val value = frame.getInt(src)
if (frame.writeThroughPropertyLikeSlot(dst, ObjInt.of(value))) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) {
frame.setIntUnchecked(dst, value)
} else {
@ -123,9 +117,6 @@ class CmdMoveIntLocal(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdMoveReal(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val value = frame.getReal(src)
if (frame.writeThroughPropertyLikeSlot(dst, ObjReal.of(value))) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) {
frame.setRealUnchecked(dst, value)
} else {
@ -146,9 +137,6 @@ class CmdMoveRealLocal(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdMoveBool(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val value = frame.getBool(src)
if (frame.writeThroughPropertyLikeSlot(dst, if (value) ObjTrue else ObjFalse)) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) {
frame.setBoolUnchecked(dst, value)
} else {
@ -2607,7 +2595,7 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<Stri
}
val scoped = frame.scope.chainLookupIgnoreClosure(name, followClosure = true) ?: frame.scope.get(name)
if (scoped != null) {
records += scoped
records += ObjRecord(RecordSlotRef(scoped), isMutable = scoped.isMutable)
continue
}
frame.ensureScope().raiseSymbolNotFound("capture $name not found")
@ -2870,7 +2858,7 @@ class CmdDeclInstanceProperty(internal val constId: Int, internal val slot: Int)
val decl = frame.fn.constants[constId] as? BytecodeConst.InstancePropertyDecl
?: error("DECL_INSTANCE_PROPERTY expects InstancePropertyDecl at $constId")
val scope = frame.ensureScope()
val prop = frame.storedSlotObj(slot)
val prop = frame.slotToObj(slot)
scope.addItem(
decl.name,
decl.isMutable,
@ -3736,7 +3724,7 @@ class BytecodeLambdaCallable(
?: context.parent?.get(name)
?: context.get(name)
?: continue
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
context.resolve(record, name)
} else {
record.value
@ -3829,29 +3817,7 @@ class CmdFrame(
}
internal fun getLocalSlotTypeCode(localIndex: Int): Byte = frame.getSlotTypeCode(localIndex)
internal fun readLocalObj(localIndex: Int): Obj {
return when (frame.getSlotTypeCode(localIndex)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(localIndex))
SlotType.REAL.code -> ObjReal.of(frame.getReal(localIndex))
SlotType.BOOL.code -> if (frame.getBool(localIndex)) ObjTrue else ObjFalse
SlotType.OBJ.code -> {
val obj = frame.getObj(localIndex)
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read()
else -> obj
}
}
else -> {
val obj = frame.getObj(localIndex)
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read()
else -> obj
}
}
}
}
internal fun readLocalObj(localIndex: Int): Obj = localSlotToObj(localIndex)
internal fun isFastLocalSlot(slot: Int): Boolean {
if (slot < fn.scopeSlotCount) return false
val localIndex = slot - fn.scopeSlotCount
@ -4296,7 +4262,7 @@ class CmdFrame(
return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot)
} else {
readLocalSlotValue(slot - fn.scopeSlotCount)
localSlotToObj(slot - fn.scopeSlotCount)
}
}
@ -4339,35 +4305,6 @@ class CmdFrame(
}
}
suspend fun writeThroughPropertyLikeSlot(slot: Int, value: Obj): Boolean {
if (slot < fn.scopeSlotCount) {
val target = scopeTarget(slot)
val index = ensureScopeSlot(target, slot)
val name = fn.scopeSlotNames.getOrNull(slot)
val record = resolveScopeSlotRecordForWrite(target, index, name)
if (name != null && record != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) {
target.assign(record, name, value)
return true
}
return false
}
val localIndex = slot - fn.scopeSlotCount
val name = fn.localSlotNames.getOrNull(localIndex) ?: return false
val isCapture = fn.localSlotCaptures.getOrNull(localIndex) == true
val raw = frame.getRawObj(localIndex)
if (raw is RecordSlotRef) {
if (raw.write(scope, name, value)) return true
return false
}
if (!isCapture && raw !== ObjUnset && raw !is ObjProperty) return false
val record = scope.parent?.get(name) ?: scope.get(name) ?: return false
if (record.type != ObjRecord.Type.Delegated && record.type != ObjRecord.Type.Property && record.value !is ObjProperty) {
return false
}
scope.assign(record, name, value)
return true
}
suspend fun getInt(slot: Int): Long {
return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toLong()
@ -4377,7 +4314,14 @@ class CmdFrame(
SlotType.INT.code -> frame.getInt(local)
SlotType.REAL.code -> frame.getReal(local).toLong()
SlotType.BOOL.code -> if (frame.getBool(local)) 1L else 0L
SlotType.OBJ.code -> readLocalSlotValue(local).toLong()
SlotType.OBJ.code -> {
val obj = frame.getObj(local)
when (obj) {
is FrameSlotRef -> obj.read().toLong()
is RecordSlotRef -> obj.read().toLong()
else -> obj.toLong()
}
}
else -> 0L
}
}
@ -4457,7 +4401,14 @@ class CmdFrame(
SlotType.REAL.code -> frame.getReal(local)
SlotType.INT.code -> frame.getInt(local).toDouble()
SlotType.BOOL.code -> if (frame.getBool(local)) 1.0 else 0.0
SlotType.OBJ.code -> readLocalSlotValue(local).toDouble()
SlotType.OBJ.code -> {
val obj = frame.getObj(local)
when (obj) {
is FrameSlotRef -> obj.read().toDouble()
is RecordSlotRef -> obj.read().toDouble()
else -> obj.toDouble()
}
}
else -> 0.0
}
}
@ -4526,7 +4477,14 @@ class CmdFrame(
SlotType.BOOL.code -> frame.getBool(local)
SlotType.INT.code -> frame.getInt(local) != 0L
SlotType.REAL.code -> frame.getReal(local) != 0.0
SlotType.OBJ.code -> readLocalSlotValue(local).toBool()
SlotType.OBJ.code -> {
val obj = frame.getObj(local)
when (obj) {
is FrameSlotRef -> obj.read().toBool()
is RecordSlotRef -> obj.read().toBool()
else -> obj.toBool()
}
}
else -> false
}
}
@ -4638,42 +4596,21 @@ class CmdFrame(
}
val local = slot - fn.scopeSlotCount
if (fn.localSlotCaptures.getOrNull(local) == true) {
return readLocalSlotValue(local)
return localSlotToObj(local)
}
return when (frame.getSlotTypeCode(local)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(local))
SlotType.REAL.code -> ObjReal.of(frame.getReal(local))
SlotType.BOOL.code -> if (frame.getBool(local)) ObjTrue else ObjFalse
SlotType.OBJ.code -> readLocalSlotValue(local)
else -> readLocalSlotValue(local)
}
}
fun storedSlotObj(slot: Int): Obj {
if (slot < fn.scopeSlotCount) {
val target = scopeTarget(slot)
val index = ensureScopeSlot(target, slot)
val record = target.getSlotRecord(index)
return when (val direct = record.value) {
is FrameSlotRef -> direct.read()
is RecordSlotRef -> direct.read()
is ScopeSlotRef -> direct.read()
else -> direct
SlotType.OBJ.code -> {
val obj = frame.getObj(local)
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read()
else -> obj
}
}
}
val local = slot - fn.scopeSlotCount
return when (frame.getSlotTypeCode(local)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(local))
SlotType.REAL.code -> ObjReal.of(frame.getReal(local))
SlotType.BOOL.code -> if (frame.getBool(local)) ObjTrue else ObjFalse
SlotType.OBJ.code, SlotType.UNKNOWN.code -> when (val raw = frame.getRawObj(local)) {
is FrameSlotRef -> raw.read()
is RecordSlotRef -> raw.read()
is ScopeSlotRef -> raw.read()
null -> ObjNull
else -> raw
}
else -> frame.getRawObj(local) ?: ObjNull
else -> localSlotToObj(local)
}
}
@ -4829,8 +4766,7 @@ class CmdFrame(
}
}
private suspend fun readLocalSlotValue(localIndex: Int): Obj {
val localName = fn.localSlotNames.getOrNull(localIndex)
private fun localSlotToObj(localIndex: Int): Obj {
return when (frame.getSlotTypeCode(localIndex)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(localIndex))
SlotType.REAL.code -> ObjReal.of(frame.getReal(localIndex))
@ -4839,9 +4775,7 @@ class CmdFrame(
val obj = frame.getObj(localIndex)
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read(scope, localName)
is ObjProperty -> resolvePropertyLikeLocal(localName, obj)
ObjUnset -> resolveUnsetLocal(localName)
is RecordSlotRef -> obj.read()
else -> obj
}
}
@ -4849,34 +4783,13 @@ class CmdFrame(
val obj = frame.getObj(localIndex)
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read(scope, localName)
is ObjProperty -> resolvePropertyLikeLocal(localName, obj)
ObjUnset -> resolveUnsetLocal(localName)
is RecordSlotRef -> obj.read()
else -> obj
}
}
}
}
private suspend fun resolvePropertyLikeLocal(localName: String?, property: ObjProperty): Obj {
if (localName != null) {
val record = scope.parent?.get(localName) ?: scope.get(localName)
if (record != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) {
return scope.resolve(record, localName)
}
}
return property.callGetter(scope, scope.thisObj)
}
private suspend fun resolveUnsetLocal(localName: String?): Obj {
if (localName == null) return ObjUnset
val record = scope.parent?.get(localName) ?: scope.get(localName) ?: return ObjUnset
if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
return scope.resolve(record, localName)
}
return record.value
}
private suspend fun getScopeSlotValue(slot: Int): Obj {
val target = scopeTarget(slot)
val name = fn.scopeSlotNames[slot]
@ -4965,33 +4878,16 @@ class CmdFrame(
private suspend fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) {
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
val index = addrIndices[addrSlot]
val record = target.getSlotRecord(index)
val slotId = addrScopeSlots[addrSlot]
val name = fn.scopeSlotNames.getOrNull(slotId)
val record = resolveScopeSlotRecordForWrite(target, index, name)
if (name != null && record != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) {
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) {
target.assign(record, name, value)
return
}
target.setSlotValue(index, value)
}
private fun resolveScopeSlotRecordForWrite(target: Scope, index: Int, name: String?): ObjRecord? {
val record = target.getSlotRecord(index)
if (name == null) return record
if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
return record
}
if (record.value !== ObjUnset && record.memberName == null) {
return record
}
val resolved = target.get(name) ?: return record
if (resolved.value !== ObjUnset || resolved.type == ObjRecord.Type.Delegated || resolved.type == ObjRecord.Type.Property || resolved.value is ObjProperty) {
target.updateSlotFor(name, resolved)
return resolved
}
return record
}
internal fun ensureScopeSlot(target: Scope, slot: Int): Int {
val name = fn.scopeSlotNames[slot]
if (name != null) {

View File

@ -31,7 +31,7 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
val record = scope.getLocalRecordDirect(name)
?: scope.chainLookupIgnoreClosure(name, followClosure = true)
?: continue
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is net.sergeych.lyng.obj.ObjProperty) {
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
scope.resolve(record, name)
} else {
record.value

View File

@ -591,14 +591,11 @@ open class Obj {
return obj.copy(value = res, type = ObjRecord.Type.Other)
}
val value = obj.value
if (value is ObjProperty) {
val res = value.callGetter(scope, this, decl)
return obj.copy(value = res, type = ObjRecord.Type.Other)
}
if (obj.type == ObjRecord.Type.Property) {
// Some runtime paths cache the resolved property value back into the record.
// Treat that as an already-resolved read result instead of trying to call a getter again.
return obj.copy(type = ObjRecord.Type.Other)
if (value is ObjProperty || obj.type == ObjRecord.Type.Property) {
val prop = (value as? ObjProperty)
?: scope.raiseError("Expected ObjProperty for property member $name, got ${value::class}")
val res = prop.callGetter(scope, this, decl)
return ObjRecord(res, obj.isMutable)
}
val caller = scope.currentClassCtx
// Check visibility for non-property members here if they weren't checked before

View File

@ -18,14 +18,8 @@
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.bridge.bind
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.bridge.data
import net.sergeych.lyng.bridge.bindGlobalVar
import net.sergeych.lyng.bridge.globalBinder
import net.sergeych.lyng.obj.ObjFalse
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjTrue
import kotlin.test.Test
import kotlin.test.assertEquals
@ -55,97 +49,4 @@ class GlobalPropertyCaptureRegressionTest {
assertEquals(2.0, x, "bound extern var should stay live inside function bodies")
}
@Test
fun externGlobalVarShouldStayLiveWhenScriptRunsInChildScope() = runTest {
val base = Script.newScope() as ModuleScope
var x = 1.0
base.eval("extern var X: Real")
base.globalBinder().bindGlobalVar(
name = "X",
get = { x },
set = { x = it }
)
val child = base.createChildScope()
child.eval(
Source(
"child-scope-probe",
"""
fun main() {
X = X + 1.0
}
""".trimIndent()
)
)
val mainRecord = child["main"]
check(mainRecord?.type == ObjRecord.Type.Fun)
child.eval("main()")
assertEquals(2.0, x, "bound extern var should stay live in child-scope execution")
}
@Test
fun externGlobalVarShouldStayLiveAfterExternClassPropertyBranchInChildScope() = runTest {
val base = Script.newScope() as ModuleScope
var x = 3.0
base.eval(
"""
extern var X: Real
class ChoiceInputResult {
extern val isSkip: Bool
}
extern fun requestChoice(): ChoiceInputResult
""".trimIndent()
)
base.bind("ChoiceInputResult") {
addVal("isSkip") {
if (thisObjData<ChoicePayload>().isSkip) ObjTrue else ObjFalse
}
}
base.globalBinder().bindGlobalVar(
name = "X",
get = { x },
set = { x = it }
)
base.globalBinder().bindGlobalFunRaw("requestChoice") { _, _ ->
val instance = base.requireClass("ChoiceInputResult").callOn(base.createChildScope()) as ObjInstance
instance.data = ChoicePayload(isSkip = true)
instance
}
val child = base.createChildScope()
child.eval(
"""
fun main() {
val c: ChoiceInputResult = requestChoice()
if (c.isSkip) {
X = 77.0
}
}
""".trimIndent()
)
child.eval("main()")
assertEquals(77.0, x, "bound extern var should stay live after extern class property branch in child scope")
}
}
private data class ChoicePayload(
val isSkip: Boolean,
)
@Suppress("UNCHECKED_CAST")
private fun <T> ScopeFacade.thisObjData(): T {
val instance = thisObj as? ObjInstance ?: raiseClassCastError("Expected result object instance")
return instance.data as? T ?: raiseIllegalState("Bridge payload is not initialized")
}

View File

@ -1,76 +0,0 @@
/*
* 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

@ -1,158 +0,0 @@
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 }
)