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(1,1+1)
>>> Point(x=1,y=2) >>> 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 # Theory
## Basic principles: ## Basic principles:

View File

@ -15,11 +15,11 @@ you can use it's class to ensure type:
## Member functions ## Member functions
| name | meaning | type | | name | meaning | type |
|-----------------|------------------------------------|------| |-----------------|-------------------------------------------------------------|------|
| `.roundToInt()` | round to nearest int like round(x) | Int | | `.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 | | [Range] | substring at range |
| s1 + s2 | concatenation | | s1 + s2 | concatenation |
| s1 += s2 | self-modifying 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 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "0.6.8-SNAPSHOT" version = "0.6.9-SNAPSHOT"
buildscript { buildscript {
repositories { repositories {

View File

@ -83,8 +83,7 @@ class Compiler(
private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? { private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? {
if (level == lastLevel) if (level == lastLevel)
return parseTerm(tokens) return parseTerm(tokens)
var lvalue = parseExpressionLevel(tokens, level + 1) var lvalue: Accessor? = parseExpressionLevel(tokens, level + 1) ?: return null
if (lvalue == null) return null
while (true) { while (true) {
@ -124,7 +123,7 @@ class Compiler(
Token.Type.DOT, Token.Type.NULL_COALESCE -> { Token.Type.DOT, Token.Type.NULL_COALESCE -> {
val isOptional = t.type == Token.Type.NULL_COALESCE val isOptional = t.type == Token.Type.NULL_COALESCE
operand?.let { left -> 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 var isCall = false
val next = cc.next() val next = cc.next()
if (next.type == Token.Type.ID) { if (next.type == Token.Type.ID) {
@ -154,7 +153,6 @@ class Compiler(
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
// isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
// single lambda arg, like assertTrows { ... } // single lambda arg, like assertTrows { ... }
cc.next() cc.next()
isCall = true isCall = true
@ -300,12 +298,12 @@ class Compiler(
operand?.let { left -> operand?.let { left ->
// post increment // post increment
left.setter(startPos) left.setter(startPos)
operand = Accessor({ cxt -> operand = Accessor { cxt ->
val x = left.getter(cxt) val x = left.getter(cxt)
if (x.isMutable) if (x.isMutable)
x.value.getAndIncrement(cxt).asReadonly x.value.getAndIncrement(cxt).asReadonly
else cxt.raiseError("Cannot increment immutable value") else cxt.raiseError("Cannot increment immutable value")
}) }
} ?: run { } ?: run {
// no lvalue means pre-increment, expression to increment follows // no lvalue means pre-increment, expression to increment follows
val next = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression") val next = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression")
@ -421,7 +419,7 @@ class Compiler(
} }
private fun parseArrayLiteral(cc: CompilerContext): List<ListEntry> { 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>() val entries = mutableListOf<ListEntry>()
while (true) { while (true) {
val t = cc.next() val t = cc.next()
@ -554,8 +552,6 @@ class Compiler(
} }
} }
} }
// arg list is valid:
checkNotNull(endTokenType)
return ArgsDeclaration(result, endTokenType) return ArgsDeclaration(result, endTokenType)
} }
@ -687,10 +683,10 @@ class Compiler(
else -> { else -> {
Accessor({ Accessor({
it.pos = t.pos it.pos = t.pos
it.get(t.value) it[t.value]
?: it.raiseError("symbol not defined: '${t.value}'") ?: it.raiseError("symbol not defined: '${t.value}'")
}) { ctx, newValue -> }) { ctx, newValue ->
ctx.get(t.value)?.let { stored -> ctx[t.value]?.let { stored ->
ctx.pos = t.pos ctx.pos = t.pos
if (stored.isMutable) if (stored.isMutable)
stored.value = newValue stored.value = newValue
@ -935,7 +931,7 @@ class Compiler(
cc.skipTokens(Token.Type.NEWLINE) cc.skipTokens(Token.Type.NEWLINE)
t = cc.next() t = cc.next()
} else { } 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") cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
catches += CatchBlockData( catches += CatchBlockData(
Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"), Token("it", cc.currentPos(), Token.Type.ID), listOf("Exception"),
@ -1098,8 +1094,8 @@ class Compiler(
} }
return statement(body.pos) { return statement(body.pos) { ctx ->
val forContext = it.copy(start) val forContext = ctx.copy(start)
// loop var: StoredObject // loop var: StoredObject
val loopSO = forContext.addItem(tVar.value, true, ObjNull) val loopSO = forContext.addItem(tVar.value, true, ObjNull)
@ -1155,7 +1151,7 @@ class Compiler(
} }
} }
if (!breakCaught && elseStatement != null) { if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(it) result = elseStatement.execute(ctx)
} }
result result
} }
@ -1432,11 +1428,22 @@ class Compiler(
): Statement { ): Statement {
var t = cc.next() var t = cc.next()
val start = t.pos val start = t.pos
val name = if (t.type != Token.Type.ID) var extTypeName: String? = null
throw ScriptError(t.pos, "Expected identifier after 'fn'") var name = if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "Expected identifier after 'fun'")
else t.value else t.value
t = cc.next() 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) if (t.type != Token.Type.LPAREN)
throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'") throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'")
@ -1465,13 +1472,25 @@ class Compiler(
// load params from caller context // load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val) argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
if( extTypeName != null ) {
context.thisObj = callerContext.thisObj
}
fnStatements.execute(context) fnStatements.execute(context)
} }
return statement(start) { context -> return statement(start) { context ->
// we added fn in the context. now we must save closure // we added fn in the context. now we must save closure
// for the function // for the function
closure = context 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 // as the function can be called from anywhere, we have
// saved the proper context in the closure // saved the proper context in the closure
fnBody fnBody
@ -1526,7 +1545,7 @@ class Compiler(
if (context.containsLocal(name)) if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined") 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: // create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
@ -1544,19 +1563,19 @@ class Compiler(
companion object { companion object {
fun simple(tokenType: Token.Type, priority: Int, f: suspend (Context, Obj, Obj) -> Obj): Operator = 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 } Accessor { f(it, a.getter(it).value, b.getter(it).value).asReadonly }
}) }
} }
} }
companion object { companion object {
private var lastPrty = 0 private var lastPriority = 0
val allOps = listOf( val allOps = listOf(
// assignments, lowest priority // assignments, lowest priority
Operator(Token.Type.ASSIGN, lastPrty) { pos, a, b -> Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
Accessor { Accessor {
val value = b.getter(it).value val value = b.getter(it).value
val access = a.getter(it) val access = a.getter(it)
@ -1566,7 +1585,7 @@ class Compiler(
value.asReadonly value.asReadonly
} }
}, },
Operator(Token.Type.PLUSASSIGN, lastPrty) { pos, a, b -> Operator(Token.Type.PLUSASSIGN, lastPriority) { pos, a, b ->
Accessor { Accessor {
val x = a.getter(it).value val x = a.getter(it).value
val y = b.getter(it).value val y = b.getter(it).value
@ -1577,7 +1596,7 @@ class Compiler(
}).asReadonly }).asReadonly
} }
}, },
Operator(Token.Type.MINUSASSIGN, lastPrty) { pos, a, b -> Operator(Token.Type.MINUSASSIGN, lastPriority) { pos, a, b ->
Accessor { Accessor {
val x = a.getter(it).value val x = a.getter(it).value
val y = b.getter(it).value val y = b.getter(it).value
@ -1588,7 +1607,7 @@ class Compiler(
}).asReadonly }).asReadonly
} }
}, },
Operator(Token.Type.STARASSIGN, lastPrty) { pos, a, b -> Operator(Token.Type.STARASSIGN, lastPriority) { pos, a, b ->
Accessor { Accessor {
val x = a.getter(it).value val x = a.getter(it).value
val y = b.getter(it).value val y = b.getter(it).value
@ -1600,7 +1619,7 @@ class Compiler(
}).asReadonly }).asReadonly
} }
}, },
Operator(Token.Type.SLASHASSIGN, lastPrty) { pos, a, b -> Operator(Token.Type.SLASHASSIGN, lastPriority) { pos, a, b ->
Accessor { Accessor {
val x = a.getter(it).value val x = a.getter(it).value
val y = b.getter(it).value val y = b.getter(it).value
@ -1611,7 +1630,7 @@ class Compiler(
}).asReadonly }).asReadonly
} }
}, },
Operator(Token.Type.PERCENTASSIGN, lastPrty) { pos, a, b -> Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
Accessor { Accessor {
val x = a.getter(it).value val x = a.getter(it).value
val y = b.getter(it).value val y = b.getter(it).value
@ -1623,42 +1642,42 @@ class Compiler(
} }
}, },
// logical 1 // 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 // 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 or 2
// bitwise and 3 // bitwise and 3
// equality/ne 4 // equality/not equality 4
Operator.simple(Token.Type.EQARROW, ++lastPrty) { c, a, b -> ObjMapEntry(a, b) }, 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.EQ, ++lastPriority) { 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.NEQ, lastPriority) { 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_EQ, lastPriority) { _, a, b -> ObjBool(a === b) },
Operator.simple(Token.Type.REF_NEQ, lastPrty) { _, a, b -> ObjBool(a !== b) }, Operator.simple(Token.Type.REF_NEQ, lastPriority) { _, a, b -> ObjBool(a !== b) },
// relational <=,... 5 // relational <=,... 5
Operator.simple(Token.Type.LTE, ++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, lastPrty) { 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, lastPrty) { 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, lastPrty) { 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: // in, is:
Operator.simple(Token.Type.IN, lastPrty) { c, a, b -> ObjBool(b.contains(c, a)) }, Operator.simple(Token.Type.IN, lastPriority) { 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.NOTIN, lastPriority) { 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.IS, lastPriority) { _, a, b -> ObjBool(a.isInstanceOf(b)) },
Operator.simple(Token.Type.NOTIS, lastPrty) { c, 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 // 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()) ObjInt(a.compareTo(c, b).toLong())
}, },
// bit shifts 7 // bit shifts 7
Operator.simple(Token.Type.PLUS, ++lastPrty) { ctx, a, b -> a.plus(ctx, b) }, Operator.simple(Token.Type.PLUS, ++lastPriority) { ctx, a, b -> a.plus(ctx, b) },
Operator.simple(Token.Type.MINUS, lastPrty) { ctx, a, b -> a.minus(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.STAR, ++lastPriority) { ctx, a, b -> a.mul(ctx, b) },
Operator.simple(Token.Type.SLASH, lastPrty) { ctx, a, b -> a.div(ctx, b) }, Operator.simple(Token.Type.SLASH, lastPriority) { ctx, a, b -> a.div(ctx, b) },
Operator.simple(Token.Type.PERCENT, lastPrty) { ctx, a, b -> a.mod(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 } // private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN }
@ -1667,11 +1686,10 @@ class Compiler(
// assigner.generate(context.pos, left, right) // 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 -> val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l ->
allOps.filter { it.priority == l } allOps.filter { it.priority == l }.associateBy { it.tokenType }
.map { it.tokenType to it }.toMap()
} }
fun compile(code: String): Script = Compiler().compile(Source("<eval>", code)) fun compile(code: String): Script = Compiler().compile(Source("<eval>", code))

View File

@ -72,6 +72,7 @@ open class Context(
else { else {
objects[name] objects[name]
?: parent?.get(name) ?: parent?.get(name)
?: thisObj.objClass.getInstanceMemberOrNull(name)
} }
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context = 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() (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) thisAs<ObjString>().value.uppercase().let(::ObjString)
} }
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) } addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble())}
} }
} }
} }

View File

@ -2284,4 +2284,31 @@ class ScriptTest {
""".trimIndent()) """.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())
}
} }