Compare commits

..

3 Commits

9 changed files with 275 additions and 29 deletions

View File

@ -9,6 +9,7 @@ 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. - `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. - 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` 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. - The docs, homepage samples, and release metadata now point at the current stable version.
## User Highlights Across 1.5.x ## User Highlights Across 1.5.x
@ -303,6 +304,23 @@ object Config {
Config.show() 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 ### 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. 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,6 +1449,13 @@ class Compiler(
} }
cc.nextNonWhitespace() cc.nextNonWhitespace()
val afterSegment = cc.peekNextNonWhitespace() 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 && if (afterSegment.type != Token.Type.DOT &&
afterSegment.type != Token.Type.LT && afterSegment.type != Token.Type.LT &&
afterSegment.type != Token.Type.QUESTION && afterSegment.type != Token.Type.QUESTION &&
@ -1496,6 +1503,38 @@ 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 { private fun shouldImplicitTypeVar(name: String, explicit: Set<String>): Boolean {
if (explicit.contains(name)) return true if (explicit.contains(name)) return true
if (name.contains('.')) return false if (name.contains('.')) return false
@ -1797,7 +1836,8 @@ class Compiler(
externBindingNames = externBindingNames, externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys, preparedModuleBindingNames = importBindings.keys,
scopeRefPosByName = moduleReferencePosByName, scopeRefPosByName = moduleReferencePosByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef,
implicitThisTypeName = currentImplicitThisTypeName()
) as BytecodeStatement ) as BytecodeStatement
unwrapped to bytecodeStmt.bytecodeFunction() unwrapped to bytecodeStmt.bytecodeFunction()
} else { } else {
@ -2218,7 +2258,8 @@ class Compiler(
externBindingNames = externBindingNames, externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys, preparedModuleBindingNames = importBindings.keys,
scopeRefPosByName = moduleReferencePosByName, scopeRefPosByName = moduleReferencePosByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef,
implicitThisTypeName = currentImplicitThisTypeName()
) )
} }
@ -4253,6 +4294,13 @@ class Compiler(
} }
cc.nextNonWhitespace() cc.nextNonWhitespace()
val afterSegment = cc.peekNextNonWhitespace() 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 && if (afterSegment.type != Token.Type.DOT &&
afterSegment.type != Token.Type.LT && afterSegment.type != Token.Type.LT &&
afterSegment.type != Token.Type.QUESTION && afterSegment.type != Token.Type.QUESTION &&

View File

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

View File

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

View File

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

View File

@ -3094,11 +3094,7 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val decl = frame.fn.constants[constId] as? BytecodeConst.ExtensionPropertyDecl val decl = frame.fn.constants[constId] as? BytecodeConst.ExtensionPropertyDecl
?: error("DECL_EXT_PROPERTY expects ExtensionPropertyDecl at $constId") ?: error("DECL_EXT_PROPERTY expects ExtensionPropertyDecl at $constId")
val type = frame.ensureScope()[decl.extTypeName]?.value val type = frame.ensureScope().resolveExtensionReceiverClass(decl.extTypeName)
?: 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( frame.ensureScope().addExtension(
type, type,
decl.property.name, decl.property.name,

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -47,4 +47,46 @@ class BitwiseTest {
// type mismatch should raise // type mismatch should raise
assertFails { e("1 & 2.0") } 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,6 +384,32 @@ 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 @Test
fun testExtensionsAreScopeIsolated() = runTest { fun testExtensionsAreScopeIsolated() = runTest {
val scope1 = Script.newScope() val scope1 = Script.newScope()