Add minimal generics and typed callable casts
This commit is contained in:
parent
51b397686d
commit
c9da0b256f
@ -22,7 +22,8 @@ sealed class CodeContext {
|
|||||||
class Function(
|
class Function(
|
||||||
val name: String,
|
val name: String,
|
||||||
val implicitThisMembers: Boolean = false,
|
val implicitThisMembers: Boolean = false,
|
||||||
val implicitThisTypeName: String? = null
|
val implicitThisTypeName: String? = null,
|
||||||
|
val typeParams: Set<String> = emptySet()
|
||||||
): CodeContext()
|
): CodeContext()
|
||||||
class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() {
|
class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() {
|
||||||
val pendingInitializations = mutableMapOf<String, Pos>()
|
val pendingInitializations = mutableMapOf<String, Pos>()
|
||||||
|
|||||||
@ -103,6 +103,8 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun moduleSlotPlan(): SlotPlan? = slotPlanStack.firstOrNull()
|
private fun moduleSlotPlan(): SlotPlan? = slotPlanStack.firstOrNull()
|
||||||
|
private val slotTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
|
||||||
|
private val nameObjClass: MutableMap<String, ObjClass> = mutableMapOf()
|
||||||
|
|
||||||
private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) {
|
private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) {
|
||||||
val plan = moduleSlotPlan() ?: return
|
val plan = moduleSlotPlan() ?: return
|
||||||
@ -112,6 +114,36 @@ class Compiler(
|
|||||||
if (!record.visibility.isPublic) continue
|
if (!record.visibility.isPublic) continue
|
||||||
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
|
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()) {
|
for ((name, slotIndex) in current.slotNameToIndexSnapshot()) {
|
||||||
val record = current.getSlotRecord(slotIndex)
|
val record = current.getSlotRecord(slotIndex)
|
||||||
if (!record.visibility.isPublic) continue
|
if (!record.visibility.isPublic) continue
|
||||||
@ -126,6 +158,8 @@ class Compiler(
|
|||||||
val plan = moduleSlotPlan() ?: return
|
val plan = moduleSlotPlan() ?: return
|
||||||
val saved = cc.savePos()
|
val saved = cc.savePos()
|
||||||
var depth = 0
|
var depth = 0
|
||||||
|
var parenDepth = 0
|
||||||
|
var bracketDepth = 0
|
||||||
fun nextNonWs(): Token {
|
fun nextNonWs(): Token {
|
||||||
var t = cc.next()
|
var t = cc.next()
|
||||||
while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
|
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) {
|
when (t.type) {
|
||||||
Token.Type.LBRACE -> depth++
|
Token.Type.LBRACE -> depth++
|
||||||
Token.Type.RBRACE -> if (depth > 0) 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) {
|
Token.Type.ID -> if (depth == 0) {
|
||||||
|
if (parenDepth > 0 || bracketDepth > 0) continue
|
||||||
when (t.value) {
|
when (t.value) {
|
||||||
"fun", "fn" -> {
|
"fun", "fn" -> {
|
||||||
val nameToken = nextNonWs()
|
val nameToken = nextNonWs()
|
||||||
@ -399,6 +438,14 @@ class Compiler(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun currentTypeParams(): Set<String> {
|
||||||
|
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? {
|
private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? {
|
||||||
for (i in slotPlanStack.indices.reversed()) {
|
for (i in slotPlanStack.indices.reversed()) {
|
||||||
if (!includeModule && i == 0) continue
|
if (!includeModule && i == 0) continue
|
||||||
@ -468,6 +515,15 @@ class Compiler(
|
|||||||
val ids = resolveMemberIds(name, pos, null)
|
val ids = resolveMemberIds(name, pos, null)
|
||||||
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, currentImplicitThisTypeName())
|
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 modulePlan = moduleSlotPlan()
|
||||||
val moduleEntry = modulePlan?.slots?.get(name)
|
val moduleEntry = modulePlan?.slots?.get(name)
|
||||||
if (moduleEntry != null) {
|
if (moduleEntry != null) {
|
||||||
@ -514,15 +570,6 @@ class Compiler(
|
|||||||
return ref
|
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 }
|
val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
|
||||||
if (classContext && extensionNames.contains(name)) {
|
if (classContext && extensionNames.contains(name)) {
|
||||||
resolutionSink?.referenceMember(name, pos)
|
resolutionSink?.referenceMember(name, pos)
|
||||||
@ -915,9 +962,16 @@ class Compiler(
|
|||||||
private fun resolveImplicitThisMemberIds(name: String, pos: Pos, implicitTypeName: String?): MemberIds {
|
private fun resolveImplicitThisMemberIds(name: String, pos: Pos, implicitTypeName: String?): MemberIds {
|
||||||
if (implicitTypeName == null) return resolveMemberIds(name, pos, null)
|
if (implicitTypeName == null) return resolveMemberIds(name, pos, null)
|
||||||
val info = resolveCompileClassInfo(implicitTypeName)
|
val info = resolveCompileClassInfo(implicitTypeName)
|
||||||
?: if (allowUnresolvedRefs) return MemberIds(null, null) else throw ScriptError(pos, "unknown type $implicitTypeName")
|
val fieldId = info?.fieldIds?.get(name)
|
||||||
val fieldId = info.fieldIds[name]
|
val methodId = info?.methodIds?.get(name)
|
||||||
val methodId = info.methodIds[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 (fieldId == null && methodId == null) {
|
||||||
if (hasExtensionFor(implicitTypeName, name)) return MemberIds(null, null)
|
if (hasExtensionFor(implicitTypeName, name)) return MemberIds(null, null)
|
||||||
if (allowUnresolvedRefs) return MemberIds(null, null)
|
if (allowUnresolvedRefs) return MemberIds(null, null)
|
||||||
@ -925,6 +979,19 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
return MemberIds(fieldId, methodId)
|
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<String>
|
private val currentRangeParamNames: Set<String>
|
||||||
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
|
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
|
||||||
private val capturePlanStack = mutableListOf<CapturePlan>()
|
private val capturePlanStack = mutableListOf<CapturePlan>()
|
||||||
@ -1443,10 +1510,19 @@ class Compiler(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
val rvalue = parseExpressionLevel(level + 1)
|
val res = if (opToken.type == Token.Type.AS || opToken.type == Token.Type.ASNULL) {
|
||||||
?: throw ScriptError(opToken.pos, "Expecting expression")
|
val (typeDecl, _) = parseTypeExpressionWithMini()
|
||||||
|
val typeRef = typeDeclToTypeRef(typeDecl, opToken.pos)
|
||||||
val res = op.generate(opToken.pos, lvalue!!, rvalue)
|
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) {
|
if (opToken.type == Token.Type.ASSIGN) {
|
||||||
val ctx = codeContexts.lastOrNull()
|
val ctx = codeContexts.lastOrNull()
|
||||||
if (ctx is CodeContext.ClassBody) {
|
if (ctx is CodeContext.ClassBody) {
|
||||||
@ -1527,7 +1603,10 @@ class Compiler(
|
|||||||
Token.Type.LPAREN -> {
|
Token.Type.LPAREN -> {
|
||||||
cc.next()
|
cc.next()
|
||||||
// instance method call
|
// 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 args = parsed.first
|
||||||
val tailBlock = parsed.second
|
val tailBlock = parsed.second
|
||||||
if (left is LocalVarRef && left.name == "scope") {
|
if (left is LocalVarRef && left.name == "scope") {
|
||||||
@ -1578,7 +1657,10 @@ class Compiler(
|
|||||||
// single lambda arg, like assertThrows { ... }
|
// single lambda arg, like assertThrows { ... }
|
||||||
cc.next()
|
cc.next()
|
||||||
isCall = true
|
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 argPos = next.pos
|
||||||
val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos))
|
val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos))
|
||||||
operand = when (left) {
|
operand = when (left) {
|
||||||
@ -2274,6 +2356,89 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
||||||
|
parseFunctionTypeWithMini()?.let { return it }
|
||||||
|
return parseSimpleTypeExpressionWithMini()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFunctionTypeWithMini(): Pair<TypeDecl, MiniTypeRef>? {
|
||||||
|
val saved = cc.savePos()
|
||||||
|
val startPos = cc.currentPos()
|
||||||
|
|
||||||
|
fun parseParamTypes(): List<Pair<TypeDecl, MiniTypeRef>> {
|
||||||
|
val params = mutableListOf<Pair<TypeDecl, MiniTypeRef>>()
|
||||||
|
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<TypeDecl, MiniTypeRef> {
|
||||||
// Parse a qualified base name: ID ('.' ID)*
|
// Parse a qualified base name: ID ('.' ID)*
|
||||||
val segments = mutableListOf<MiniTypeName.Segment>()
|
val segments = mutableListOf<MiniTypeName.Segment>()
|
||||||
var first = true
|
var first = true
|
||||||
@ -2295,7 +2460,11 @@ class Compiler(
|
|||||||
val dotPos = cc.savePos()
|
val dotPos = cc.savePos()
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
if (t.type == Token.Type.DOT) {
|
if (t.type == Token.Type.DOT) {
|
||||||
// continue
|
val nextAfterDot = cc.peekNextNonWhitespace()
|
||||||
|
if (nextAfterDot.type == Token.Type.LPAREN) {
|
||||||
|
cc.restorePos(dotPos)
|
||||||
|
break
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
cc.restorePos(dotPos)
|
cc.restorePos(dotPos)
|
||||||
@ -2304,6 +2473,18 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val qualified = segments.joinToString(".") { it.name }
|
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) {
|
if (segments.size > 1) {
|
||||||
lastPos?.let { pos -> resolutionSink?.reference(qualified, pos) }
|
lastPos?.let { pos -> resolutionSink?.reference(qualified, pos) }
|
||||||
} else {
|
} else {
|
||||||
@ -2366,6 +2547,44 @@ class Compiler(
|
|||||||
return Pair(sem, miniRef)
|
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
|
* Parse arguments list during the call and detect last block argument
|
||||||
* _following the parenthesis_ call: `(1,2) { ... }`
|
* _following the parenthesis_ call: `(1,2) { ... }`
|
||||||
@ -2502,6 +2721,11 @@ class Compiler(
|
|||||||
): ObjRef {
|
): ObjRef {
|
||||||
var detectedBlockArgument = blockArgument
|
var detectedBlockArgument = blockArgument
|
||||||
val expectedReceiver = tailBlockReceiverType(left)
|
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) {
|
val args = if (blockArgument) {
|
||||||
// Leading '{' has already been consumed by the caller token branch.
|
// Leading '{' has already been consumed by the caller token branch.
|
||||||
// Parse only the lambda expression as the last argument and DO NOT
|
// Parse only the lambda expression as the last argument and DO NOT
|
||||||
@ -2511,9 +2735,24 @@ class Compiler(
|
|||||||
val callableAccessor = parseLambdaExpression(expectedReceiver)
|
val callableAccessor = parseLambdaExpression(expectedReceiver)
|
||||||
listOf(ParsedArgument(ExpressionStatement(callableAccessor, cc.currentPos()), cc.currentPos()))
|
listOf(ParsedArgument(ExpressionStatement(callableAccessor, cc.currentPos()), cc.currentPos()))
|
||||||
} else {
|
} else {
|
||||||
val r = parseArgs(expectedReceiver)
|
if (withReceiver != null) {
|
||||||
detectedBlockArgument = r.second
|
val parsedArgs = parseArgsNoTailBlock().toMutableList()
|
||||||
r.first
|
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()
|
val implicitThisTypeName = currentImplicitThisTypeName()
|
||||||
return when (left) {
|
return when (left) {
|
||||||
@ -2571,6 +2810,26 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferReceiverTypeFromArgs(args: List<ParsedArgument>): 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? {
|
private suspend fun parseAccessor(): ObjRef? {
|
||||||
// could be: literal
|
// could be: literal
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
@ -3597,6 +3856,40 @@ class Compiler(
|
|||||||
miniSink?.onClassDecl(node)
|
miniSink?.onClassDecl(node)
|
||||||
}
|
}
|
||||||
resolutionSink?.declareClass(nameToken.value, baseSpecs.map { it.name }, startPos)
|
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
|
// restore if no body starts here
|
||||||
cc.restorePos(saved)
|
cc.restorePos(saved)
|
||||||
null
|
null
|
||||||
@ -4067,6 +4360,25 @@ class Compiler(
|
|||||||
declareLocalName(extensionWrapperName, isMutable = false)
|
declareLocalName(extensionWrapperName, isMutable = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val typeParams = mutableSetOf<String>()
|
||||||
|
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 =
|
val argsDeclaration: ArgsDeclaration =
|
||||||
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
|
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
|
||||||
cc.nextNonWhitespace() // consume (
|
cc.nextNonWhitespace() // consume (
|
||||||
@ -4128,7 +4440,8 @@ class Compiler(
|
|||||||
CodeContext.Function(
|
CodeContext.Function(
|
||||||
name,
|
name,
|
||||||
implicitThisMembers = implicitThisMembers,
|
implicitThisMembers = implicitThisMembers,
|
||||||
implicitThisTypeName = extTypeName
|
implicitThisTypeName = extTypeName,
|
||||||
|
typeParams = typeParams
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
cc.labels.add(name)
|
cc.labels.add(name)
|
||||||
@ -4545,6 +4858,8 @@ class Compiler(
|
|||||||
val rawName = when (type) {
|
val rawName = when (type) {
|
||||||
is TypeDecl.Simple -> type.name
|
is TypeDecl.Simple -> type.name
|
||||||
is TypeDecl.Generic -> type.name
|
is TypeDecl.Generic -> type.name
|
||||||
|
is TypeDecl.Function -> "Callable"
|
||||||
|
is TypeDecl.TypeVar -> return null
|
||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
val name = rawName.substringAfterLast('.')
|
val name = rawName.substringAfterLast('.')
|
||||||
@ -4986,6 +5301,12 @@ class Compiler(
|
|||||||
val slotIndex = slotPlan?.slots?.get(name)?.index
|
val slotIndex = slotPlan?.slots?.get(name)?.index
|
||||||
val scopeId = slotPlan?.id
|
val scopeId = slotPlan?.id
|
||||||
val initObjClass = resolveInitializerObjClass(initialExpression) ?: resolveTypeDeclObjClass(varTypeDecl)
|
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(
|
return VarDeclStatement(
|
||||||
name,
|
name,
|
||||||
isMutable,
|
isMutable,
|
||||||
|
|||||||
@ -23,7 +23,13 @@ package net.sergeych.lyng
|
|||||||
// very soon
|
// very soon
|
||||||
sealed class TypeDecl(val isNullable:Boolean = false) {
|
sealed class TypeDecl(val isNullable:Boolean = false) {
|
||||||
// ??
|
// ??
|
||||||
// data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
|
data class Function(
|
||||||
|
val receiver: TypeDecl?,
|
||||||
|
val params: List<TypeDecl>,
|
||||||
|
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 TypeAny : TypeDecl()
|
||||||
object TypeNullableAny : TypeDecl(true)
|
object TypeNullableAny : TypeDecl(true)
|
||||||
|
|
||||||
|
|||||||
@ -4368,6 +4368,7 @@ class BytecodeCompiler(
|
|||||||
is ListLiteralRef -> ObjList.type
|
is ListLiteralRef -> ObjList.type
|
||||||
is MapLiteralRef -> ObjMap.type
|
is MapLiteralRef -> ObjMap.type
|
||||||
is RangeRef -> ObjRange.type
|
is RangeRef -> ObjRange.type
|
||||||
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClass(it.ref) }
|
||||||
is ConstRef -> when (ref.constValue) {
|
is ConstRef -> when (ref.constValue) {
|
||||||
is ObjList -> ObjList.type
|
is ObjList -> ObjList.type
|
||||||
is ObjMap -> ObjMap.type
|
is ObjMap -> ObjMap.type
|
||||||
@ -4397,7 +4398,13 @@ class BytecodeCompiler(
|
|||||||
} else {
|
} else {
|
||||||
when (ref.name) {
|
when (ref.name) {
|
||||||
"map",
|
"map",
|
||||||
|
"mapNotNull",
|
||||||
"filter",
|
"filter",
|
||||||
|
"filterNotNull",
|
||||||
|
"drop",
|
||||||
|
"take",
|
||||||
|
"flatMap",
|
||||||
|
"flatten",
|
||||||
"sorted",
|
"sorted",
|
||||||
"sortedBy",
|
"sortedBy",
|
||||||
"sortedWith",
|
"sortedWith",
|
||||||
@ -4405,6 +4412,9 @@ class BytecodeCompiler(
|
|||||||
"toList",
|
"toList",
|
||||||
"shuffle",
|
"shuffle",
|
||||||
"shuffled" -> ObjList.type
|
"shuffled" -> ObjList.type
|
||||||
|
"dropLast" -> ObjFlow.type
|
||||||
|
"takeLast" -> ObjRingBuffer.type
|
||||||
|
"count" -> ObjInt.type
|
||||||
"toSet" -> ObjSet.type
|
"toSet" -> ObjSet.type
|
||||||
"toMap" -> ObjMap.type
|
"toMap" -> ObjMap.type
|
||||||
"joinToString" -> ObjString.type
|
"joinToString" -> ObjString.type
|
||||||
@ -4429,6 +4439,7 @@ class BytecodeCompiler(
|
|||||||
is ListLiteralRef -> ObjList.type
|
is ListLiteralRef -> ObjList.type
|
||||||
is MapLiteralRef -> ObjMap.type
|
is MapLiteralRef -> ObjMap.type
|
||||||
is RangeRef -> ObjRange.type
|
is RangeRef -> ObjRange.type
|
||||||
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClassForScopeCollection(it.ref) }
|
||||||
is ConstRef -> when (ref.constValue) {
|
is ConstRef -> when (ref.constValue) {
|
||||||
is ObjList -> ObjList.type
|
is ObjList -> ObjList.type
|
||||||
is ObjMap -> ObjMap.type
|
is ObjMap -> ObjMap.type
|
||||||
@ -4449,8 +4460,34 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
is MethodCallRef -> {
|
is MethodCallRef -> {
|
||||||
val targetClass = resolveReceiverClassForScopeCollection(ref.receiver) ?: return null
|
val targetClass = resolveReceiverClassForScopeCollection(ref.receiver) ?: return null
|
||||||
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) ObjRegex.type
|
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
|
||||||
else null
|
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
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,20 +36,19 @@ val ObjIterable by lazy {
|
|||||||
code = null
|
code = null
|
||||||
)
|
)
|
||||||
|
|
||||||
addPropertyDoc(
|
addFnDoc(
|
||||||
name = "toList",
|
name = "toList",
|
||||||
doc = "Collect elements of this iterable into a new list.",
|
doc = "Collect elements of this iterable into a new list.",
|
||||||
type = type("lyng.List"),
|
returns = type("lyng.List"),
|
||||||
moduleName = "lyng.stdlib",
|
moduleName = "lyng.stdlib"
|
||||||
getter = {
|
) {
|
||||||
val result = mutableListOf<Obj>()
|
val result = mutableListOf<Obj>()
|
||||||
val it = this.thisObj.invokeInstanceMethod(this, "iterator")
|
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||||
result.add(it.invokeInstanceMethod(this, "next"))
|
result.add(it.invokeInstanceMethod(this, "next"))
|
||||||
}
|
|
||||||
ObjList(result)
|
|
||||||
}
|
}
|
||||||
)
|
ObjList(result)
|
||||||
|
}
|
||||||
|
|
||||||
// it is not effective, but it is open:
|
// it is not effective, but it is open:
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
|
|||||||
@ -247,6 +247,10 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val type = object : ObjClass("List", ObjArray) {
|
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 {
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
return ObjList(decoder.decodeAnyList(scope))
|
return ObjList(decoder.decodeAnyList(scope))
|
||||||
}
|
}
|
||||||
@ -519,4 +523,3 @@ fun <T>MutableList<T>.swap(i: Int, j: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -17,10 +17,8 @@
|
|||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.lyng.eval
|
import net.sergeych.lyng.eval
|
||||||
import kotlin.test.Ignore
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@Ignore
|
|
||||||
class StdlibTest {
|
class StdlibTest {
|
||||||
@Test
|
@Test
|
||||||
fun testIterableFilter() = runTest {
|
fun testIterableFilter() = runTest {
|
||||||
|
|||||||
@ -229,7 +229,7 @@ fun Iterable.sortedBy(predicate) {
|
|||||||
|
|
||||||
/* Return a shuffled copy of the iterable as a list. */
|
/* Return a shuffled copy of the iterable as a list. */
|
||||||
fun Iterable.shuffled() {
|
fun Iterable.shuffled() {
|
||||||
val list: List = toList
|
val list: List = toList()
|
||||||
list.shuffle()
|
list.shuffle()
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
@ -327,10 +327,10 @@ interface Delegate {
|
|||||||
Executes the block with `this` set to self and
|
Executes the block with `this` set to self and
|
||||||
returns what the block returns.
|
returns what the block returns.
|
||||||
*/
|
*/
|
||||||
fun with(self, block) {
|
fun with<T,R>(self: T, block: T.()->R): R {
|
||||||
var result = Unset
|
var result = Unset
|
||||||
self.apply { result = block() }
|
self.apply { result = block() }
|
||||||
result
|
result as R
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -4,7 +4,7 @@ better than even in C++ ;)
|
|||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
fun t(x) {
|
fun t(x) {
|
||||||
// x is Object, and x is nullable
|
// x is Object (non-null)
|
||||||
println(x)
|
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`).
|
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?
|
- 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:
|
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.
|
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:
|
Example:
|
||||||
```lyng
|
```lyng
|
||||||
fun f(x) { // x: Object
|
fun f(x) { // x: Object
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user