operators overriding

This commit is contained in:
Sergey Chernov 2026-01-07 09:33:10 +01:00
parent fe5dded7af
commit 2c0a6c7b34
24 changed files with 216 additions and 57 deletions

View File

@ -544,6 +544,99 @@ class Critical {
Attempting to override a `closed` member results in a compile-time error.
## Operator Overloading
Lyng allows you to overload standard operators by defining specific named methods in your classes. When an operator expression is evaluated, Lyng delegates the operation to these methods if they are available.
### Binary Operators
To overload a binary operator, define the corresponding method that takes one argument:
| Operator | Method Name |
| :--- | :--- |
| `a + b` | `plus(other)` |
| `a - b` | `minus(other)` |
| `a * b` | `mul(other)` |
| `a / b` | `div(other)` |
| `a % b` | `mod(other)` |
| `a && b` | `logicalAnd(other)` |
| `a \|\| b` | `logicalOr(other)` |
| `a =~ b` | `operatorMatch(other)` |
| `a & b` | `bitAnd(other)` |
| `a \| b` | `bitOr(other)` |
| `a ^ b` | `bitXor(other)` |
| `a << b` | `shl(other)` |
| `a >> b` | `shr(other)` |
Example:
```lyng
class Vector(val x, val y) {
fun plus(other) = Vector(x + other.x, y + other.y)
override fun toString() = "Vector(${x}, ${y})"
}
val v1 = Vector(1, 2)
val v2 = Vector(3, 4)
assertEquals(Vector(4, 6), v1 + v2)
```
### Unary Operators
Unary operators are overloaded by defining methods with no arguments:
| Operator | Method Name |
| :--- | :--- |
| `-a` | `negate()` |
| `!a` | `logicalNot()` |
| `~a` | `bitNot()` |
### Assignment Operators
Assignment operators like `+=` first attempt to call a specific assignment method. If that method is not defined, they fall back to a combination of the binary operator and a regular assignment (e.g., `a = a + b`).
| Operator | Method Name | Fallback |
| :--- | :--- | :--- |
| `a += b` | `plusAssign(other)` | `a = a + b` |
| `a -= b` | `minusAssign(other)` | `a = a - b` |
| `a *= b` | `mulAssign(other)` | `a = a * b` |
| `a /= b` | `divAssign(other)` | `a = a / b` |
| `a %= b` | `modAssign(other)` | `a = a % b` |
Example of in-place mutation:
```lyng
class Counter(var value) {
fun plusAssign(n) {
value = value + n
}
}
val c = Counter(10)
c += 5
assertEquals(15, c.value)
```
### Comparison Operators
Comparison operators use `compareTo` and `equals`.
| Operator | Method Name |
| :--- | :--- |
| `a == b`, `a != b` | `equals(other)` |
| `<`, `>`, `<=`, `>=`, `<=>` | `compareTo(other)` |
- `compareTo` should return:
- `0` if `a == b`
- A negative integer if `a < b`
- A positive integer if `a > b`
- The `<=>` (shuttle) operator returns the result of `compareTo` directly.
- `equals` returns a `Bool`. If `equals` is not explicitly defined, Lyng falls back to `compareTo(other) == 0`.
> **Note**: Methods that are already defined in the base `Obj` class (like `equals`, `toString`, or `contains`) require the `override` keyword when redefined in your class or as an extension. Other operator methods (like `plus` or `negate`) do not require `override` unless they are already present in your class's hierarchy.
### Increment and Decrement
`++` and `--` operators are implemented using `plus(1)` or `minus(1)` combined with an assignment back to the variable. If the variable is a field or local variable, it will be updated with the result of the operation.
Compatibility notes:
- Existing single‑inheritance code continues to work unchanged; its resolution order reduces to the single base.

View File

@ -1351,8 +1351,11 @@ class Compiler(
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
if (!isMember && (isOverride || isClosed))
throw ScriptError(currentToken.pos, "modifiers override and closed are only allowed for class members")
if (!isMember && isClosed)
throw ScriptError(currentToken.pos, "modifier closed is only allowed for class members")
if (!isMember && isOverride && currentToken.value != "fun" && currentToken.value != "fn")
throw ScriptError(currentToken.pos, "modifier override outside class is only allowed for extension functions")
if (!isMember && isAbstract && currentToken.value != "class")
throw ScriptError(currentToken.pos, "modifier abstract at top level is only allowed for classes")

View File

@ -143,11 +143,17 @@ open class Obj {
open suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other === this) return 0
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "compareTo", Arguments(other)) {
scope.raiseNotImplemented("compareTo for ${objClass.className}")
}.cast<ObjInt>(scope).toInt()
}
open suspend fun equals(scope: Scope, other: Obj): Boolean {
if (other === this) return true
val m = objClass.getInstanceMemberOrNull("equals") ?: scope.findExtension(objClass, "equals")
if (m != null) {
return invokeInstanceMethod(scope, "equals", Arguments(other)).toBool()
}
return try {
compareTo(scope, other) == 0
} catch (e: ExecutionError) {
@ -225,46 +231,66 @@ open class Obj {
* Class of the object: definition of member functions (top-level), etc.
* Note that using lazy allows to avoid endless recursion here
*/
open val objClass: ObjClass = rootObjectType
open val objClass: ObjClass get() = rootObjectType
open suspend fun plus(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "plus", Arguments(other)) {
scope.raiseNotImplemented("plus for ${objClass.className}")
}
}
open suspend fun minus(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "minus", Arguments(other)) {
scope.raiseNotImplemented("minus for ${objClass.className}")
}
}
open suspend fun negate(scope: Scope): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "negate", Arguments.EMPTY) {
scope.raiseNotImplemented("negate for ${objClass.className}")
}
}
open suspend fun mul(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "mul", Arguments(other)) {
scope.raiseNotImplemented("mul for ${objClass.className}")
}
}
open suspend fun div(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "div", Arguments(other)) {
scope.raiseNotImplemented("div for ${objClass.className}")
}
}
open suspend fun mod(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "mod", Arguments(other)) {
scope.raiseNotImplemented("mod for ${objClass.className}")
}
}
open suspend fun logicalNot(scope: Scope): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "logicalNot", Arguments.EMPTY) {
scope.raiseNotImplemented("logicalNot for ${objClass.className}")
}
}
open suspend fun logicalAnd(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "logicalAnd", Arguments(other)) {
scope.raiseNotImplemented("logicalAnd for ${objClass.className}")
}
}
open suspend fun logicalOr(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "logicalOr", Arguments(other)) {
scope.raiseNotImplemented("logicalOr for ${objClass.className}")
}
}
open suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "operatorMatch", Arguments(other)) {
scope.raiseNotImplemented("operatorMatch for ${objClass.className}")
}
}
open suspend fun operatorNotMatch(scope: Scope, other: Obj): Obj {
@ -273,27 +299,39 @@ open class Obj {
// Bitwise ops default (override in numeric types that support them)
open suspend fun bitAnd(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "bitAnd", Arguments(other)) {
scope.raiseNotImplemented("bitAnd for ${objClass.className}")
}
}
open suspend fun bitOr(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "bitOr", Arguments(other)) {
scope.raiseNotImplemented("bitOr for ${objClass.className}")
}
}
open suspend fun bitXor(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "bitXor", Arguments(other)) {
scope.raiseNotImplemented("bitXor for ${objClass.className}")
}
}
open suspend fun shl(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "shl", Arguments(other)) {
scope.raiseNotImplemented("shl for ${objClass.className}")
}
}
open suspend fun shr(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "shr", Arguments(other)) {
scope.raiseNotImplemented("shr for ${objClass.className}")
}
}
open suspend fun bitNot(scope: Scope): Obj {
scope.raiseNotImplemented()
return invokeInstanceMethod(scope, "bitNot", Arguments.EMPTY) {
scope.raiseNotImplemented("bitNot for ${objClass.className}")
}
}
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
@ -305,15 +343,43 @@ open class Obj {
* if( the operation is not defined, it returns null and the compiler would try
* to generate it as 'this = this + other', reassigning its variable
*/
open suspend fun plusAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun plusAssign(scope: Scope, other: Obj): Obj? {
val m = objClass.getInstanceMemberOrNull("plusAssign") ?: scope.findExtension(objClass, "plusAssign")
return if (m != null) {
invokeInstanceMethod(scope, "plusAssign", Arguments(other))
} else null
}
/**
* `-=` operations, see [plusAssign]
*/
open suspend fun minusAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun mulAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun divAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun modAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun minusAssign(scope: Scope, other: Obj): Obj? {
val m = objClass.getInstanceMemberOrNull("minusAssign") ?: scope.findExtension(objClass, "minusAssign")
return if (m != null) {
invokeInstanceMethod(scope, "minusAssign", Arguments(other))
} else null
}
open suspend fun mulAssign(scope: Scope, other: Obj): Obj? {
val m = objClass.getInstanceMemberOrNull("mulAssign") ?: scope.findExtension(objClass, "mulAssign")
return if (m != null) {
invokeInstanceMethod(scope, "mulAssign", Arguments(other))
} else null
}
open suspend fun divAssign(scope: Scope, other: Obj): Obj? {
val m = objClass.getInstanceMemberOrNull("divAssign") ?: scope.findExtension(objClass, "divAssign")
return if (m != null) {
invokeInstanceMethod(scope, "divAssign", Arguments(other))
} else null
}
open suspend fun modAssign(scope: Scope, other: Obj): Obj? {
val m = objClass.getInstanceMemberOrNull("modAssign") ?: scope.findExtension(objClass, "modAssign")
return if (m != null) {
invokeInstanceMethod(scope, "modAssign", Arguments(other))
} else null
}
open suspend fun getAndIncrement(scope: Scope): Obj {
scope.raiseNotImplemented()

View File

@ -23,7 +23,7 @@ import net.sergeych.lynon.BitArray
class ObjBitBuffer(val bitArray: BitArray) : Obj() {
override val objClass = type
override val objClass get() = type
override suspend fun getAt(scope: Scope, index: Obj): Obj {
return bitArray[index.toLong()].toObj()

View File

@ -37,7 +37,7 @@ data class ObjBool(val value: Boolean) : Obj() {
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
override suspend fun logicalNot(scope: Scope): Obj = ObjBool(!value)

View File

@ -34,7 +34,7 @@ import kotlin.math.min
open class ObjBuffer(val byteArray: UByteArray) : Obj() {
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
val hex by lazy { byteArray.encodeToHex("")}
val base64 by lazy { byteArray.toByteArray().encodeToBase64Url()}

View File

@ -23,7 +23,7 @@ import net.sergeych.lyng.miniast.type
class ObjChar(val value: Char): Obj() {
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
override suspend fun compareTo(scope: Scope, other: Obj): Int =
(other as? ObjChar)?.let { value.compareTo(it.value) } ?: -1

View File

@ -25,7 +25,7 @@ import net.sergeych.lyng.miniast.type
class ObjCompletableDeferred(val completableDeferred: CompletableDeferred<Obj>): ObjDeferred(completableDeferred) {
override val objClass = type
override val objClass get() = type
companion object {
val type = object: ObjClass("CompletableDeferred", ObjDeferred.type){

View File

@ -24,7 +24,7 @@ import net.sergeych.lyng.miniast.type
open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
override val objClass = type
override val objClass get() = type
companion object {
val type = object: ObjClass("Deferred"){

View File

@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
class ObjDuration(val duration: Duration) : Obj() {
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
override fun toString(): String {
return duration.toString()

View File

@ -23,7 +23,7 @@ import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
companion object {
val type = ObjClass("DelegateContext").apply {
@ -54,7 +54,7 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
*/
open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: Statement? = null) : Obj() {
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
// Capture the lexical scope used to build this dynamic so callbacks can see outer locals
internal var builderScope: Scope? = null

View File

@ -35,7 +35,7 @@ import kotlin.coroutines.cancellation.CancellationException
class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
override val objClass = type
override val objClass get() = type
companion object {
@OptIn(DelicateCoroutinesApi::class)
@ -91,7 +91,7 @@ private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChann
class ObjFlow(val producer: Statement, val scope: Scope) : Obj() {
override val objClass = type
override val objClass get() = type
companion object {
val type = object : ObjClass("Flow", ObjIterable) {
@ -119,7 +119,7 @@ class ObjFlow(val producer: Statement, val scope: Scope) : Obj() {
class ObjFlowIterator(val producer: Statement) : Obj() {
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
private var channel: ReceiveChannel<Obj>? = null

View File

@ -42,9 +42,6 @@ class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(na
}
init {
addFn("toString", true) {
thisObj.toString(this, true)
}
}
}

View File

@ -29,7 +29,7 @@ import net.sergeych.lyng.Scope
*/
class ObjKotlinIterator(val iterator: Iterator<Any?>) : Obj() {
override val objClass = type
override val objClass get() = type
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {
@ -46,7 +46,7 @@ class ObjKotlinIterator(val iterator: Iterator<Any?>) : Obj() {
*/
class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
override val objClass = type
override val objClass get() = type
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {

View File

@ -56,7 +56,7 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
return ObjString("(${key.toString(scope).value} => ${value.toString(scope).value})")
}
override val objClass = type
override val objClass get() = type
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAny(scope,key)
@ -106,7 +106,7 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override val objClass = type
override val objClass get() = type
override suspend fun getAt(scope: Scope, index: Obj): Obj =
map.get(index) ?: ObjNull

View File

@ -26,7 +26,7 @@ import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
class ObjMutex(val mutex: Mutex): Obj() {
override val objClass = type
override val objClass get() = type
companion object {
val type = object: ObjClass("Mutex") {

View File

@ -27,7 +27,7 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
val isOpenStart by lazy { start == null || start.isNull }
val isOpenEnd by lazy { end == null || end.isNull }
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
override suspend fun defaultToString(scope: Scope): ObjString {
val result = StringBuilder()

View File

@ -26,7 +26,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
private var lastIndex = 0
private var isCharRange: Boolean = false
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
fun Scope.init() {
val s = self.start
@ -84,7 +84,7 @@ class ObjFastIntRangeIterator(private val start: Int, private val endExclusive:
private var cur: Int = start
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
fun hasNext(): Boolean = cur < endExclusive

View File

@ -37,7 +37,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override val toObjInt: ObjInt get() = ObjInt.of(longValue)
override val toObjReal: ObjReal get() = this
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
override fun byValueCopy(): Obj = this

View File

@ -26,7 +26,7 @@ import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
class ObjRegex(val regex: Regex) : Obj() {
override val objClass = type
override val objClass get() = type
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
return regex.find(other.cast<ObjString>(scope).value)?.let {
@ -81,7 +81,7 @@ class ObjRegex(val regex: Regex) : Obj() {
}
class ObjRegexMatch(val match: MatchResult) : Obj() {
override val objClass = type
override val objClass get() = type
val objGroups: ObjList by lazy {
// Use groupValues so that index 0 is the whole match and subsequent indices are capturing groups,

View File

@ -81,7 +81,7 @@ class RingBuffer<T>(val maxSize: Int) : Iterable<T> {
class ObjRingBuffer(val capacity: Int) : Obj() {
val buffer = RingBuffer<Obj>(capacity)
override val objClass: ObjClass = type
override val objClass: ObjClass get() = type
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
buffer.add(other.byValueCopy())

View File

@ -28,7 +28,7 @@ import net.sergeych.lynon.LynonType
class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override val objClass = type
override val objClass get() = type
override suspend fun contains(scope: Scope, other: Obj): Boolean {
return set.contains(other)

View File

@ -4386,7 +4386,7 @@ class ScriptTest {
"""
class A(x,y)
class B(x,y) {
fun toString() {
override fun toString() {
"B(%d,%d)"(x,y)
}
}
@ -4394,7 +4394,7 @@ class ScriptTest {
assertEquals("B(1,2)", B(1,2).toString())
assertEquals("A(x=1,y=2)", A(1,2).toString())
// now tricky part: this _should_ cakk custom toString()
// now tricky part: this _should_ call custom toString()
assertEquals(":B(1,2)", ":" + B(1,2).toString())
// and this must be exactly same:
assertEquals(":B(1,2)", ":" + B(1,2))

View File

@ -235,7 +235,7 @@ fun Iterable.flatMap(transform): List {
}
/* Return string representation like [a,b,c]. */
fun List.toString() {
override fun List.toString() {
"[" + joinToString(",") + "]"
}
@ -257,7 +257,7 @@ class StackTraceEntry(
val sourceString: String
) {
/* Formatted representation: source:line:column: text. */
fun toString() {
override fun toString() {
"%s:%d:%d: %s"(sourceName, line, column, sourceString.trim())
}
}