Compare commits

..

2 Commits

Author SHA1 Message Date
ea2cf1f373 code cleaning 2025-07-01 12:31:40 +03:00
eee6d75587 extension methids 2025-06-20 03:43:58 +04:00
9 changed files with 150 additions and 63 deletions

View File

@ -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:

View File

@ -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 |
| | | |
| | | |
| | | |
| | | |

View File

@ -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 |
| | |
| | |

View File

@ -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 {

View File

@ -83,8 +83,7 @@ class Compiler(
private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? {
if (level == lastLevel)
return parseTerm(tokens)
var lvalue = parseExpressionLevel(tokens, level + 1)
if (lvalue == null) return null
var lvalue: Accessor? = parseExpressionLevel(tokens, level + 1) ?: return null
while (true) {
@ -124,7 +123,7 @@ class Compiler(
Token.Type.DOT, Token.Type.NULL_COALESCE -> {
val isOptional = t.type == Token.Type.NULL_COALESCE
operand?.let { left ->
// dotcall: calling method on the operand, if next is ID, "("
// dot call: calling method on the operand, if next is ID, "("
var isCall = false
val next = cc.next()
if (next.type == Token.Type.ID) {
@ -154,7 +153,6 @@ class Compiler(
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
// isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
// single lambda arg, like assertTrows { ... }
cc.next()
isCall = true
@ -300,12 +298,12 @@ class Compiler(
operand?.let { left ->
// post increment
left.setter(startPos)
operand = Accessor({ cxt ->
operand = Accessor { cxt ->
val x = left.getter(cxt)
if (x.isMutable)
x.value.getAndIncrement(cxt).asReadonly
else cxt.raiseError("Cannot increment immutable value")
})
}
} ?: run {
// no lvalue means pre-increment, expression to increment follows
val next = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression")
@ -421,7 +419,7 @@ class Compiler(
}
private fun parseArrayLiteral(cc: CompilerContext): List<ListEntry> {
// it should be called after LBRACKET is consumed
// it should be called after Token.Type.LBRACKET is consumed
val entries = mutableListOf<ListEntry>()
while (true) {
val t = cc.next()
@ -554,8 +552,6 @@ class Compiler(
}
}
}
// arg list is valid:
checkNotNull(endTokenType)
return ArgsDeclaration(result, endTokenType)
}
@ -687,10 +683,10 @@ class Compiler(
else -> {
Accessor({
it.pos = t.pos
it.get(t.value)
it[t.value]
?: it.raiseError("symbol not defined: '${t.value}'")
}) { ctx, newValue ->
ctx.get(t.value)?.let { stored ->
ctx[t.value]?.let { stored ->
ctx.pos = t.pos
if (stored.isMutable)
stored.value = newValue
@ -935,7 +931,7 @@ class Compiler(
cc.skipTokens(Token.Type.NEWLINE)
t = cc.next()
} else {
// no (e: Exception) block: should be shortest variant `catch { ... }`
// no (e: Exception) block: should be the shortest variant `catch { ... }`
cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
catches += CatchBlockData(
Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
@ -1098,8 +1094,8 @@ class Compiler(
}
return statement(body.pos) {
val forContext = it.copy(start)
return statement(body.pos) { ctx ->
val forContext = ctx.copy(start)
// loop var: StoredObject
val loopSO = forContext.addItem(tVar.value, true, ObjNull)
@ -1155,7 +1151,7 @@ class Compiler(
}
}
if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(it)
result = elseStatement.execute(ctx)
}
result
}
@ -1432,11 +1428,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 +1472,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[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
@ -1526,7 +1545,7 @@ class Compiler(
if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined")
// init value could be a val; when we init by-value type var with it, we need to
// init value could be a val; when we initialize by-value type var with it, we need to
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
@ -1544,19 +1563,19 @@ class Compiler(
companion object {
fun simple(tokenType: Token.Type, priority: Int, f: suspend (Context, Obj, Obj) -> Obj): Operator =
Operator(tokenType, priority, 2, { _: Pos, a: Accessor, b: Accessor ->
Operator(tokenType, priority, 2) { _: Pos, a: Accessor, b: Accessor ->
Accessor { f(it, a.getter(it).value, b.getter(it).value).asReadonly }
})
}
}
}
companion object {
private var lastPrty = 0
private var lastPriority = 0
val allOps = listOf(
// assignments, lowest priority
Operator(Token.Type.ASSIGN, lastPrty) { pos, a, b ->
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
Accessor {
val value = b.getter(it).value
val access = a.getter(it)
@ -1566,7 +1585,7 @@ class Compiler(
value.asReadonly
}
},
Operator(Token.Type.PLUSASSIGN, lastPrty) { pos, a, b ->
Operator(Token.Type.PLUSASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
@ -1577,7 +1596,7 @@ class Compiler(
}).asReadonly
}
},
Operator(Token.Type.MINUSASSIGN, lastPrty) { pos, a, b ->
Operator(Token.Type.MINUSASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
@ -1588,7 +1607,7 @@ class Compiler(
}).asReadonly
}
},
Operator(Token.Type.STARASSIGN, lastPrty) { pos, a, b ->
Operator(Token.Type.STARASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
@ -1600,7 +1619,7 @@ class Compiler(
}).asReadonly
}
},
Operator(Token.Type.SLASHASSIGN, lastPrty) { pos, a, b ->
Operator(Token.Type.SLASHASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
@ -1611,7 +1630,7 @@ class Compiler(
}).asReadonly
}
},
Operator(Token.Type.PERCENTASSIGN, lastPrty) { pos, a, b ->
Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
Accessor {
val x = a.getter(it).value
val y = b.getter(it).value
@ -1623,42 +1642,42 @@ class Compiler(
}
},
// logical 1
Operator.simple(Token.Type.OR, ++lastPrty) { ctx, a, b -> a.logicalOr(ctx, b) },
Operator.simple(Token.Type.OR, ++lastPriority) { ctx, a, b -> a.logicalOr(ctx, b) },
// logical 2
Operator.simple(Token.Type.AND, ++lastPrty) { ctx, a, b -> a.logicalAnd(ctx, b) },
Operator.simple(Token.Type.AND, ++lastPriority) { ctx, a, b -> a.logicalAnd(ctx, b) },
// bitwise or 2
// bitwise and 3
// equality/ne 4
Operator.simple(Token.Type.EQARROW, ++lastPrty) { c, a, b -> ObjMapEntry(a, b) },
// equality/not equality 4
Operator.simple(Token.Type.EQARROW, ++lastPriority) { _, a, b -> ObjMapEntry(a, b) },
//
Operator.simple(Token.Type.EQ, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) },
Operator.simple(Token.Type.NEQ, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
Operator.simple(Token.Type.REF_EQ, lastPrty) { _, a, b -> ObjBool(a === b) },
Operator.simple(Token.Type.REF_NEQ, lastPrty) { _, a, b -> ObjBool(a !== b) },
Operator.simple(Token.Type.EQ, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) == 0) },
Operator.simple(Token.Type.NEQ, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) != 0) },
Operator.simple(Token.Type.REF_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
// relational <=,... 5
Operator.simple(Token.Type.LTE, ++lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
Operator.simple(Token.Type.LT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
Operator.simple(Token.Type.GTE, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) >= 0) },
Operator.simple(Token.Type.GT, lastPrty) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) },
Operator.simple(Token.Type.LTE, ++lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) <= 0) },
Operator.simple(Token.Type.LT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) < 0) },
Operator.simple(Token.Type.GTE, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) >= 0) },
Operator.simple(Token.Type.GT, lastPriority) { c, a, b -> ObjBool(a.compareTo(c, b) > 0) },
// in, is:
Operator.simple(Token.Type.IN, lastPrty) { c, a, b -> ObjBool(b.contains(c, a)) },
Operator.simple(Token.Type.NOTIN, lastPrty) { c, a, b -> ObjBool(!b.contains(c, a)) },
Operator.simple(Token.Type.IS, lastPrty) { c, a, b -> ObjBool(a.isInstanceOf(b)) },
Operator.simple(Token.Type.NOTIS, lastPrty) { c, a, b -> ObjBool(!a.isInstanceOf(b)) },
Operator.simple(Token.Type.IN, lastPriority) { c, a, b -> ObjBool(b.contains(c, a)) },
Operator.simple(Token.Type.NOTIN, lastPriority) { c, a, b -> ObjBool(!b.contains(c, a)) },
Operator.simple(Token.Type.IS, lastPriority) { _, a, b -> ObjBool(a.isInstanceOf(b)) },
Operator.simple(Token.Type.NOTIS, lastPriority) { _, a, b -> ObjBool(!a.isInstanceOf(b)) },
Operator.simple(Token.Type.ELVIS, ++lastPrty) { c, a, b -> if (a == ObjNull) b else a },
Operator.simple(Token.Type.ELVIS, ++lastPriority) { _, a, b -> if (a == ObjNull) b else a },
// shuttle <=> 6
Operator.simple(Token.Type.SHUTTLE, ++lastPrty) { c, a, b ->
Operator.simple(Token.Type.SHUTTLE, ++lastPriority) { c, a, b ->
ObjInt(a.compareTo(c, b).toLong())
},
// bit shifts 7
Operator.simple(Token.Type.PLUS, ++lastPrty) { ctx, a, b -> a.plus(ctx, b) },
Operator.simple(Token.Type.MINUS, lastPrty) { ctx, a, b -> a.minus(ctx, b) },
Operator.simple(Token.Type.PLUS, ++lastPriority) { ctx, a, b -> a.plus(ctx, b) },
Operator.simple(Token.Type.MINUS, lastPriority) { ctx, a, b -> a.minus(ctx, b) },
Operator.simple(Token.Type.STAR, ++lastPrty) { ctx, a, b -> a.mul(ctx, b) },
Operator.simple(Token.Type.SLASH, lastPrty) { ctx, a, b -> a.div(ctx, b) },
Operator.simple(Token.Type.PERCENT, lastPrty) { ctx, a, b -> a.mod(ctx, b) },
Operator.simple(Token.Type.STAR, ++lastPriority) { ctx, a, b -> a.mul(ctx, b) },
Operator.simple(Token.Type.SLASH, lastPriority) { ctx, a, b -> a.div(ctx, b) },
Operator.simple(Token.Type.PERCENT, lastPriority) { ctx, a, b -> a.mod(ctx, b) },
)
// private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN }
@ -1667,11 +1686,10 @@ class Compiler(
// assigner.generate(context.pos, left, right)
// }
val lastLevel = lastPrty + 1
val lastLevel = lastPriority + 1
val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l ->
allOps.filter { it.priority == l }
.map { it.tokenType to it }.toMap()
allOps.filter { it.priority == l }.associateBy { it.tokenType }
}
fun compile(code: String): Script = Compiler().compile(Source("<eval>", code))

View File

@ -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 =

View File

@ -64,6 +64,9 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
(it.thisObj as ObjReal).value.roundToLong().toObj()
},
)
addFn("toInt") {
ObjInt(thisAs<ObjReal>().value.toLong())
}
}
}
}

View File

@ -112,6 +112,7 @@ data class ObjString(val value: String) : Obj() {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble())}
}
}
}

View File

@ -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())
}
}