Compare commits

...

3 Commits

15 changed files with 720 additions and 70 deletions

1
.gitignore vendored
View File

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

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

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "1.5.2" version = "1.5.3-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@ -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)
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() val sb = StringBuilder()
sb.append("package ").append(targetPkg).append("\n\n") sb.append("package ").append(targetPkg).append("\n\n")
sb.append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\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(body)
sb.append("\"\n") 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.parent?.get(localName)
?: context.get(localName) ?: context.get(localName)
?: continue ?: continue
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
context.resolve(record, localName) context.resolve(record, localName)
} else { } else {
record.value record.value

View File

@ -167,6 +167,19 @@ 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 { override suspend fun callOn(scope: Scope): Obj {
val resolved = read() val resolved = read()
if (resolved === this) { if (resolved === this) {
@ -193,4 +206,18 @@ class RecordSlotRef(
record.value = value 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,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

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

View File

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

View File

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

View File

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

View File

@ -591,11 +591,14 @@ open class Obj {
return obj.copy(value = res, type = ObjRecord.Type.Other) return obj.copy(value = res, type = ObjRecord.Type.Other)
} }
val value = obj.value val value = obj.value
if (value is ObjProperty || obj.type == ObjRecord.Type.Property) { if (value is ObjProperty) {
val prop = (value as? ObjProperty) val res = value.callGetter(scope, this, decl)
?: scope.raiseError("Expected ObjProperty for property member $name, got ${value::class}") return obj.copy(value = res, type = ObjRecord.Type.Other)
val res = prop.callGetter(scope, this, decl) }
return ObjRecord(res, obj.isMutable) 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)
} }
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Check visibility for non-property members here if they weren't checked before // Check visibility for non-property members here if they weren't checked before

View File

@ -18,8 +18,14 @@
package net.sergeych.lyng package net.sergeych.lyng
import kotlinx.coroutines.test.runTest 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.bindGlobalVar
import net.sergeych.lyng.bridge.globalBinder 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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -49,4 +55,97 @@ class GlobalPropertyCaptureRegressionTest {
assertEquals(2.0, x, "bound extern var should stay live inside function bodies") 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

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