fix #23 string formatting and manipulations

This commit is contained in:
sergeych committed 2025-06-14 11:53:18 +04:00
1 parent 1db1f12be3
commit 19eae213ec
13 files changed
+207 -40

No files matched your search

@@ -33,6 +33,13 @@ data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : L
return list.first()
}
/**
* Convert to list of kotlin objects, see [Obj.toKotlin].
*/
suspend fun toKotlinList(context: Context): List<Any?> {
return list.map { it.toKotlin(context) }
}
companion object {
val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.toList())
@@ -59,7 +59,7 @@ class Compiler(
parseBlock(cc)
}
Token.Type.RBRACE -> {
Token.Type.RBRACE, Token.Type.RBRACKET -> {
cc.previous()
return null
}
@@ -226,8 +226,7 @@ class Compiler(
val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression")
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
operand = Accessor({ cxt ->
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
?: cxt.raiseError("index must be integer")
val i = index.execute(cxt)
val x = left.getter(cxt).value
if( x == ObjNull && isOptional) ObjNull.asReadonly
else x.getAt(cxt, i).asMutable
@@ -341,10 +340,10 @@ class Compiler(
}
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> {
// closed-range operator
// range operator
val isEndInclusive = t.type == Token.Type.DOTDOT
val left = operand
val right = parseStatement(cc)
val right = parseExpression(cc)
operand = Accessor {
ObjRange(
left?.getter?.invoke(it)?.value ?: ObjNull,
@@ -366,11 +365,15 @@ class Compiler(
} ?: parseLambdaExpression(cc)
}
Token.Type.RBRACKET -> {
cc.previous()
return operand
}
else -> {
cc.previous()
operand?.let { return it }
operand = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression")
operand = parseAccessor(cc) ?: return null //throw ScriptError(t.pos, "Expecting expression")
}
}
}
@@ -1115,7 +1118,7 @@ class Compiler(
var breakCaught = false
if (size > 0) {
var current = runCatching { sourceObj.getAt(forContext, 0) }
var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) }
.getOrElse {
throw ScriptError(
tOp.pos,
@@ -1142,7 +1145,7 @@ class Compiler(
}
} else result = body.execute(forContext)
if (++index >= size) break
current = sourceObj.getAt(forContext, index)
current = sourceObj.getAt(forContext, ObjInt(index.toLong()))
}
}
if (!breakCaught && elseStatement != null) {
@@ -42,6 +42,9 @@ data class Accessor(
}
open class Obj {
val isNull by lazy { this === ObjNull }
var isFrozen: Boolean = false
private val monitor = Mutex()
@@ -176,6 +179,13 @@ open class Obj {
context.raiseNotImplemented()
}
/**
* Convert Lyng object to its Kotlin counterpart
*/
open suspend fun toKotlin(context: Context): Any? {
return toString()
}
fun willMutate(context: Context) {
if (isFrozen) context.raiseError("attempt to mutate frozen object")
}
@@ -201,7 +211,7 @@ open class Obj {
if (field.isMutable) field.value = newValue else context.raiseError("can't assign to read-only field: $name")
}
open suspend fun getAt(context: Context, index: Int): Obj {
open suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNotImplemented("indexing")
}
@@ -297,7 +307,7 @@ object ObjNull : Obj() {
context.raiseNPE()
}
override suspend fun getAt(context: Context, index: Int): Obj {
override suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNPE()
}
@@ -310,6 +320,10 @@ object ObjNull : Obj() {
}
override fun toString(): String = "null"
override suspend fun toKotlin(context: Context): Any? {
return null
}
}
interface Numeric {
@@ -18,6 +18,10 @@ data class ObjBool(val value: Boolean) : Obj() {
override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun toKotlin(context: Context): Any {
return value
}
companion object {
val type = ObjClass("Bool")
}
@@ -162,7 +162,7 @@ val ObjArray by lazy {
addFn("contains", isOpen = true) {
val obj = args.firstAndOnly()
for( i in 0..< thisObj.invokeInstanceMethod(this, "size").toInt()) {
if( thisObj.getAt(this, i).compareTo(this, obj) == 0 ) return@addFn ObjTrue
if( thisObj.getAt(this, ObjInt(i.toLong())).compareTo(this, obj) == 0 ) return@addFn ObjTrue
}
ObjFalse
}
@@ -72,6 +72,10 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
} else null
}
override suspend fun toKotlin(context: Context): Any {
return value
}
companion object {
val type = ObjClass("Int")
}
@@ -11,15 +11,15 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
list.joinToString(separator = ", ") { it.inspect() }
}]"
fun normalize(context: Context, index: Int, allowisEndInclusive: Boolean = false): Int {
fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
val i = if (index < 0) list.size + index else index
if (allowisEndInclusive && i == list.size) return i
if (allowsEndInclusive && i == list.size) return i
if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}")
return i
}
override suspend fun getAt(context: Context, index: Int): Obj {
val i = normalize(context, index)
override suspend fun getAt(context: Context, index: Obj): Obj {
val i = normalize(context, index.toInt())
return list[i]
}
@@ -82,6 +82,10 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
override val objClass: ObjClass
get() = type
override suspend fun toKotlin(context: Context): Any {
return list.map { it.toKotlin(context) }
}
companion object {
val type = ObjClass("List", ObjArray).apply {
@@ -92,7 +96,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
)
addFn("getAt") {
requireExactCount(1)
thisAs<ObjList>().getAt(this, requiredArg<ObjInt>(0).value.toInt())
thisAs<ObjList>().getAt(this, requiredArg<Obj>(0))
}
addFn("putAt") {
requireExactCount(2)
@@ -113,7 +117,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
val l = thisAs<ObjList>()
var index = l.normalize(
this, requiredArg<ObjInt>(0).value.toInt(),
allowisEndInclusive = true
allowsEndInclusive = true
)
for (i in 1..<args.size) l.list.add(index++, args[i])
ObjVoid
@@ -36,6 +36,13 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override suspend fun mod(context: Context, other: Obj): Obj =
ObjReal(this.value % other.toDouble())
/**
* Returns unboxed Double value
*/
override suspend fun toKotlin(context: Context): Any {
return value
}
companion object {
val type: ObjClass = ObjClass("Real").apply {
createField(
@@ -2,11 +2,20 @@ package net.sergeych.lyng
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.sprintf.sprintf
@Serializable
@SerialName("string")
data class ObjString(val value: String) : Obj() {
// fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
// val i = if (index < 0) value.length + index else index
// if (allowsEndInclusive && i == value.length) return i
// if (i !in value.indices) context.raiseError("index $index out of bounds for length ${value.length} of \"$value\"")
// return i
// }
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjString) return -2
return this.value.compareTo(other.value)
@@ -27,8 +36,21 @@ data class ObjString(val value: String) : Obj() {
return ObjString(value + other.asStr.value)
}
override suspend fun getAt(context: Context, index: Int): Obj {
return ObjChar(value[index])
override suspend fun getAt(context: Context, index: Obj): Obj {
if( index is ObjInt ) return ObjChar(value[index.toInt()])
if( index is ObjRange ) {
val start = if(index.start == null || index.start.isNull) 0 else index.start.toInt()
val end = if( index.end == null || index.end.isNull ) value.length else {
val e = index.end.toInt()
if( index.isEndInclusive) e + 1 else e
}
return ObjString(value.substring(start, end))
}
context.raiseArgumentError("String index must be Int or Range")
}
override suspend fun callOn(context: Context): Obj {
return ObjString(this.value.sprintf(*context.args.toKotlinList(context).toTypedArray()))
}
override suspend fun contains(context: Context, other: Obj): Boolean {
@@ -41,10 +63,12 @@ data class ObjString(val value: String) : Obj() {
companion object {
val type = ObjClass("String").apply {
addConst("startsWith",
statement {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
})
addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
}
addFn("endsWith") {
ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
}
addConst("length",
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
)