From eee6d75587b384db050c5d6d507bab53c3ff692c Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 20 Jun 2025 03:43:16 +0400 Subject: [PATCH] extension methids --- docs/OOP.md | 34 +++++++++++++++++++ docs/Real.md | 16 ++++----- docs/tutorial.md | 3 ++ lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 29 ++++++++++++++-- .../kotlin/net/sergeych/lyng/Context.kt | 1 + .../kotlin/net/sergeych/lyng/ObjReal.kt | 3 ++ .../kotlin/net/sergeych/lyng/ObjString.kt | 1 + lynglib/src/commonTest/kotlin/ScriptTest.kt | 27 +++++++++++++++ 9 files changed, 104 insertions(+), 12 deletions(-) diff --git a/docs/OOP.md b/docs/OOP.md index 6708e11..91fa2a4 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -160,6 +160,40 @@ For example, for our class Point: Point(1,1+1) >>> Point(x=1,y=2) +# Extending classes + +It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method +that would test if some object of unknown type contains something that can be interpreted +as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type: + + fun Object.isInteger() { + when(this) { + // already Int? + is Int -> true + + // real, but with no declimal part? + is Real -> toInt() == this + + // string with int or real reuusig code above + is String -> toReal().isInteger() + + // otherwise, no: + else -> false + } + } + + // Let's test: + assert( 12.isInteger() == true ) + assert( 12.1.isInteger() == false ) + assert( "5".isInteger() ) + assert( ! "5.2".isInteger() ) + >>> void + +__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created, +as they are modifying the type, not the context. + +Beware of it. We might need to reconsider it later. + # Theory ## Basic principles: diff --git a/docs/Real.md b/docs/Real.md index a76b47d..2f63c7a 100644 --- a/docs/Real.md +++ b/docs/Real.md @@ -15,11 +15,11 @@ you can use it's class to ensure type: ## Member functions -| name | meaning | type | -|-----------------|------------------------------------|------| -| `.roundToInt()` | round to nearest int like round(x) | Int | -| | | | -| | | | -| | | | -| | | | -| | | | +| name | meaning | type | +|-----------------|-------------------------------------------------------------|------| +| `.roundToInt()` | round to nearest int like round(x) | Int | +| `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int | +| | | | +| | | | +| | | | +| | | | diff --git a/docs/tutorial.md b/docs/tutorial.md index 9f552ca..8cc033a 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1214,6 +1214,9 @@ Typical set of String functions includes: | [Range] | substring at range | | s1 + s2 | concatenation | | s1 += s2 | self-modifying concatenation | +| toReal() | attempts to parse string as a Real value | +| | | +| | | diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index efa9f08..18cbf76 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.6.8-SNAPSHOT" +version = "0.6.9-SNAPSHOT" buildscript { repositories { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 9a2ed5f..010a970 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1432,11 +1432,22 @@ class Compiler( ): Statement { var t = cc.next() val start = t.pos - val name = if (t.type != Token.Type.ID) - throw ScriptError(t.pos, "Expected identifier after 'fn'") + var extTypeName: String? = null + var name = if (t.type != Token.Type.ID) + throw ScriptError(t.pos, "Expected identifier after 'fun'") else t.value t = cc.next() + // Is extension? + if( t.type == Token.Type.DOT) { + extTypeName = name + t = cc.next() + if( t.type != Token.Type.ID) + throw ScriptError(t.pos, "illegal extension format: expected function name") + name = t.value + t = cc.next() + } + if (t.type != Token.Type.LPAREN) throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'") @@ -1465,13 +1476,25 @@ class Compiler( // load params from caller context argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val) + if( extTypeName != null ) { + context.thisObj = callerContext.thisObj + } fnStatements.execute(context) } return statement(start) { context -> // we added fn in the context. now we must save closure // for the function closure = context - context.addItem(name, false, fnBody, visibility) + extTypeName?.let { typeName -> + // class extension method + val type = context.get(typeName)?.value ?: context.raiseSymbolNotFound("class $typeName not found") + if( type !is ObjClass ) context.raiseClassCastError("$typeName is not the class instance") + type.addFn( name, isOpen = true) { + fnBody.execute(this) + } + } + // regular function/method + ?: context.addItem(name, false, fnBody, visibility) // as the function can be called from anywhere, we have // saved the proper context in the closure fnBody diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index ec67348..ec26443 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -72,6 +72,7 @@ open class Context( else { objects[name] ?: parent?.get(name) + ?: thisObj.objClass.getInstanceMemberOrNull(name) } fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt index 7d8fcf4..b1de8fd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjReal.kt @@ -64,6 +64,9 @@ data class ObjReal(val value: Double) : Obj(), Numeric { (it.thisObj as ObjReal).value.roundToLong().toObj() }, ) + addFn("toInt") { + ObjInt(thisAs().value.toLong()) + } } } } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt index d5d6456..29ae702 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt @@ -112,6 +112,7 @@ data class ObjString(val value: String) : Obj() { thisAs().value.uppercase().let(::ObjString) } addFn("size") { ObjInt(thisAs().value.length.toLong()) } + addFn("toReal") { ObjReal(thisAs().value.toDouble())} } } } \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 02d334b..f34f68b 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -2284,4 +2284,31 @@ class ScriptTest { """.trimIndent()) } + + @Test + fun testExtend() = runTest() { + eval(""" + + fun Int.isEven() { + this % 2 == 0 + } + + fun Object.isInteger() { + when(this) { + is Int -> true + is Real -> toInt() == this + is String -> toReal().isInteger() + else -> false + } + } + + assert( 4.isEven() ) + assert( !5.isEven() ) + + assert( 12.isInteger() == true ) + assert( 12.1.isInteger() == false ) + assert( "5".isInteger() ) + assert( ! "5.2".isInteger() ) + """.trimIndent()) + } } \ No newline at end of file