fix #58 improper toString overload processing in Lyng

fix #57 Exception#getStackTrace()
ref #56 StackTraceEntry is serializable now
This commit is contained in:
Sergey Chernov 2025-08-22 00:01:59 +03:00
parent 8d1cafae80
commit 4b613fda7c
30 changed files with 250 additions and 119 deletions

View File

@ -87,7 +87,7 @@ You can use Char as both ends of the closed range:
Exclusive end char ranges are supported too: Exclusive end char ranges are supported too:
('a'..<'c').toList ('a'..<'c').toList
>>> ['a', 'b'] >>> [a,b]
# Instance members # Instance members

View File

@ -83,7 +83,7 @@ or whatever implementing [Iterable], is called _splats_. Here is how we use it:
} }
val array = [1,2,3] val array = [1,2,3]
testSplat("start", ...array, "end") testSplat("start", ...array, "end")
>>> ["start", 1, 2, 3, "end"] >>> [start,1,2,3,end]
>>> void >>> void
There could be any number of splats at any positions. You can splat any other [Iterable] type: There could be any number of splats at any positions. You can splat any other [Iterable] type:
@ -93,7 +93,7 @@ There could be any number of splats at any positions. You can splat any other [I
} }
val range = 1..3 val range = 1..3
testSplat("start", ...range, "end") testSplat("start", ...range, "end")
>>> ["start", 1, 2, 3, "end"] >>> [start,1,2,3,end]
>>> void >>> void

View File

@ -183,8 +183,8 @@ Important difference from the channels or like, every time you collect the flow,
// and again: // and again:
assertEquals( result, f.toList() ) assertEquals( result, f.toList() )
>>> ["start", 1, 2, 3, 4] >>> [start,1,2,3,4]
>>> ["start", 1, 2, 3, 4] >>> [start,1,2,3,4]
>>> void >>> void
Notice that flow's lambda is not called until actual collection is started. Cold flows are Notice that flow's lambda is not called until actual collection is started. Cold flows are

View File

@ -512,7 +512,7 @@ Of course, you can splat from anything that is List (or list-like, but it will b
val a = ["one", "two"] val a = ["one", "two"]
val b = [10.1, 20.2] val b = [10.1, 20.2]
["start", ...b, ...a, "end"] ["start", ...b, ...a, "end"]
>>> ["start", 10.1, 20.2, "one", "two", "end"] >>> [start,10.1,20.2,one,two,end]
Of course, you can set any list element: Of course, you can set any list element:

View File

@ -63,7 +63,7 @@ data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) :
return list.map { it.toKotlin(scope) } return list.map { it.toKotlin(scope) }
} }
fun inspect(): String = list.joinToString(", ") { it.inspect() } suspend fun inspect(scope: Scope): String = list.map{ it.inspect(scope)}.joinToString(",")
companion object { companion object {
val EMPTY = Arguments(emptyList()) val EMPTY = Arguments(emptyList())

View File

@ -1363,8 +1363,8 @@ class Compiler(
} }
return statement(body.pos) { ctx -> return statement(body.pos) { cxt ->
val forContext = ctx.copy(start) val forContext = cxt.copy(start)
// loop var: StoredObject // loop var: StoredObject
val loopSO = forContext.addItem(tVar.value, true, ObjNull) val loopSO = forContext.addItem(tVar.value, true, ObjNull)
@ -1393,7 +1393,7 @@ class Compiler(
.getOrElse { .getOrElse {
throw ScriptError( throw ScriptError(
tOp.pos, tOp.pos,
"object is not enumerable: no index access for ${sourceObj.inspect()}", "object is not enumerable: no index access for ${sourceObj.inspect(cxt)}",
it it
) )
} }
@ -1420,7 +1420,7 @@ class Compiler(
} }
} }
if (!breakCaught && elseStatement != null) { if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(ctx) result = elseStatement.execute(cxt)
} }
result result
} }

View File

@ -57,15 +57,15 @@ class Script(
ObjException.addExceptionsToContext(this) ObjException.addExceptionsToContext(this)
addFn("print") { addFn("print") {
for ((i, a) in args.withIndex()) { for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value) if (i > 0) print(' ' + a.toString(this).value)
else print(a.asStr.value) else print(a.toString(this).value)
} }
ObjVoid ObjVoid
} }
addFn("println") { addFn("println") {
for ((i, a) in args.withIndex()) { for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value) if (i > 0) print(' ' + a.toString(this).value)
else print(a.asStr.value) else print(a.toString(this).value)
} }
println() println()
ObjVoid ObjVoid
@ -165,13 +165,13 @@ class Script(
val a = requiredArg<Obj>(0) val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1) val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 ) if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}")) raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
} }
addVoidFn("assertNotEquals") { addVoidFn("assertNotEquals") {
val a = requiredArg<Obj>(0) val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1) val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) == 0 ) if( a.compareTo(this, b) == 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} != ${b.inspect()}")) raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} != ${b.inspect(this)}"))
} }
addFn("assertThrows") { addFn("assertThrows") {
val code = requireOnlyArg<Statement>() val code = requireOnlyArg<Statement>()
@ -287,7 +287,7 @@ class Script(
is ObjInt -> delay(a.value * 1000) is ObjInt -> delay(a.value * 1000)
is ObjReal -> delay((a.value * 1000).roundToLong()) is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration) is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect()}") else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect(this)}")
} }
ObjVoid ObjVoid
} }

View File

@ -17,10 +17,14 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjString
class Source(val fileName: String, text: String) { class Source(val fileName: String, text: String) {
val lines = text.lines().map { it.trimEnd() } val lines = text.lines().map { it.trimEnd() }
val objSourceName by lazy { ObjString(fileName) }
companion object { companion object {
val builtIn: Source by lazy { Source("built-in", "") } val builtIn: Source by lazy { Source("built-in", "") }
val UNKNOWN: Source by lazy { Source("UNKNOWN", "") } val UNKNOWN: Source by lazy { Source("UNKNOWN", "") }

View File

@ -26,6 +26,7 @@ import net.sergeych.lyng.*
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
import net.sergeych.mptools.CachedExpression
import net.sergeych.synctools.ProtectedOp import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
@ -49,7 +50,7 @@ open class Obj {
private val opInstances = ProtectedOp() private val opInstances = ProtectedOp()
open fun inspect(): String = toString() open suspend fun inspect(scope: Scope): String = toString(scope).value
/** /**
* Some objects are by-value, historically [ObjInt] and [ObjReal] are usually treated as such. * Some objects are by-value, historically [ObjInt] and [ObjReal] are usually treated as such.
@ -84,14 +85,14 @@ open class Obj {
scope: Scope, scope: Scope,
name: String, name: String,
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null onNotFoundResult: (()->Obj?)? = null
): Obj { ): Obj {
return objClass.getInstanceMemberOrNull(name)?.value?.invoke( return objClass.getInstanceMemberOrNull(name)?.value?.invoke(
scope, scope,
this, this,
args args
) )
?: onNotFoundResult ?: onNotFoundResult?.invoke()
?: scope.raiseSymbolNotFound(name) ?: scope.raiseSymbolNotFound(name)
} }
@ -115,8 +116,11 @@ open class Obj {
return invokeInstanceMethod(scope, "contains", other).toBool() return invokeInstanceMethod(scope, "contains", other).toBool()
} }
open val asStr: ObjString by lazy { suspend open fun toString(scope: Scope): ObjString {
if (this is ObjString) this else ObjString(this.toString()) return if (this is ObjString) this
else invokeInstanceMethod(scope, "toString") {
ObjString(this.toString())
} as ObjString
} }
/** /**
@ -287,11 +291,11 @@ open class Obj {
companion object { companion object {
val rootObjectType = ObjClass("Obj").apply { val rootObjectType = ObjClass("Obj").apply {
addFn("toString") { addFn("toString", true) {
thisObj.asStr ObjString(thisObj.toString())
} }
addFn("inspect", true) { addFn("inspect", true) {
thisObj.inspect().toObj() thisObj.inspect(this).toObj()
} }
addFn("contains") { addFn("contains") {
ObjBool(thisObj.contains(this, args.firstAndOnly())) ObjBool(thisObj.contains(this, args.firstAndOnly()))
@ -392,9 +396,14 @@ object ObjNull : Obj() {
scope.raiseNPE() scope.raiseNPE()
} }
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: Obj?): Obj { // override suspend fun invokeInstanceMethod(
scope.raiseNPE() // scope: Scope,
} // name: String,
// args: Arguments,
// onNotFoundResult: (()->Obj?)?
// ): Obj {
// scope.raiseNPE()
// }
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
scope.raiseNPE() scope.raiseNPE()
@ -469,20 +478,51 @@ fun Obj.toBool(): Boolean =
data class ObjNamespace(val name: String) : Obj() { data class ObjNamespace(val name: String) : Obj() {
override val objClass by lazy { ObjClass(name) } override val objClass by lazy { ObjClass(name) }
override fun inspect(): String = "Ns[$name]" override suspend fun inspect(scope: Scope): String = "Ns[$name]"
override fun toString(): String { override fun toString(): String {
return "package $name" return "package $name"
} }
} }
open class ObjException(exceptionClass: ExceptionClass, val scope: Scope, val message: String) : Obj() { open class ObjException(
val exceptionClass: ExceptionClass,
val scope: Scope,
val message: String,
@Suppress("unused") val extraData: Obj = ObjNull
) : Obj() {
constructor(name: String, scope: Scope, message: String) : this( constructor(name: String, scope: Scope, message: String) : this(
getOrCreateExceptionClass(name), getOrCreateExceptionClass(name),
scope, scope,
message message
) )
private val cachedStackTrace = CachedExpression<ObjList>()
suspend fun getStackTrace(): ObjList {
return cachedStackTrace.get {
val result = ObjList()
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
var s: Scope? = scope
var lastPos: Pos? = null
while( s != null ) {
val pos = s.pos
if( pos != lastPos && !pos.currentLine.isEmpty() ) {
result.list += cls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
}
s = s.parent
lastPos = pos
}
result
}
}
constructor(scope: Scope, message: String) : this(Root, scope, message) constructor(scope: Scope, message: String) : this(Root, scope, message)
fun raise(): Nothing { fun raise(): Nothing {
@ -495,6 +535,12 @@ open class ObjException(exceptionClass: ExceptionClass, val scope: Scope, val me
return "ObjException:${objClass.className}:${scope.pos}@${hashCode().encodeToHex()}" return "ObjException:${objClass.className}:${scope.pos}@${hashCode().encodeToHex()}"
} }
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAny(scope, ObjString(exceptionClass.name))
encoder.encodeAny(scope, ObjString(message))
}
companion object { companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) { class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
@ -510,6 +556,9 @@ open class ObjException(exceptionClass: ExceptionClass, val scope: Scope, val me
addConst("message", statement { addConst("message", statement {
(thisObj as ObjException).message.toObj() (thisObj as ObjException).message.toObj()
}) })
addFn("getStackTrace") {
(thisObj as ObjException).getStackTrace()
}
} }
private val op = ProtectedOp() private val op = ProtectedOp()

View File

@ -50,6 +50,5 @@ val ObjArray by lazy {
addFn("indices") { addFn("indices") {
ObjRange(0.toObj(), thisObj.invokeInstanceMethod(this, "size"), false) ObjRange(0.toObj(), thisObj.invokeInstanceMethod(this, "size"), false)
} }
} }
} }

View File

@ -23,7 +23,6 @@ import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
data class ObjBool(val value: Boolean) : Obj() { data class ObjBool(val value: Boolean) : Obj() {
override val asStr by lazy { ObjString(value.toString()) }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjBool) return -2 if (other !is ObjBool) return -2

View File

@ -87,7 +87,7 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray() byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
.toUByteArray() .toUByteArray()
) )
} else scope.raiseIllegalArgument("can't concatenate buffer with ${other.inspect()}") } else scope.raiseIllegalArgument("can't concatenate buffer with ${other.inspect(scope)}")
} }
override fun toString(): String = base64 override fun toString(): String = base64
@ -107,7 +107,7 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
encoder.encodeCachedBytes(byteArray.asByteArray()) encoder.encodeCachedBytes(byteArray.asByteArray())
} }
override fun inspect(): String = "Buf($base64)" override suspend fun inspect(scope: Scope): String = "Buf($base64)"
companion object { companion object {
private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer = private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer =
@ -129,7 +129,7 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
) )
} else } else
scope.raiseIllegalArgument( scope.raiseIllegalArgument(
"can't construct buffer from ${obj.inspect()}" "can't construct buffer from ${obj.inspect(scope)}"
) )
} }
} }
@ -149,7 +149,7 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
is ObjChar -> b.value.code.toUByte() is ObjChar -> b.value.code.toUByte()
is ObjInt -> b.value.toUByte() is ObjInt -> b.value.toUByte()
else -> scope.raiseIllegalArgument( else -> scope.raiseIllegalArgument(
"invalid byte value for buffer constructor at index $i: ${b.inspect()}" "invalid byte value for buffer constructor at index $i: ${b.inspect(scope)}"
) )
} }
data[i] = code data[i] = code

View File

@ -28,7 +28,7 @@ class ObjChar(val value: Char): Obj() {
override fun toString(): String = value.toString() override fun toString(): String = value.toString()
override fun inspect(): String = "'$value'" override suspend fun inspect(scope: Scope): String = "'$value'"
override fun hashCode(): Int { override fun hashCode(): Int {
return value.hashCode() return value.hashCode()

View File

@ -68,6 +68,10 @@ open class ObjClass(
return instance return instance
} }
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
return callOn(scope.copy(Arguments(*plainArgs)))
}
fun createField( fun createField(
name: String, name: String,
@ -140,13 +144,16 @@ open class ObjClass(
?: super.writeField(scope, name, newValue) ?: super.writeField(scope, name, newValue)
} }
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, override suspend fun invokeInstanceMethod(
onNotFoundResult: Obj?): Obj { scope: Scope, name: String, args: Arguments,
onNotFoundResult: (() -> Obj?)?
): Obj {
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args) return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult) ?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
} }
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = scope.raiseNotImplemented() open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
scope.raiseNotImplemented()
} }

View File

@ -57,7 +57,7 @@ class ObjDuration(val duration: Duration) : Obj() {
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
val args = scope.args val args = scope.args
if( args.list.size > 1 ) if( args.list.size > 1 )
scope.raiseIllegalArgument("can't construct Duration(${args.inspect()})") scope.raiseIllegalArgument("can't construct Duration(${args.inspect(scope)})")
val a0 = args.list.getOrNull(0) val a0 = args.list.getOrNull(0)
return ObjDuration( return ObjDuration(
@ -66,7 +66,7 @@ class ObjDuration(val duration: Duration) : Obj() {
is ObjInt -> a0.value.seconds is ObjInt -> a0.value.seconds
is ObjReal -> a0.value.seconds is ObjReal -> a0.value.seconds
else -> { else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})") scope.raiseIllegalArgument("can't construct Instant(${args.inspect(scope)})")
} }
} }
) )

View File

@ -48,7 +48,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj = onNotFoundResult: (()->Obj?)?): Obj =
instanceScope[name]?.let { instanceScope[name]?.let {
if (it.visibility.isPublic) if (it.visibility.isPublic)
it.value.invoke( it.value.invoke(

View File

@ -35,7 +35,14 @@ class ObjInstanceClass(val name: String) : ObjClass(name) {
val newScope = scope.copy(args = Arguments(args)) val newScope = scope.copy(args = Arguments(args))
return (callOn(newScope) as ObjInstance).apply { return (callOn(newScope) as ObjInstance).apply {
deserializeStateVars(scope, decoder) deserializeStateVars(scope, decoder)
invokeInstanceMethod(scope, "onDeserialized", onNotFoundResult = ObjVoid) invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
}
}
init {
addFn("toString", true) {
println("-------------- tos! --------------")
ObjString(thisObj.toString())
} }
} }

View File

@ -117,7 +117,7 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
is ObjInstant -> a0.instant is ObjInstant -> a0.instant
else -> { else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})") scope.raiseIllegalArgument("can't construct Instant(${args.inspect(scope)})")
} }
} }
) )

View File

@ -23,7 +23,6 @@ import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric { class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
override val asStr get() = ObjString(value.toString())
override val longValue get() = value override val longValue get() = value
override val doubleValue get() = value.toDouble() override val doubleValue get() = value.toDouble()
override val toObjInt get() = this override val toObjInt get() = this

View File

@ -95,5 +95,5 @@ suspend fun Obj.enumerate(scope: Scope,callback: suspend (Obj)->Boolean) {
} }
} }
if (closeIt) if (closeIt)
iterator.invokeInstanceMethod(scope, "cancelIteration", onNotFoundResult = ObjVoid) iterator.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
} }

View File

@ -25,10 +25,6 @@ import net.sergeych.lynon.LynonType
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() { class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
override fun toString(): String = "[${
list.joinToString(separator = ", ") { it.inspect() }
}]"
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
return when (index) { return when (index) {
is ObjInt -> { is ObjInt -> {
@ -65,7 +61,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
} }
} }
else -> scope.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}") else -> scope.raiseIllegalArgument("Illegal index object for a list: ${index.inspect(scope)}")
} }
} }

View File

@ -28,7 +28,7 @@ class ObjMutableBuffer(byteArray: UByteArray) : ObjBuffer(byteArray) {
is ObjInt -> newValue.value.toUByte() is ObjInt -> newValue.value.toUByte()
is ObjChar -> newValue.value.code.toUByte() is ObjChar -> newValue.value.code.toUByte()
else -> scope.raiseIllegalArgument( else -> scope.raiseIllegalArgument(
"invalid byte value for buffer at index ${index.inspect()}: ${newValue.inspect()}" "invalid byte value for buffer at index ${index.inspect(scope)}: ${newValue.inspect(scope)}"
) )
} }
} }
@ -54,7 +54,7 @@ class ObjMutableBuffer(byteArray: UByteArray) : ObjBuffer(byteArray) {
) )
} else } else
scope.raiseIllegalArgument( scope.raiseIllegalArgument(
"can't construct buffer from ${obj.inspect()}" "can't construct buffer from ${obj.inspect(scope)}"
) )
} }
} }
@ -74,7 +74,7 @@ class ObjMutableBuffer(byteArray: UByteArray) : ObjBuffer(byteArray) {
is ObjChar -> b.value.code.toUByte() is ObjChar -> b.value.code.toUByte()
is ObjInt -> b.value.toUByte() is ObjInt -> b.value.toUByte()
else -> scope.raiseIllegalArgument( else -> scope.raiseIllegalArgument(
"invalid byte value for buffer constructor at index $i: ${b.inspect()}" "invalid byte value for buffer constructor at index $i: ${b.inspect(scope)}"
) )
} }
data[i] = code data[i] = code

View File

@ -26,12 +26,12 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
override val objClass: ObjClass = type override val objClass: ObjClass = type
override fun toString(): String { override suspend fun toString(scope: Scope): ObjString {
val result = StringBuilder() val result = StringBuilder()
result.append("${start?.inspect() ?: '∞'} ..") result.append("${start?.inspect(scope) ?: '∞'} ..")
if (!isEndInclusive) result.append('<') if (!isEndInclusive) result.append('<')
result.append(" ${end?.inspect() ?: '∞'}") result.append(" ${end?.inspect(scope) ?: '∞'}")
return result.toString() return ObjString(result.toString())
} }
/** /**
@ -52,11 +52,11 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
* if start is not ObjInt, raises [ObjIllegalArgumentException] * if start is not ObjInt, raises [ObjIllegalArgumentException]
* otherwise returns start.value.toInt() * otherwise returns start.value.toInt()
*/ */
fun startInt(scope: Scope): Int = suspend fun startInt(scope: Scope): Int =
if( start == null || start is ObjNull) 0 if( start == null || start is ObjNull) 0
else { else {
if( start is ObjInt) start.value.toInt() if( start is ObjInt) start.value.toInt()
else scope.raiseIllegalArgument("start is not Int: ${start.inspect()}") else scope.raiseIllegalArgument("start is not Int: ${start.inspect(scope)}")
} }
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean { suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {

View File

@ -27,7 +27,6 @@ import kotlin.math.floor
import kotlin.math.roundToLong import kotlin.math.roundToLong
data class ObjReal(val value: Double) : Obj(), Numeric { data class ObjReal(val value: Double) : Obj(), Numeric {
override val asStr by lazy { ObjString(value.toString()) }
override val longValue: Long by lazy { floor(value).toLong() } override val longValue: Long by lazy { floor(value).toLong() }
override val doubleValue: Double by lazy { value } override val doubleValue: Double by lazy { value }
override val toObjInt: ObjInt by lazy { ObjInt(longValue) } override val toObjInt: ObjInt by lazy { ObjInt(longValue) }

View File

@ -44,9 +44,7 @@ data class ObjString(val value: String) : Obj() {
override fun toString(): String = value override fun toString(): String = value
override val asStr: ObjString by lazy { this } override suspend fun inspect(scope: Scope): String {
override fun inspect(): String {
return "\"$value\"" return "\"$value\""
} }
@ -54,7 +52,7 @@ data class ObjString(val value: String) : Obj() {
get() = type get() = type
override suspend fun plus(scope: Scope, other: Obj): Obj { override suspend fun plus(scope: Scope, other: Obj): Obj {
return ObjString(value + other.asStr.value) return ObjString(value + other.toString(scope).value)
} }
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
@ -159,6 +157,9 @@ data class ObjString(val value: String) : Obj() {
addFn("toReal") { addFn("toReal") {
ObjReal(thisAs<ObjString>().value.toDouble()) ObjReal(thisAs<ObjString>().value.toDouble())
} }
addFn("trim") {
thisAs<ObjString>().value.trim().let(::ObjString)
}
} }
} }
} }

View File

@ -104,5 +104,20 @@ fun Iterable.all(predicate): Bool {
!any { !predicate(it) } !any { !predicate(it) }
} }
fun List.toString() {
"[" + joinToString(",") + "]"
}
class StackTraceEntry(
val sourceName: String,
val line: Int,
val column: Int,
val sourceString: String
) {
fun toString() {
"%s:%d:%d: %s"(sourceName, line, column, sourceString.trim())
}
}
""".trimIndent() """.trimIndent()

View File

@ -2661,7 +2661,8 @@ class ScriptTest {
@Test @Test
fun testBufferEncodings() = runTest { fun testBufferEncodings() = runTest {
eval(""" eval(
"""
import lyng.buffer import lyng.buffer
val b = Buffer("hello") val b = Buffer("hello")
@ -2676,7 +2677,8 @@ class ScriptTest {
println(b.inspect()) println(b.inspect())
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@ -2900,7 +2902,8 @@ class ScriptTest {
@Test @Test
fun tesFunAnnotation() = runTest { fun tesFunAnnotation() = runTest {
eval(""" eval(
"""
val exportedSymbols = Map() val exportedSymbols = Map()
@ -2919,7 +2922,8 @@ class ScriptTest {
assert( exportedSymbols["getBalance"] != null ) assert( exportedSymbols["getBalance"] != null )
assertEquals(122, getBalance(1)) assertEquals(122, getBalance(1))
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@ -2939,12 +2943,14 @@ class ScriptTest {
assertEquals( Color.valueOf("GREEN"), Color.GREEN ) assertEquals( Color.valueOf("GREEN"), Color.GREEN )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun enumSerializationTest() = runTest { fun enumSerializationTest() = runTest {
eval(""" eval(
"""
import lyng.serialization import lyng.serialization
enum Color { enum Color {
@ -2960,12 +2966,14 @@ class ScriptTest {
assert( e1.size / 1000.0 < 6) assert( e1.size / 1000.0 < 6)
println(Lynon.encode( (1..100).map { "RED" } ).toDump() ) println(Lynon.encode( (1..100).map { "RED" } ).toDump() )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun cachedTest() = runTest { fun cachedTest() = runTest {
eval( """ eval(
"""
var counter = 0 var counter = 0
var value = cached { var value = cached {
@ -2978,66 +2986,82 @@ class ScriptTest {
assertEquals(1, counter) assertEquals(1, counter)
assertEquals("ok", value()) assertEquals("ok", value())
assertEquals(1, counter) assertEquals(1, counter)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testJoinToString() = runTest { fun testJoinToString() = runTest {
eval(""" eval(
"""
assertEquals( (1..3).joinToString(), "1 2 3") assertEquals( (1..3).joinToString(), "1 2 3")
assertEquals( (1..3).joinToString(":"), "1:2:3") assertEquals( (1..3).joinToString(":"), "1:2:3")
assertEquals( (1..3).joinToString { it * 10 }, "10 20 30") assertEquals( (1..3).joinToString { it * 10 }, "10 20 30")
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testElvisAndThrow() = runTest { fun testElvisAndThrow() = runTest {
eval(""" eval(
"""
val x = assertThrows { val x = assertThrows {
null ?: throw "test" + "x" null ?: throw "test" + "x"
} }
assertEquals( "testx", x.message) assertEquals( "testx", x.message)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testElvisAndThrow2() = runTest { fun testElvisAndThrow2() = runTest {
eval(""" eval(
"""
val t = "112" val t = "112"
val x = t ?: run { throw "testx" } val x = t ?: run { throw "testx" }
} }
assertEquals( "112", x) assertEquals( "112", x)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testElvisAndRunThrow() = runTest { fun testElvisAndRunThrow() = runTest {
eval(""" eval(
"""
val x = assertThrows { val x = assertThrows {
null ?: run { throw "testx" } null ?: run { throw "testx" }
} }
assertEquals( "testx", x.message) assertEquals( "testx", x.message)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testNewlinesAnsCommentsInExpressions() = runTest { fun testNewlinesAnsCommentsInExpressions() = runTest {
assertEquals( 2, (Scope().eval(""" assertEquals(
2, (Scope().eval(
"""
val e = 1 + 4 - val e = 1 + 4 -
3 3
""".trimIndent())).toInt()) """.trimIndent()
)).toInt()
)
eval(""" eval(
"""
val x = [1,2,3] val x = [1,2,3]
.map { it * 10 } .map { it * 10 }
.map { it + 1 } .map { it + 1 }
assertEquals( [11,21,31], x) assertEquals( [11,21,31], x)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testNotExpressionWithoutWs() = runTest { fun testNotExpressionWithoutWs() = runTest {
eval(""" eval(
"""
fun test() { false } fun test() { false }
class T(value) class T(value)
assert( !false ) assert( !false )
@ -3046,12 +3070,14 @@ class ScriptTest {
val t = T(false) val t = T(false)
assert( !t.value ) assert( !t.value )
assert( !if( true ) false else true ) assert( !if( true ) false else true )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testMultilineFnDeclaration() = runTest { fun testMultilineFnDeclaration() = runTest {
eval(""" eval(
"""
fun test( fun test(
x = 1, x = 1,
y = 2 y = 2
@ -3063,8 +3089,38 @@ class ScriptTest {
6, 6,
7 7
) ) ) )
""".trimIndent()
)
}
@Test
fun testOverridenListToString() = runTest {
eval("""
val x = [1,2,3]
assertEquals( "[1,2,3]", x.toString() )
""".trimIndent()) """.trimIndent())
} }
@Test
fun testExceptionSerialization() = runTest {
eval(
"""
import lyng.serialization
val x = [1,2,3]
assertEquals( "[1,2,3]", x.toString() )
try {
require(false)
}
catch (e) {
println(e)
println(e.getStackTrace())
for( t in e.getStackTrace() ) {
println(t)
}
// val coded = Lynon.encode(e)
// println(coded.toDump())
}
""".trimIndent()
)
}
} }

View File

@ -177,11 +177,11 @@ suspend fun DocTest.test(_scope: Scope? = null) {
scope.apply { scope.apply {
addFn("println") { addFn("println") {
if( bookMode ) { if( bookMode ) {
println("${currentTest.fileNamePart}:${currentTest.line}> ${args.joinToString(" "){it.asStr.value}}") println("${currentTest.fileNamePart}:${currentTest.line}> ${args.map{it.toString(this).value}.joinToString(" ")}")
} }
else { else {
for ((i, a) in args.withIndex()) { for ((i, a) in args.withIndex()) {
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.asStr.value) if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.toString(this).value)
collectedOutput.append('\n') collectedOutput.append('\n')
} }
} }
@ -194,7 +194,7 @@ suspend fun DocTest.test(_scope: Scope? = null) {
} catch (e: Throwable) { } catch (e: Throwable) {
error = e error = e
null null
}?.inspect()?.replace(Regex("@\\d+"), "@...") }?.inspect(scope)?.replace(Regex("@\\d+"), "@...")
if (bookMode) { if (bookMode) {
if (error != null) { if (error != null) {