Compare commits

..

No commits in common. "83099148bd9cb524558643a2740940f6ba12753d" and "671583638ba95648f8d6a3ff7210f19eace75c5d" have entirely different histories.

9 changed files with 29 additions and 275 deletions

View File

@ -9,7 +9,6 @@ For a programmer-focused migration summary across 1.5.x, see `docs/whats_new_1_5
- `1.5.4` is the stabilization release for the 1.5 feature set.
- The 1.5 line now brings together richer ranges and loops, interpolation, math modules, immutable and observable collections, richer `lyngio`, and much better CLI/IDE support.
- `1.5.4` specifically fixes user-visible issues around decimal arithmetic, mixed numeric flows, list behavior, and observable list hooks.
- `1.5.4` also fixes extension-member registration for named singleton `object` declarations, so `fun X.foo()` and `val X.bar` now work as expected.
- The docs, homepage samples, and release metadata now point at the current stable version.
## User Highlights Across 1.5.x
@ -304,23 +303,6 @@ object Config {
Config.show()
```
Named singleton objects can also be used as extension receivers:
```lyng
object X {
fun base() = "base"
}
fun X.decorate(value): String {
this.base() + ":" + value.toString()
}
val X.tag get() = this.base() + ":tag"
assertEquals("base:42", X.decorate(42))
assertEquals("base:tag", X.tag)
```
### Nested Declarations and Lifted Enums
You can now declare classes, objects, enums, and type aliases inside another class. These nested declarations live in the class namespace (no outer instance capture) and are accessed with a qualifier.

View File

@ -1449,13 +1449,6 @@ class Compiler(
}
cc.nextNonWhitespace()
val afterSegment = cc.peekNextNonWhitespace()
if (afterSegment.type == Token.Type.LT) {
val nextAfterSuffix = peekTokenAfterExtensionReceiverSegmentSuffix()
if (nextAfterSuffix != Token.Type.DOT) {
cc.restorePos(dotPos)
break
}
}
if (afterSegment.type != Token.Type.DOT &&
afterSegment.type != Token.Type.LT &&
afterSegment.type != Token.Type.QUESTION &&
@ -1503,38 +1496,6 @@ class Compiler(
}
}
private fun peekTokenAfterExtensionReceiverSegmentSuffix(): Token.Type {
val saved = cc.savePos()
try {
if (cc.peekNextNonWhitespace().type == Token.Type.LT) {
var depth = 0
while (true) {
when (val tok = cc.nextNonWhitespace().type) {
Token.Type.LT -> depth += 1
Token.Type.GT -> {
depth -= 1
if (depth <= 0) break
}
Token.Type.SHR -> {
depth -= 2
if (depth <= 0) break
}
Token.Type.EOF -> return Token.Type.EOF
else -> {}
}
}
}
if (cc.peekNextNonWhitespace().type == Token.Type.QUESTION ||
cc.peekNextNonWhitespace().type == Token.Type.IFNULLASSIGN
) {
cc.nextNonWhitespace()
}
return cc.peekNextNonWhitespace().type
} finally {
cc.restorePos(saved)
}
}
private fun shouldImplicitTypeVar(name: String, explicit: Set<String>): Boolean {
if (explicit.contains(name)) return true
if (name.contains('.')) return false
@ -1836,8 +1797,7 @@ class Compiler(
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
scopeRefPosByName = moduleReferencePosByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef,
implicitThisTypeName = currentImplicitThisTypeName()
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) as BytecodeStatement
unwrapped to bytecodeStmt.bytecodeFunction()
} else {
@ -2258,8 +2218,7 @@ class Compiler(
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
scopeRefPosByName = moduleReferencePosByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef,
implicitThisTypeName = currentImplicitThisTypeName()
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
@ -4294,13 +4253,6 @@ class Compiler(
}
cc.nextNonWhitespace()
val afterSegment = cc.peekNextNonWhitespace()
if (afterSegment.type == Token.Type.LT) {
val nextAfterSuffix = peekTokenAfterExtensionReceiverSegmentSuffix()
if (nextAfterSuffix != Token.Type.DOT) {
cc.restorePos(dotPos)
break
}
}
if (afterSegment.type != Token.Type.DOT &&
afterSegment.type != Token.Type.LT &&
afterSegment.type != Token.Type.QUESTION &&

View File

@ -85,7 +85,8 @@ internal suspend fun executeFunctionDecl(
}
if (spec.extTypeName != null) {
val type = scope.resolveExtensionReceiverClass(spec.extTypeName)
val type = scope[spec.extTypeName]?.value ?: scope.raiseSymbolNotFound("class ${spec.extTypeName} not found")
if (type !is ObjClass) scope.raiseClassCastError("${spec.extTypeName} is not the class instance")
scope.addExtension(
type,
spec.name,
@ -166,7 +167,8 @@ internal suspend fun executeFunctionDecl(
val compiledFnBody = annotatedFnBody
spec.extTypeName?.let { typeName ->
val type = scope.resolveExtensionReceiverClass(typeName)
val type = scope[typeName]?.value ?: scope.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) scope.raiseClassCastError("$typeName is not the class instance")
if (spec.isStatic) {
type.createClassField(
spec.name,

View File

@ -1004,13 +1004,4 @@ open class Scope(
return rec.value as? net.sergeych.lyng.obj.ObjClass
?: raiseClassCastError("Expected class $name, got ${rec.value.objClass.className}")
}
internal fun resolveExtensionReceiverClass(name: String): net.sergeych.lyng.obj.ObjClass {
val value = get(name)?.value ?: raiseSymbolNotFound("class $name not found")
return when (value) {
is net.sergeych.lyng.obj.ObjClass -> value
is net.sergeych.lyng.obj.ObjInstance -> value.objClass
else -> raiseClassCastError("$name is not the class instance")
}
}
}

View File

@ -46,7 +46,6 @@ class BytecodeCompiler(
private val preparedModuleBindingNames: Set<String> = emptySet(),
private val scopeRefPosByName: Map<String, Pos> = emptyMap(),
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
private val implicitThisTypeName: String? = null,
) {
private val useScopeSlots: Boolean = allowedScopeNames != null || scopeSlotNameSet != null
private var builder = CmdBuilder()
@ -695,8 +694,6 @@ class BytecodeCompiler(
val receiver = ref.preferredThisTypeName()?.let { typeName ->
compileThisVariantRef(typeName) ?: return null
} ?: compileThisRef()
val ownerClass = ref.preferredThisTypeName()?.let { resolveTypeNameClass(it) }
?: implicitThisTypeName?.let { resolveTypeNameClass(it) }
val fieldId = ref.fieldId ?: -1
val methodId = ref.methodId ?: -1
if (fieldId < 0 && methodId < 0) {
@ -713,13 +710,11 @@ class BytecodeCompiler(
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_SLOT, calleeObj.slot, args.base, encodedCount, dst)
updateSlotType(dst, SlotType.OBJ)
annotateIndexedReceiverSlot(dst, ownerClass?.let { inferFieldReturnClass(it, ref.name) })
return CompiledValue(dst, SlotType.OBJ)
}
val slot = allocSlot()
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId, methodId, slot)
updateSlotType(slot, SlotType.OBJ)
annotateIndexedReceiverSlot(slot, ownerClass?.let { inferFieldReturnClass(it, ref.name) })
CompiledValue(slot, SlotType.OBJ)
}
is ImplicitThisMethodCallRef -> compileImplicitThisMethodCall(ref)
@ -1428,23 +1423,6 @@ class BytecodeCompiler(
BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT,
BinOp.BAND, BinOp.BOR, BinOp.BXOR, BinOp.SHL, BinOp.SHR
)
val intOnlyOps = setOf(BinOp.BAND, BinOp.BOR, BinOp.BXOR, BinOp.SHL, BinOp.SHR)
if (op in intOnlyOps) {
coerceToArithmeticInt(leftRef, a)?.let { a = it }
coerceToArithmeticInt(rightRef, b)?.let { b = it }
if (a.type == SlotType.OBJ) {
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, emitAssertObjSlotIsInt(a.slot), intSlot)
updateSlotType(intSlot, SlotType.INT)
a = CompiledValue(intSlot, SlotType.INT)
}
if (b.type == SlotType.OBJ) {
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, emitAssertObjSlotIsInt(b.slot), intSlot)
updateSlotType(intSlot, SlotType.INT)
b = CompiledValue(intSlot, SlotType.INT)
}
}
val leftIsLoopVar = (leftRef as? LocalSlotRef)?.name?.let { intLoopVarNames.contains(it) } == true
val rightIsLoopVar = (rightRef as? LocalSlotRef)?.name?.let { intLoopVarNames.contains(it) } == true
if (a.type == SlotType.UNKNOWN && b.type == SlotType.INT && op in intOps && leftIsLoopVar) {
@ -3525,7 +3503,6 @@ class BytecodeCompiler(
builder.mark(endLabel)
}
updateSlotType(dst, SlotType.OBJ)
annotateIndexedReceiverSlot(dst, inferFieldReturnClass(receiverClass, ref.name))
return CompiledValue(dst, SlotType.OBJ)
}
val extSlot = resolveExtensionGetterSlot(receiverClass, ref.name)
@ -3558,7 +3535,6 @@ class BytecodeCompiler(
builder.mark(endLabel)
}
updateSlotType(dst, SlotType.OBJ)
annotateIndexedReceiverSlot(dst, inferFieldReturnClass(receiverClass, ref.name))
return CompiledValue(dst, SlotType.OBJ)
}
@ -3582,7 +3558,6 @@ class BytecodeCompiler(
private fun compileThisFieldSlotRef(ref: ThisFieldSlotRef): CompiledValue? {
val receiver = compileThisRef()
val ownerClass = implicitThisTypeName?.let { resolveTypeNameClass(it) }
val fieldId = ref.fieldId() ?: -1
val methodId = ref.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
@ -3609,13 +3584,11 @@ class BytecodeCompiler(
builder.mark(endLabel)
}
updateSlotType(dst, SlotType.OBJ)
annotateIndexedReceiverSlot(dst, ownerClass?.let { inferFieldReturnClass(it, ref.name) })
return CompiledValue(dst, SlotType.OBJ)
}
private fun compileQualifiedThisFieldSlotRef(ref: QualifiedThisFieldSlotRef): CompiledValue? {
val receiver = compileThisVariantRef(ref.receiverTypeName()) ?: return null
val ownerClass = resolveTypeNameClass(ref.receiverTypeName())
val fieldId = ref.fieldId() ?: -1
val methodId = ref.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
@ -3642,7 +3615,6 @@ class BytecodeCompiler(
builder.mark(endLabel)
}
updateSlotType(dst, SlotType.OBJ)
annotateIndexedReceiverSlot(dst, ownerClass?.let { inferFieldReturnClass(it, ref.name) })
return CompiledValue(dst, SlotType.OBJ)
}
@ -7871,25 +7843,16 @@ class BytecodeCompiler(
}
private fun listElementClassFromDecl(decl: TypeDecl): ObjClass? {
return when (decl) {
is TypeDecl.Generic -> {
if (decl.name != "List" || decl.args.size != 1) return null
val arg = decl.args.first()
val cls = when (arg) {
is TypeDecl.Simple -> resolveTypeNameClass(arg.name)
is TypeDecl.Generic -> resolveTypeNameClass(arg.name)
else -> null
}
when (cls) {
ObjInt.type, ObjReal.type, ObjString.type, ObjBool.type -> cls
else -> null
}
}
is TypeDecl.Simple -> when (decl.name.substringAfterLast('.')) {
"Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type
"String" -> ObjChar.type
else -> null
}
val generic = decl as? TypeDecl.Generic ?: return null
if (generic.name != "List" || generic.args.size != 1) return null
val arg = generic.args.first()
val cls = when (arg) {
is TypeDecl.Simple -> resolveTypeNameClass(arg.name)
is TypeDecl.Generic -> resolveTypeNameClass(arg.name)
else -> null
}
return when (cls) {
ObjInt.type, ObjReal.type, ObjString.type, ObjBool.type -> cls
else -> null
}
}
@ -7902,67 +7865,12 @@ class BytecodeCompiler(
val decl = slotTypeDeclByScopeId[scopeId]?.get(slot) ?: return null
listElementClassFromDecl(decl)
}
is ImplicitThisMemberRef -> {
val ownerClass = ref.preferredThisTypeName()?.let { resolveTypeNameClass(it) } ?: return null
val fieldClass = inferFieldReturnClass(ownerClass, ref.name) ?: return null
when (fieldClass.className) {
"Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type
"String" -> ObjChar.type
else -> null
}
}
is ThisFieldSlotRef -> {
val ownerClass = implicitThisTypeName?.let { resolveTypeNameClass(it) } ?: return null
val fieldClass = inferFieldReturnClass(ownerClass, ref.name) ?: return null
when (fieldClass.className) {
"Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type
"String" -> ObjChar.type
else -> null
}
}
is QualifiedThisFieldSlotRef -> {
val ownerClass = resolveTypeNameClass(ref.receiverTypeName()) ?: return null
val fieldClass = inferFieldReturnClass(ownerClass, ref.name) ?: return null
when (fieldClass.className) {
"Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type
"String" -> ObjChar.type
else -> null
}
}
is FieldRef -> {
val fieldClass = resolveReceiverClass(ref) ?: return null
when (fieldClass.className) {
"Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type
"String" -> ObjChar.type
else -> listElementClassFromDecl(TypeDecl.Simple(fieldClass.className, false))
}
}
else -> {
val receiverClass = resolveReceiverClass(ref) ?: return null
when (receiverClass.className) {
"Buffer", "MutableBuffer", "BitBuffer" -> ObjInt.type
"String" -> ObjChar.type
else -> null
}
}
else -> null
}
}
private fun annotateIndexedReceiverSlot(slot: Int, receiverClass: ObjClass?) {
if (receiverClass == null) return
slotObjClass[slot] = receiverClass
when (receiverClass.className) {
"Buffer", "MutableBuffer", "BitBuffer" -> listElementClassBySlot[slot] = ObjInt.type
"String" -> listElementClassBySlot[slot] = ObjChar.type
}
}
private fun indexElementClass(receiverSlot: Int, targetRef: ObjRef): ObjClass? {
listElementClassBySlot[receiverSlot]?.let { return it }
listElementClassFromReceiverRef(targetRef)?.let { return it }
val receiverClass = resolveReceiverClass(targetRef) ?: return null
return inferFieldReturnClass(receiverClass, "getAt")
}
private fun indexElementClass(receiverSlot: Int, targetRef: ObjRef): ObjClass? =
listElementClassBySlot[receiverSlot] ?: listElementClassFromReceiverRef(targetRef)
private fun indexElementSlotType(receiverSlot: Int, targetRef: ObjRef): SlotType? =
slotTypeFromClass(indexElementClass(receiverSlot, targetRef))
@ -9067,25 +8975,10 @@ class BytecodeCompiler(
private fun coerceToArithmeticInt(ref: ObjRef, value: CompiledValue): CompiledValue? {
if (value.type == SlotType.INT) return value
val refSuggestsInt = isIntLikeRef(ref) || inferNumericKind(ref) == NumericKind.INT
val refSuggestsInt = inferNumericKind(ref) == NumericKind.INT
val stableNonTemp = !isTempSlot(value.slot) && isStablePrimitiveSourceSlot(value.slot)
return when (value.type) {
SlotType.UNKNOWN -> {
if (!refSuggestsInt) return null
updateSlotType(value.slot, SlotType.INT)
CompiledValue(value.slot, SlotType.INT)
}
SlotType.OBJ -> {
if (!refSuggestsInt && !stableNonTemp) return null
coerceToLoopInt(value)?.let { return it }
if (!refSuggestsInt) return null
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, emitAssertObjSlotIsInt(value.slot), intSlot)
updateSlotType(intSlot, SlotType.INT)
CompiledValue(intSlot, SlotType.INT)
}
else -> null
}
if (!refSuggestsInt && !stableNonTemp) return null
return coerceToLoopInt(value)
}
private fun emitAssertObjSlotIsInt(slot: Int): Int {

View File

@ -92,7 +92,6 @@ class BytecodeStatement private constructor(
scopeRefPosByName: Map<String, Pos> = emptyMap(),
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
slotTypeDeclByScopeId: Map<Int, Map<Int, TypeDecl>> = emptyMap(),
implicitThisTypeName: String? = null,
): Statement {
if (statement is BytecodeStatement) return statement
val hasUnsupported = containsUnsupportedStatement(statement)
@ -129,8 +128,7 @@ class BytecodeStatement private constructor(
externBindingNames = externBindingNames,
preparedModuleBindingNames = preparedModuleBindingNames,
scopeRefPosByName = scopeRefPosByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef,
implicitThisTypeName = implicitThisTypeName
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
val compiled = compiler.compileStatement(nameHint, statement)
val fn = compiled ?: throw BytecodeCompileException(

View File

@ -3094,7 +3094,11 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm
override suspend fun perform(frame: CmdFrame) {
val decl = frame.fn.constants[constId] as? BytecodeConst.ExtensionPropertyDecl
?: error("DECL_EXT_PROPERTY expects ExtensionPropertyDecl at $constId")
val type = frame.ensureScope().resolveExtensionReceiverClass(decl.extTypeName)
val type = frame.ensureScope()[decl.extTypeName]?.value
?: frame.ensureScope().raiseSymbolNotFound("class ${decl.extTypeName} not found")
if (type !is ObjClass) {
frame.ensureScope().raiseClassCastError("${decl.extTypeName} is not the class instance")
}
frame.ensureScope().addExtension(
type,
decl.property.name,

View File

@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2025 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.
@ -47,46 +47,4 @@ class BitwiseTest {
// type mismatch should raise
assertFails { e("1 & 2.0") }
}
@Test
fun testBitwiseInference() = runTest {
eval(
"""
import lyng.buffer
class Foo() {
val buf = Buffer(64).toMutable()
fn fn2(): Int {
val tmp = this.buf[1] & 127
println("fn2: ", tmp)
tmp
}
}
val foo = Foo()
assertEquals(0, foo.fn2())
"""
)
}
@Test
fun testCustomIndexerIntInference() = runTest {
eval(
"""
class TestBuffer() {
override fn getAt(index): Int = index + 1
}
class Foo() {
val buf = TestBuffer()
fn fn2(): Int {
val tmp = (this.buf[1] & 127) + this.buf[2] * 2 - 1
tmp
}
}
val foo = Foo()
assertEquals(7, foo.fn2())
"""
)
}
}

View File

@ -384,32 +384,6 @@ class OOTest {
}
}
@Test
fun testObjectSingletonSupportsExtensions() = runTest {
val scope = Script.newScope()
scope.eval(
"""
object X {
fun base() = "base"
}
fun X.decorate<T>(value: T): String {
this.base() + ":" + value.toString()
}
val X.tag get() = this.base() + ":tag"
assertEquals("base", X.base())
assertEquals("base:42", X.decorate(42))
assertEquals("base:ok", X.decorate("ok"))
assertEquals("base:tag", X.tag)
// Wrapper names should be generated for singleton-object receivers too.
assertEquals("base:17", __ext__X__decorate(X, 17))
assertEquals("base:tag", __ext_get__X__tag(X))
""".trimIndent()
)
}
@Test
fun testExtensionsAreScopeIsolated() = runTest {
val scope1 = Script.newScope()