From c9da0b256f6699e4df2f525ffc680b6bcce39bf3 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 3 Feb 2026 07:36:09 +0300 Subject: [PATCH] Add minimal generics and typed callable casts --- .../kotlin/net/sergeych/lyng/CodeContext.kt | 3 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 367 ++++++++++++++++-- .../kotlin/net/sergeych/lyng/TypeDecl.kt | 8 +- .../lyng/bytecode/BytecodeCompiler.kt | 41 +- .../net/sergeych/lyng/obj/ObjIterable.kt | 21 +- .../kotlin/net/sergeych/lyng/obj/ObjList.kt | 5 +- lynglib/src/commonTest/kotlin/StdlibTest.kt | 2 - lynglib/stdlib/lyng/root.lyng | 6 +- notes/new_lyng_type_system_spec.md | 11 +- 9 files changed, 419 insertions(+), 45 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt index bc9773d..c6b38f6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt @@ -22,7 +22,8 @@ sealed class CodeContext { class Function( val name: String, val implicitThisMembers: Boolean = false, - val implicitThisTypeName: String? = null + val implicitThisTypeName: String? = null, + val typeParams: Set = emptySet() ): CodeContext() class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() { val pendingInitializations = mutableMapOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index abce1ed..06bef14 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -103,6 +103,8 @@ class Compiler( } private fun moduleSlotPlan(): SlotPlan? = slotPlanStack.firstOrNull() + private val slotTypeByScopeId: MutableMap> = mutableMapOf() + private val nameObjClass: MutableMap = mutableMapOf() private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) { val plan = moduleSlotPlan() ?: return @@ -112,6 +114,36 @@ class Compiler( if (!record.visibility.isPublic) continue declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated) } + for ((cls, map) in current.extensions) { + for ((name, record) in map) { + if (!record.visibility.isPublic) continue + when (record.type) { + ObjRecord.Type.Property -> { + declareSlotNameIn( + plan, + extensionPropertyGetterName(cls.className, name), + isMutable = false, + isDelegated = false + ) + val prop = record.value as? ObjProperty + if (prop?.setter != null) { + declareSlotNameIn( + plan, + extensionPropertySetterName(cls.className, name), + isMutable = false, + isDelegated = false + ) + } + } + else -> declareSlotNameIn( + plan, + extensionCallableName(cls.className, name), + isMutable = false, + isDelegated = false + ) + } + } + } for ((name, slotIndex) in current.slotNameToIndexSnapshot()) { val record = current.getSlotRecord(slotIndex) if (!record.visibility.isPublic) continue @@ -126,6 +158,8 @@ class Compiler( val plan = moduleSlotPlan() ?: return val saved = cc.savePos() var depth = 0 + var parenDepth = 0 + var bracketDepth = 0 fun nextNonWs(): Token { var t = cc.next() while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { @@ -139,7 +173,12 @@ class Compiler( when (t.type) { Token.Type.LBRACE -> depth++ Token.Type.RBRACE -> if (depth > 0) depth-- + Token.Type.LPAREN -> parenDepth++ + Token.Type.RPAREN -> if (parenDepth > 0) parenDepth-- + Token.Type.LBRACKET -> bracketDepth++ + Token.Type.RBRACKET -> if (bracketDepth > 0) bracketDepth-- Token.Type.ID -> if (depth == 0) { + if (parenDepth > 0 || bracketDepth > 0) continue when (t.value) { "fun", "fn" -> { val nameToken = nextNonWs() @@ -399,6 +438,14 @@ class Compiler( return null } + private fun currentTypeParams(): Set { + for (ctx in codeContexts.asReversed()) { + val fn = ctx as? CodeContext.Function ?: continue + if (fn.typeParams.isNotEmpty()) return fn.typeParams + } + return emptySet() + } + private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? { for (i in slotPlanStack.indices.reversed()) { if (!includeModule && i == 0) continue @@ -468,6 +515,15 @@ class Compiler( val ids = resolveMemberIds(name, pos, null) return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, currentImplicitThisTypeName()) } + val implicitThisMembers = codeContexts.any { ctx -> + (ctx as? CodeContext.Function)?.implicitThisMembers == true + } + val implicitType = if (implicitThisMembers) currentImplicitThisTypeName() else null + if (implicitType != null && hasImplicitThisMember(name, implicitType)) { + resolutionSink?.referenceMember(name, pos, implicitType) + val ids = resolveImplicitThisMemberIds(name, pos, implicitType) + return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType) + } val modulePlan = moduleSlotPlan() val moduleEntry = modulePlan?.slots?.get(name) if (moduleEntry != null) { @@ -514,15 +570,6 @@ class Compiler( return ref } } - val implicitThis = codeContexts.any { ctx -> - (ctx as? CodeContext.Function)?.implicitThisMembers == true - } - if (implicitThis) { - val implicitType = currentImplicitThisTypeName() - resolutionSink?.referenceMember(name, pos, implicitType) - val ids = resolveImplicitThisMemberIds(name, pos, implicitType) - return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType) - } val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody } if (classContext && extensionNames.contains(name)) { resolutionSink?.referenceMember(name, pos) @@ -915,9 +962,16 @@ class Compiler( private fun resolveImplicitThisMemberIds(name: String, pos: Pos, implicitTypeName: String?): MemberIds { if (implicitTypeName == null) return resolveMemberIds(name, pos, null) val info = resolveCompileClassInfo(implicitTypeName) - ?: if (allowUnresolvedRefs) return MemberIds(null, null) else throw ScriptError(pos, "unknown type $implicitTypeName") - val fieldId = info.fieldIds[name] - val methodId = info.methodIds[name] + val fieldId = info?.fieldIds?.get(name) + val methodId = info?.methodIds?.get(name) + if (fieldId == null && methodId == null) { + val cls = resolveClassByName(implicitTypeName) + if (cls != null) { + val fId = cls.instanceFieldIdMap()[name] + val mId = cls.instanceMethodIdMap(includeAbstract = true)[name] + if (fId != null || mId != null) return MemberIds(fId, mId) + } + } if (fieldId == null && methodId == null) { if (hasExtensionFor(implicitTypeName, name)) return MemberIds(null, null) if (allowUnresolvedRefs) return MemberIds(null, null) @@ -925,6 +979,19 @@ class Compiler( } return MemberIds(fieldId, methodId) } + + private fun hasImplicitThisMember(name: String, implicitTypeName: String?): Boolean { + if (implicitTypeName == null) return false + val info = resolveCompileClassInfo(implicitTypeName) + if (info != null && (info.fieldIds.containsKey(name) || info.methodIds.containsKey(name))) return true + val cls = resolveClassByName(implicitTypeName) + if (cls != null) { + if (cls.instanceFieldIdMap().containsKey(name) || cls.instanceMethodIdMap(includeAbstract = true).containsKey(name)) { + return true + } + } + return hasExtensionFor(implicitTypeName, name) + } private val currentRangeParamNames: Set get() = rangeParamNamesStack.lastOrNull() ?: emptySet() private val capturePlanStack = mutableListOf() @@ -1443,10 +1510,19 @@ class Compiler( break } - val rvalue = parseExpressionLevel(level + 1) - ?: throw ScriptError(opToken.pos, "Expecting expression") - - val res = op.generate(opToken.pos, lvalue!!, rvalue) + val res = if (opToken.type == Token.Type.AS || opToken.type == Token.Type.ASNULL) { + val (typeDecl, _) = parseTypeExpressionWithMini() + val typeRef = typeDeclToTypeRef(typeDecl, opToken.pos) + if (opToken.type == Token.Type.AS) { + CastRef(lvalue!!, typeRef, false, opToken.pos) + } else { + CastRef(lvalue!!, typeRef, true, opToken.pos) + } + } else { + val rvalue = parseExpressionLevel(level + 1) + ?: throw ScriptError(opToken.pos, "Expecting expression") + op.generate(opToken.pos, lvalue!!, rvalue) + } if (opToken.type == Token.Type.ASSIGN) { val ctx = codeContexts.lastOrNull() if (ctx is CodeContext.ClassBody) { @@ -1527,7 +1603,10 @@ class Compiler( Token.Type.LPAREN -> { cc.next() // instance method call - val parsed = parseArgs() + val receiverType = if (next.value == "apply" || next.value == "run") { + inferReceiverTypeFromRef(left) + } else null + val parsed = parseArgs(receiverType) val args = parsed.first val tailBlock = parsed.second if (left is LocalVarRef && left.name == "scope") { @@ -1578,7 +1657,10 @@ class Compiler( // single lambda arg, like assertThrows { ... } cc.next() isCall = true - val lambda = parseLambdaExpression() + val receiverType = if (next.value == "apply" || next.value == "run") { + inferReceiverTypeFromRef(left) + } else null + val lambda = parseLambdaExpression(receiverType) val argPos = next.pos val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos)) operand = when (left) { @@ -2274,6 +2356,89 @@ class Compiler( } private fun parseTypeExpressionWithMini(): Pair { + parseFunctionTypeWithMini()?.let { return it } + return parseSimpleTypeExpressionWithMini() + } + + private fun parseFunctionTypeWithMini(): Pair? { + val saved = cc.savePos() + val startPos = cc.currentPos() + + fun parseParamTypes(): List> { + val params = mutableListOf>() + cc.skipWsTokens() + if (cc.peekNextNonWhitespace().type == Token.Type.RPAREN) { + cc.nextNonWhitespace() + return params + } + while (true) { + val (paramDecl, paramMini) = parseTypeExpressionWithMini() + params += paramDecl to paramMini + val sep = cc.nextNonWhitespace() + when (sep.type) { + Token.Type.COMMA -> continue + Token.Type.RPAREN -> return params + else -> sep.raiseSyntax("expected ',' or ')' in function type") + } + } + } + + var receiverDecl: TypeDecl? = null + var receiverMini: MiniTypeRef? = null + + val first = cc.peekNextNonWhitespace() + if (first.type == Token.Type.LPAREN) { + cc.nextNonWhitespace() + } else { + val recv = parseSimpleTypeExpressionWithMini() + val dotPos = cc.savePos() + if (cc.skipTokenOfType(Token.Type.DOT, isOptional = true) && cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { + receiverDecl = recv.first + receiverMini = recv.second + cc.nextNonWhitespace() + } else { + cc.restorePos(saved) + return null + } + } + + val params = parseParamTypes() + val arrow = cc.nextNonWhitespace() + if (arrow.type != Token.Type.ARROW) { + cc.restorePos(saved) + return null + } + val (retDecl, retMini) = parseTypeExpressionWithMini() + + val isNullable = if (cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)) { + true + } else if (cc.skipTokenOfType(Token.Type.IFNULLASSIGN, isOptional = true)) { + cc.pushPendingAssign() + true + } else false + + val rangeStart = when (receiverMini) { + null -> startPos + else -> receiverMini.range.start + } + val rangeEnd = cc.currentPos() + val mini = MiniFunctionType( + range = MiniRange(rangeStart, rangeEnd), + receiver = receiverMini, + params = params.map { it.second }, + returnType = retMini, + nullable = isNullable + ) + val sem = TypeDecl.Function( + receiver = receiverDecl, + params = params.map { it.first }, + returnType = retDecl, + nullable = isNullable + ) + return sem to mini + } + + private fun parseSimpleTypeExpressionWithMini(): Pair { // Parse a qualified base name: ID ('.' ID)* val segments = mutableListOf() var first = true @@ -2295,7 +2460,11 @@ class Compiler( val dotPos = cc.savePos() val t = cc.next() if (t.type == Token.Type.DOT) { - // continue + val nextAfterDot = cc.peekNextNonWhitespace() + if (nextAfterDot.type == Token.Type.LPAREN) { + cc.restorePos(dotPos) + break + } continue } else { cc.restorePos(dotPos) @@ -2304,6 +2473,18 @@ class Compiler( } val qualified = segments.joinToString(".") { it.name } + val typeParams = currentTypeParams() + if (segments.size == 1 && typeParams.contains(qualified)) { + val isNullable = if (cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)) { + true + } else if (cc.skipTokenOfType(Token.Type.IFNULLASSIGN, isOptional = true)) { + cc.pushPendingAssign() + true + } else false + val rangeEnd = cc.currentPos() + val miniRef = MiniTypeVar(MiniRange(typeStart, rangeEnd), qualified, isNullable) + return TypeDecl.TypeVar(qualified, isNullable) to miniRef + } if (segments.size > 1) { lastPos?.let { pos -> resolutionSink?.reference(qualified, pos) } } else { @@ -2366,6 +2547,44 @@ class Compiler( return Pair(sem, miniRef) } + private fun typeDeclToTypeRef(typeDecl: TypeDecl, pos: Pos): ObjRef { + return when (typeDecl) { + TypeDecl.TypeAny, + TypeDecl.TypeNullableAny, + is TypeDecl.TypeVar -> ConstRef(Obj.rootObjectType.asReadonly) + else -> { + val cls = resolveTypeDeclObjClass(typeDecl) + if (cls != null) return ConstRef(cls.asReadonly) + val name = typeDeclName(typeDecl) + resolveLocalTypeRef(name, pos)?.let { return it } + throw ScriptError(pos, "unknown type $name") + } + } + } + + private fun typeDeclName(typeDecl: TypeDecl): String = when (typeDecl) { + is TypeDecl.Simple -> typeDecl.name + is TypeDecl.Generic -> typeDecl.name + is TypeDecl.Function -> "Callable" + is TypeDecl.TypeVar -> typeDecl.name + TypeDecl.TypeAny -> "Object" + TypeDecl.TypeNullableAny -> "Object?" + } + + private fun resolveLocalTypeRef(name: String, pos: Pos): ObjRef? { + val slotLoc = lookupSlotLocation(name, includeModule = true) ?: return null + captureLocalRef(name, slotLoc, pos)?.let { return it } + return LocalSlotRef( + name, + slotLoc.slot, + slotLoc.scopeId, + slotLoc.isMutable, + slotLoc.isDelegated, + pos, + strictSlotRefs + ) + } + /** * Parse arguments list during the call and detect last block argument * _following the parenthesis_ call: `(1,2) { ... }` @@ -2502,6 +2721,11 @@ class Compiler( ): ObjRef { var detectedBlockArgument = blockArgument val expectedReceiver = tailBlockReceiverType(left) + val withReceiver = when (left) { + is LocalVarRef -> if (left.name == "with") left.name else null + is LocalSlotRef -> if (left.name == "with") left.name else null + else -> null + } val args = if (blockArgument) { // Leading '{' has already been consumed by the caller token branch. // Parse only the lambda expression as the last argument and DO NOT @@ -2511,9 +2735,24 @@ class Compiler( val callableAccessor = parseLambdaExpression(expectedReceiver) listOf(ParsedArgument(ExpressionStatement(callableAccessor, cc.currentPos()), cc.currentPos())) } else { - val r = parseArgs(expectedReceiver) - detectedBlockArgument = r.second - r.first + if (withReceiver != null) { + val parsedArgs = parseArgsNoTailBlock().toMutableList() + val pos = cc.savePos() + val end = cc.next() + if (end.type == Token.Type.LBRACE) { + val receiverType = inferReceiverTypeFromArgs(parsedArgs) + val callableAccessor = parseLambdaExpression(receiverType) + parsedArgs += ParsedArgument(ExpressionStatement(callableAccessor, end.pos), end.pos) + detectedBlockArgument = true + } else { + cc.restorePos(pos) + } + parsedArgs + } else { + val r = parseArgs(expectedReceiver) + detectedBlockArgument = r.second + r.first + } } val implicitThisTypeName = currentImplicitThisTypeName() return when (left) { @@ -2571,6 +2810,26 @@ class Compiler( } } + private fun inferReceiverTypeFromArgs(args: List): String? { + val stmt = args.firstOrNull()?.value as? ExpressionStatement ?: return null + val ref = stmt.ref + val bySlot = (ref as? LocalSlotRef)?.let { slotRef -> + slotTypeByScopeId[slotRef.scopeId]?.get(slotRef.slot) + } + val byName = (ref as? LocalVarRef)?.let { nameObjClass[it.name] } + val cls = bySlot ?: byName ?: resolveInitializerObjClass(stmt) + return cls?.className + } + + private fun inferReceiverTypeFromRef(ref: ObjRef): String? { + return when (ref) { + is LocalSlotRef -> slotTypeByScopeId[ref.scopeId]?.get(ref.slot)?.className + is LocalVarRef -> nameObjClass[ref.name]?.className + is QualifiedThisRef -> ref.typeName + else -> null + } + } + private suspend fun parseAccessor(): ObjRef? { // could be: literal val t = cc.next() @@ -3597,6 +3856,40 @@ class Compiler( miniSink?.onClassDecl(node) } resolutionSink?.declareClass(nameToken.value, baseSpecs.map { it.name }, startPos) + classCtx?.let { ctx -> + val baseIds = collectBaseMemberIds(baseSpecs.map { it.name }) + ctx.memberFieldIds.putAll(baseIds.fieldIds) + ctx.memberMethodIds.putAll(baseIds.methodIds) + ctx.nextFieldId = maxOf(ctx.nextFieldId, baseIds.nextFieldId) + ctx.nextMethodId = maxOf(ctx.nextMethodId, baseIds.nextMethodId) + for (member in ctx.declaredMembers) { + val isOverride = ctx.memberOverrides[member] == true + val hasBaseField = member in baseIds.fieldIds + val hasBaseMethod = member in baseIds.methodIds + if (isOverride) { + if (!hasBaseField && !hasBaseMethod) { + throw ScriptError(nameToken.pos, "member $member is marked 'override' but does not override anything") + } + } else { + if (hasBaseField || hasBaseMethod) { + throw ScriptError(nameToken.pos, "member $member overrides parent member but 'override' keyword is missing") + } + } + if (!ctx.memberFieldIds.containsKey(member)) { + ctx.memberFieldIds[member] = ctx.nextFieldId++ + } + if (!ctx.memberMethodIds.containsKey(member)) { + ctx.memberMethodIds[member] = ctx.nextMethodId++ + } + } + compileClassInfos[nameToken.value] = CompileClassInfo( + name = nameToken.value, + fieldIds = ctx.memberFieldIds.toMap(), + methodIds = ctx.memberMethodIds.toMap(), + nextFieldId = ctx.nextFieldId, + nextMethodId = ctx.nextMethodId + ) + } // restore if no body starts here cc.restorePos(saved) null @@ -4067,6 +4360,25 @@ class Compiler( declareLocalName(extensionWrapperName, isMutable = false) } + val typeParams = mutableSetOf() + if (cc.peekNextNonWhitespace().type == Token.Type.LT) { + cc.nextNonWhitespace() + while (true) { + val idTok = cc.requireToken(Token.Type.ID, "type parameter name expected") + typeParams.add(idTok.value) + val sep = cc.nextNonWhitespace() + when (sep.type) { + Token.Type.COMMA -> continue + Token.Type.GT -> break + Token.Type.SHR -> { + cc.pushPendingGT() + break + } + else -> sep.raiseSyntax("expected ',' or '>' in type parameter list") + } + } + } + val argsDeclaration: ArgsDeclaration = if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { cc.nextNonWhitespace() // consume ( @@ -4128,7 +4440,8 @@ class Compiler( CodeContext.Function( name, implicitThisMembers = implicitThisMembers, - implicitThisTypeName = extTypeName + implicitThisTypeName = extTypeName, + typeParams = typeParams ) ) { cc.labels.add(name) @@ -4545,6 +4858,8 @@ class Compiler( val rawName = when (type) { is TypeDecl.Simple -> type.name is TypeDecl.Generic -> type.name + is TypeDecl.Function -> "Callable" + is TypeDecl.TypeVar -> return null else -> return null } val name = rawName.substringAfterLast('.') @@ -4986,6 +5301,12 @@ class Compiler( val slotIndex = slotPlan?.slots?.get(name)?.index val scopeId = slotPlan?.id val initObjClass = resolveInitializerObjClass(initialExpression) ?: resolveTypeDeclObjClass(varTypeDecl) + if (initObjClass != null) { + if (slotIndex != null && scopeId != null) { + slotTypeByScopeId.getOrPut(scopeId) { mutableMapOf() }[slotIndex] = initObjClass + } + nameObjClass[name] = initObjClass + } return VarDeclStatement( name, isMutable, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt index 309fac6..8f40238 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt @@ -23,7 +23,13 @@ package net.sergeych.lyng // very soon sealed class TypeDecl(val isNullable:Boolean = false) { // ?? -// data class Fn(val argTypes: List, val retType: TypeDecl) : TypeDecl() + data class Function( + val receiver: TypeDecl?, + val params: List, + val returnType: TypeDecl, + val nullable: Boolean = false + ) : TypeDecl(nullable) + data class TypeVar(val name: String, val nullable: Boolean = false) : TypeDecl(nullable) object TypeAny : TypeDecl() object TypeNullableAny : TypeDecl(true) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 588a067..e75a28a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -4368,6 +4368,7 @@ class BytecodeCompiler( is ListLiteralRef -> ObjList.type is MapLiteralRef -> ObjMap.type is RangeRef -> ObjRange.type + is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClass(it.ref) } is ConstRef -> when (ref.constValue) { is ObjList -> ObjList.type is ObjMap -> ObjMap.type @@ -4397,7 +4398,13 @@ class BytecodeCompiler( } else { when (ref.name) { "map", + "mapNotNull", "filter", + "filterNotNull", + "drop", + "take", + "flatMap", + "flatten", "sorted", "sortedBy", "sortedWith", @@ -4405,6 +4412,9 @@ class BytecodeCompiler( "toList", "shuffle", "shuffled" -> ObjList.type + "dropLast" -> ObjFlow.type + "takeLast" -> ObjRingBuffer.type + "count" -> ObjInt.type "toSet" -> ObjSet.type "toMap" -> ObjMap.type "joinToString" -> ObjString.type @@ -4429,6 +4439,7 @@ class BytecodeCompiler( is ListLiteralRef -> ObjList.type is MapLiteralRef -> ObjMap.type is RangeRef -> ObjRange.type + is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClassForScopeCollection(it.ref) } is ConstRef -> when (ref.constValue) { is ObjList -> ObjList.type is ObjMap -> ObjMap.type @@ -4449,8 +4460,34 @@ class BytecodeCompiler( } is MethodCallRef -> { val targetClass = resolveReceiverClassForScopeCollection(ref.receiver) ?: return null - if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) ObjRegex.type - else null + if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) { + ObjRegex.type + } else { + when (ref.name) { + "map", + "mapNotNull", + "filter", + "filterNotNull", + "drop", + "take", + "flatMap", + "flatten", + "sorted", + "sortedBy", + "sortedWith", + "reversed", + "toList", + "shuffle", + "shuffled" -> ObjList.type + "dropLast" -> ObjFlow.type + "takeLast" -> ObjRingBuffer.type + "count" -> ObjInt.type + "toSet" -> ObjSet.type + "toMap" -> ObjMap.type + "joinToString" -> ObjString.type + else -> null + } + } } else -> null } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt index 2186522..fc13b20 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt @@ -36,20 +36,19 @@ val ObjIterable by lazy { code = null ) - addPropertyDoc( + addFnDoc( name = "toList", doc = "Collect elements of this iterable into a new list.", - type = type("lyng.List"), - moduleName = "lyng.stdlib", - getter = { - val result = mutableListOf() - val it = this.thisObj.invokeInstanceMethod(this, "iterator") - while (it.invokeInstanceMethod(this, "hasNext").toBool()) { - result.add(it.invokeInstanceMethod(this, "next")) - } - ObjList(result) + returns = type("lyng.List"), + moduleName = "lyng.stdlib" + ) { + val result = mutableListOf() + val it = thisObj.invokeInstanceMethod(this, "iterator") + while (it.invokeInstanceMethod(this, "hasNext").toBool()) { + result.add(it.invokeInstanceMethod(this, "next")) } - ) + ObjList(result) + } // it is not effective, but it is open: addFnDoc( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 60d8b4b..07d9cc3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -247,6 +247,10 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { companion object { val type = object : ObjClass("List", ObjArray) { + override suspend fun callOn(scope: Scope): Obj { + return ObjList(scope.args.list.toMutableList()) + } + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { return ObjList(decoder.decodeAnyList(scope)) } @@ -519,4 +523,3 @@ fun MutableList.swap(i: Int, j: Int) { } } - diff --git a/lynglib/src/commonTest/kotlin/StdlibTest.kt b/lynglib/src/commonTest/kotlin/StdlibTest.kt index e95fa97..b88154d 100644 --- a/lynglib/src/commonTest/kotlin/StdlibTest.kt +++ b/lynglib/src/commonTest/kotlin/StdlibTest.kt @@ -17,10 +17,8 @@ import kotlinx.coroutines.test.runTest import net.sergeych.lyng.eval -import kotlin.test.Ignore import kotlin.test.Test -@Ignore class StdlibTest { @Test fun testIterableFilter() = runTest { diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 4e88cf1..bb66718 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -229,7 +229,7 @@ fun Iterable.sortedBy(predicate) { /* Return a shuffled copy of the iterable as a list. */ fun Iterable.shuffled() { - val list: List = toList + val list: List = toList() list.shuffle() list } @@ -327,10 +327,10 @@ interface Delegate { Executes the block with `this` set to self and returns what the block returns. */ -fun with(self, block) { +fun with(self: T, block: T.()->R): R { var result = Unset self.apply { result = block() } - result + result as R } /* diff --git a/notes/new_lyng_type_system_spec.md b/notes/new_lyng_type_system_spec.md index 690f749..2ef5655 100644 --- a/notes/new_lyng_type_system_spec.md +++ b/notes/new_lyng_type_system_spec.md @@ -4,7 +4,7 @@ better than even in C++ ;) ```lyng fun t(x) { - // x is Object, and x is nullable + // x is Object (non-null) println(x) } @@ -117,6 +117,10 @@ Notes and open questions to answer in this spec: Not null by default (Object), must be specified with `?` suffix. We use Kotlin-style `!!` for non-null assertion. Therefore we check nullability at compile time, and we throw NPE only at `x!!` or `obj as X` (if obj is nullable, it is same as `obj!! as X`). +Return type inference and nullability: +- If any branch or return expression is nullable, the inferred return type is nullable. +- This is independent of whether `return` is used or implicit last-expression rules apply. + - Default type of untyped values: If a parameter has no type and no default, is it Object? (dynamic), or a new top type? Lets discuss in more details. For example: @@ -205,6 +209,11 @@ I think we can omit if not used. For kotlin interop: if the class has at least o Compile time error unless it is an Object own method. Let's force rewriting existing code in favor of explicit casts. It will repay itself: I laready have a project on Lyng that suffers from implicit casts har to trace errors. +No runtime lookups or fallbacks: +- All symbol and member resolution must be done at compile time. +- If an extension is not known at compile time (not imported or declared before use), it is a compile-time error. +- Runtime lookup is only possible via explicit reflection helpers. + Example: ```lyng fun f(x) { // x: Object