fix #5 loop optimization

fixed arguments modifying bug
added samples and samplebooks w/tests
This commit is contained in:
Sergey Chernov 2025-06-03 10:44:42 +04:00
parent 53eb1bc5e7
commit 512dda5984
14 changed files with 311 additions and 53 deletions

View File

@ -0,0 +1,44 @@
# Sample combinatorics calculations
The trivial to start with, the factorialorial calculation:
fun factorial(n) {
if( n < 1 )
1
else {
var result = 1
var cnt = 2
while( cnt <= n ) result = result * cnt++
}
}
Let's test it:
assert(factorial(2) == 2)
assert(factorial(3) == 6)
assert(factorial(4) == 24)
assert(factorial(5) == 120)
Now let's calculate _combination_, or the polynomial coefficient $C^n_k$. It is trivial also, the formulae is:
$$C^n_k = \frac {n!} {k! (n-k)!} $$
We can simplify it a little, as $ n ≥ k $, we can remove $k!$ from the fraction:
$$C^n_k = \frac {(k+1)(k+1)...n} { (n-k)!} $$
Now the code is much more effective:
fun C(n,k) {
var result = k+1
var ck = result + 1
while( ck <= n ) result *= ck++
result / factorial(n-k)
}
println(C(10,3))
assert( C(10, 3) == 120 )
to be continued...

View File

@ -0,0 +1,19 @@
/*
Count "happy tickets": 6-digit numbers where sum of first three
digits is equal to those of last three. Just to demonstrate and
test the Lyng way. It is not meant to be effective.
*/
fun naiveCountHappyNumbers() {
var count = 0
for( n1 in 0..9 )
for( n2 in 0..9 )
for( n3 in 0..9 )
for( n4 in 0..9 )
for( n5 in 0..9 )
for( n6 in 0..9 )
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
count
}
assert( naiveCountHappyNumbers() == 55252 )

View File

@ -2,6 +2,7 @@
gitea: none gitea: none
include_toc: true include_toc: true
--- ---
# Lyng tutorial # Lyng tutorial
Lyng is a very simple language, where we take only most important and popular features from Lyng is a very simple language, where we take only most important and popular features from

View File

@ -5,12 +5,14 @@ android-minSdk = "24"
android-compileSdk = "34" android-compileSdk = "34"
kotlinx-coroutines = "1.10.1" kotlinx-coroutines = "1.10.1"
mp_bintools = "0.1.12" mp_bintools = "0.1.12"
firebaseCrashlyticsBuildtools = "3.0.3"
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" } mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
[plugins] [plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }

View File

@ -72,6 +72,9 @@ android {
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
} }
dependencies {
implementation(libs.firebase.crashlytics.buildtools)
}
mavenPublishing { mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

View File

@ -652,7 +652,6 @@ class Compiler(
val label = getLabel(cc)?.also { cc.labels += it } val label = getLabel(cc)?.also { cc.labels += it }
val start = ensureLparen(cc) val start = ensureLparen(cc)
// for - in?
val tVar = cc.next() val tVar = cc.next()
if (tVar.type != Token.Type.ID) if (tVar.type != Token.Type.ID)
throw ScriptError(tVar.pos, "Bad for statement: expected loop variable") throw ScriptError(tVar.pos, "Bad for statement: expected loop variable")
@ -661,8 +660,10 @@ class Compiler(
// in loop // in loop
val source = parseStatement(cc) ?: throw ScriptError(start, "Bad for statement: expected expression") val source = parseStatement(cc) ?: throw ScriptError(start, "Bad for statement: expected expression")
ensureRparen(cc) ensureRparen(cc)
val body = parseStatement(cc) ?: throw ScriptError(start, "Bad for statement: expected loop body")
val (canBreak, body) = cc.parseLoop {
parseStatement(cc) ?: throw ScriptError(start, "Bad for statement: expected loop body")
}
// possible else clause // possible else clause
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
@ -683,7 +684,7 @@ class Compiler(
val sourceObj = source.execute(forContext) val sourceObj = source.execute(forContext)
if (sourceObj.isInstanceOf(ObjIterable)) { if (sourceObj.isInstanceOf(ObjIterable)) {
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label) loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
} else { } else {
val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() } val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() }
.getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size", it) } .getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size", it) }
@ -703,19 +704,22 @@ class Compiler(
var index = 0 var index = 0
while (true) { while (true) {
loopSO.value = current loopSO.value = current
try { if (canBreak) {
result = body.execute(forContext) try {
} catch (lbe: LoopBreakContinueException) { result = body.execute(forContext)
if (lbe.label == label || lbe.label == null) { } catch (lbe: LoopBreakContinueException) {
breakCaught = true if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue breakCaught = true
else { if (lbe.doContinue) continue
result = lbe.result else {
break result = lbe.result
} break
} else }
throw lbe } else
throw lbe
}
} }
else result = body.execute(forContext)
if (++index >= size) break if (++index >= size) break
current = sourceObj.getAt(forContext, index) current = sourceObj.getAt(forContext, index)
} }
@ -734,19 +738,25 @@ class Compiler(
private suspend fun loopIterable( private suspend fun loopIterable(
forContext: Context, sourceObj: Obj, loopVar: StoredObj, forContext: Context, sourceObj: Obj, loopVar: StoredObj,
body: Statement, elseStatement: Statement?, label: String? body: Statement, elseStatement: Statement?, label: String?,
catchBreak: Boolean
): Obj { ): Obj {
val iterObj = sourceObj.invokeInstanceMethod(forContext, "iterator") val iterObj = sourceObj.invokeInstanceMethod(forContext, "iterator")
var result: Obj = ObjVoid var result: Obj = ObjVoid
while (iterObj.invokeInstanceMethod(forContext, "hasNext").toBool()) { while (iterObj.invokeInstanceMethod(forContext, "hasNext").toBool()) {
try { if (catchBreak)
try {
loopVar.value = iterObj.invokeInstanceMethod(forContext, "next")
result = body.execute(forContext)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
}
return lbe.result
}
else {
loopVar.value = iterObj.invokeInstanceMethod(forContext, "next") loopVar.value = iterObj.invokeInstanceMethod(forContext, "next")
result = body.execute(forContext) result = body.execute(forContext)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
}
return lbe.result
} }
} }
return elseStatement?.execute(forContext) ?: result return elseStatement?.execute(forContext) ?: result
@ -818,6 +828,8 @@ class Compiler(
parseStatement(cc) parseStatement(cc)
} else null } else null
cc.addBreak()
return statement(start) { return statement(start) {
val returnValue = resultExpr?.execute(it)// ?: ObjVoid val returnValue = resultExpr?.execute(it)// ?: ObjVoid
throw LoopBreakContinueException( throw LoopBreakContinueException(
@ -840,6 +852,7 @@ class Compiler(
// check that label is defined // check that label is defined
cc.ensureLabelIsValid(start, it) cc.ensureLabelIsValid(start, it)
} }
cc.addBreak()
return statement(start) { return statement(start) {
throw LoopBreakContinueException( throw LoopBreakContinueException(
@ -943,8 +956,10 @@ class Compiler(
val fnBody = statement(t.pos) { callerContext -> val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start callerContext.pos = start
// restore closure where the function was defined: // restore closure where the function was defined, and making a copy of it
val context = closure ?: callerContext.raiseError("bug: closure not set") // for local space (otherwise it will write local stuff to closure!)
val context = closure?.copy() ?: callerContext.raiseError("bug: closure not set")
// load params from caller context // load params from caller context
for ((i, d) in params.withIndex()) { for ((i, d) in params.withIndex()) {
if (i < callerContext.args.size) if (i < callerContext.args.size)

View File

@ -3,15 +3,31 @@ package net.sergeych.lyng
internal class CompilerContext(val tokens: List<Token>) { internal class CompilerContext(val tokens: List<Token>) {
val labels = mutableSetOf<String>() val labels = mutableSetOf<String>()
var breakFound = false
private set
var loopLevel = 0
private set
inline fun <T> parseLoop(f: () -> T): Pair<Boolean,T> {
if (++loopLevel == 0) breakFound = false
val result = f()
return Pair(breakFound, result).also {
--loopLevel
}
}
var currentIndex = 0 var currentIndex = 0
fun hasNext() = currentIndex < tokens.size fun hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0 fun hasPrevious() = currentIndex > 0
fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ } fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ }
fun previous() = if( !hasPrevious() ) throw IllegalStateException("No previous token") else tokens[--currentIndex] fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = currentIndex fun savePos() = currentIndex
fun restorePos(pos: Int) { currentIndex = pos } fun restorePos(pos: Int) {
currentIndex = pos
}
fun ensureLabelIsValid(pos: Pos, label: String) { fun ensureLabelIsValid(pos: Pos, label: String) {
if (label !in labels) if (label !in labels)
@ -42,7 +58,11 @@ internal class CompilerContext(val tokens: List<Token>) {
* @return `true` if the token was skipped * @return `true` if the token was skipped
* @throws ScriptError if [isOptional] is `false` and next token is not of [tokenType] * @throws ScriptError if [isOptional] is `false` and next token is not of [tokenType]
*/ */
fun skipTokenOfType(tokenType: Token.Type, errorMessage: String="expected ${tokenType.name}", isOptional: Boolean = false): Boolean { fun skipTokenOfType(
tokenType: Token.Type,
errorMessage: String = "expected ${tokenType.name}",
isOptional: Boolean = false
): Boolean {
val t = next() val t = next()
return if (t.type != tokenType) { return if (t.type != tokenType) {
if (!isOptional) { if (!isOptional) {
@ -57,7 +77,8 @@ internal class CompilerContext(val tokens: List<Token>) {
@Suppress("unused") @Suppress("unused")
fun skipTokens(vararg tokenTypes: Token.Type) { fun skipTokens(vararg tokenTypes: Token.Type) {
while( next().type in tokenTypes ) { /**/ } while (next().type in tokenTypes) { /**/
}
previous() previous()
} }
@ -73,4 +94,8 @@ internal class CompilerContext(val tokens: List<Token>) {
} }
} }
inline fun addBreak() {
breakFound = true
}
} }

View File

@ -65,6 +65,8 @@ class Context(
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context = fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj) Context(this, args, pos, newThisObj ?: thisObj)
fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj { fun addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj {
return StoredObj(value, isMutable).also { objects.put(name, it) } return StoredObj(value, isMutable).also { objects.put(name, it) }
} }

View File

@ -106,6 +106,16 @@ private class Parser(fromPos: Pos) {
Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT) Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT)
} }
'*' -> {
pos.advance()
Token(
loadTo("*/")?.trim()
?: throw ScriptError(from, "Unterminated multiline comment"),
from,
Token.Type.MULTILINE_COMMENT
)
}
'=' -> { '=' -> {
pos.advance() pos.advance()
Token("/=", from, Token.Type.SLASHASSIGN) Token("/=", from, Token.Type.SLASHASSIGN)
@ -164,10 +174,12 @@ private class Parser(fromPos: Pos) {
pos.advance() pos.advance()
Token("!in", from, Token.Type.NOTIN) Token("!in", from, Token.Type.NOTIN)
} }
's' -> { 's' -> {
pos.advance() pos.advance()
Token("!is", from, Token.Type.NOTIS) Token("!is", from, Token.Type.NOTIS)
} }
else -> { else -> {
pos.back() pos.back()
Token("!", from, Token.Type.NOT) Token("!", from, Token.Type.NOT)
@ -397,6 +409,15 @@ private class Parser(fromPos: Pos) {
return result.toString() return result.toString()
} }
private fun loadTo(str: String): String? {
val result = StringBuilder()
while (!pos.readFragment(str)) {
if (pos.end) return null
result.append(pos.currentChar); pos.advance()
}
return result.toString()
}
/** /**
* next non-whitespace char (newline are skipped too) or null if EOF * next non-whitespace char (newline are skipped too) or null if EOF
*/ */

View File

@ -46,12 +46,15 @@ class MutablePos(private val from: Pos) {
} }
} }
fun resetTo(pos: Pos) { line = pos.line; column = pos.column }
fun back() { fun back() {
if (column > 0) column-- if (column > 0) column--
else if (line > 0) else if (line > 0)
column = lines[--line].length - 1 column = lines[--line].length - 1
else throw IllegalStateException("can't go back from line 0, column 0") else throw IllegalStateException("can't go back from line 0, column 0")
} }
val currentChar: Char val currentChar: Char
get() { get() {
if (end) return 0.toChar() if (end) return 0.toChar()
@ -60,6 +63,18 @@ class MutablePos(private val from: Pos) {
else current[column] else current[column]
} }
/**
* If the next characters are equal to the fragment, advances the position and returns true.
* Otherwise, does nothing and returns false.
*/
fun readFragment(fragment: String): Boolean {
val mark = toPos()
for(ch in fragment)
if( currentChar != ch ) { resetTo(mark); return false }
else advance()
return true
}
override fun toString() = "($line:$column)" override fun toString() = "($line:$column)"
init { init {

View File

@ -1,5 +1,6 @@
package net.sergeych.lyng package net.sergeych.lyng
import kotlinx.coroutines.delay
import kotlin.math.* import kotlin.math.*
class Script( class Script(
@ -53,6 +54,10 @@ class Script(
raiseError(ObjAssertionError(this,"Assertion failed")) raiseError(ObjAssertionError(this,"Assertion failed"))
} }
addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
}
addConst("Real", ObjReal.type) addConst("Real", ObjReal.type)
addConst("String", ObjString.type) addConst("String", ObjString.type)
addConst("Int", ObjInt.type) addConst("Int", ObjInt.type)

View File

@ -1168,4 +1168,24 @@ class ScriptTest {
""".trimIndent() """.trimIndent()
) )
} }
@Test
fun testSpoilArgsBug() = runTest {
eval(
"""
fun fnb(a,b) { a + b }
fun fna(a, b) {
val a0 = a
val b0 = b
fnb(a + 1, b + 1)
assert( a0 == a )
assert( b0 == b )
}
fna(5,6)
"""
)
}
} }

View File

@ -5,8 +5,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Context import net.sergeych.lyng.Context
import net.sergeych.lyng.ObjVoid import net.sergeych.lyng.ObjVoid
import java.nio.file.Files
import java.nio.file.Files.readAllLines import java.nio.file.Files.readAllLines
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.extension
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.fail import kotlin.test.fail
@ -29,10 +31,15 @@ data class DocTest(
val code: String, val code: String,
val expectedOutput: String, val expectedOutput: String,
val expectedResult: String, val expectedResult: String,
val expectedError: String? = null val expectedError: String? = null,
val bookMode: Boolean = false
) { ) {
val sourceLines by lazy { code.lines() } val sourceLines by lazy { code.lines() }
val fileNamePart by lazy {
Paths.get(fileName).fileName.toString()
}
override fun toString(): String { override fun toString(): String {
return "DocTest:$fileName:${line + 1}..${line + sourceLines.size}" return "DocTest:$fileName:${line + 1}..${line + sourceLines.size}"
} }
@ -40,14 +47,17 @@ data class DocTest(
val detailedString by lazy { val detailedString by lazy {
val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line + 1}: $s" }.joinToString("\n") val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line + 1}: $s" }.joinToString("\n")
var result = "$this\n$codeWithLines\n" var result = "$this\n$codeWithLines\n"
if (expectedOutput.isNotBlank()) if( !bookMode) {
result += "--------expected output--------\n$expectedOutput\n" if (expectedOutput.isNotBlank())
result += "--------expected output--------\n$expectedOutput\n"
"$result-----expected return value-----\n$expectedResult" "$result-----expected return value-----\n$expectedResult"
}
else result
} }
} }
fun parseDocTests(fileName: String): Flow<DocTest> = flow { fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> = flow {
val book = readAllLines(Paths.get(fileName)) val book = readAllLines(Paths.get(fileName))
var startOffset = 0 var startOffset = 0
val block = mutableListOf<String>() val block = mutableListOf<String>()
@ -74,6 +84,18 @@ fun parseDocTests(fileName: String): Flow<DocTest> = flow {
} while (initial - leftMargin(x) != startOffset) } while (initial - leftMargin(x) != startOffset)
block[i] = x block[i] = x
} }
if (bookMode) {
emit(
DocTest(
fileName, startIndex,
block.joinToString("\n"),
"",
"",
null,
bookMode = true
)
)
}
// println(block.joinToString("\n") { "${startIndex + ii++}: $it" }) // println(block.joinToString("\n") { "${startIndex + ii++}: $it" })
val outStart = block.indexOfFirst { it.startsWith(">>>") } val outStart = block.indexOfFirst { it.startsWith(">>>") }
if (outStart < 0) { if (outStart < 0) {
@ -134,13 +156,19 @@ fun parseDocTests(fileName: String): Flow<DocTest> = flow {
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
suspend fun DocTest.test() { suspend fun DocTest.test(context: Context = Context()) {
val collectedOutput = StringBuilder() val collectedOutput = StringBuilder()
val context = Context().apply { val currentTest = this
context.apply {
addFn("println") { addFn("println") {
for ((i, a) in args.withIndex()) { if( bookMode ) {
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.asStr.value) println("${currentTest.fileNamePart}:${currentTest.line}> ${args.joinToString(" "){it.asStr.value}}")
collectedOutput.append('\n') }
else {
for ((i, a) in args.withIndex()) {
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.asStr.value)
collectedOutput.append('\n')
}
} }
ObjVoid ObjVoid
} }
@ -153,24 +181,39 @@ suspend fun DocTest.test() {
null null
}?.inspect()?.replace(Regex("@\\d+"), "@...") }?.inspect()?.replace(Regex("@\\d+"), "@...")
if (error != null || expectedOutput != collectedOutput.toString() || if (bookMode) {
expectedResult != result if (error != null) {
) { println("Sample failed: ${this.detailedString}")
println("Test failed: ${this.detailedString}") fail("book sample failed", error)
}
} else {
if (error != null || expectedOutput != collectedOutput.toString() ||
expectedResult != result
) {
println("Test failed: ${this.detailedString}")
}
error?.let {
fail("test failed", it)
}
assertEquals(expectedOutput, collectedOutput.toString(), "script output do not match")
assertEquals(expectedResult, result.toString(), "script result does not match")
// println("OK: $this")
} }
error?.let {
fail(it.toString(), it)
}
assertEquals(expectedOutput, collectedOutput.toString(), "script output do not match")
assertEquals(expectedResult, result.toString(), "script result does not match")
// println("OK: $this")
} }
suspend fun runDocTests(fileName: String) { suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
parseDocTests(fileName).collect { dt -> val bookContext = Context()
dt.test() var count = 0
parseDocTests(fileName, bookMode).collect { dt ->
if (bookMode) dt.test(bookContext)
else dt.test()
count++
} }
print("completed mdtest: $fileName; ")
if (bookMode)
println("fragments processed: $count")
else
println("tests passed: $count")
} }
class BookTest { class BookTest {
@ -210,4 +253,12 @@ class BookTest {
runDocTests("../docs/Range.md") runDocTests("../docs/Range.md")
} }
@Test
fun testSampleBooks() = runTest {
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
if (bt.extension == "md") {
runDocTests(bt.toString(), bookMode = true)
}
}
}
} }

View File

@ -0,0 +1,35 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import net.sergeych.lyng.Context
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.extension
import kotlin.test.Test
suspend fun executeSampleTests(fileName: String) {
val sample = withContext(Dispatchers.IO) {
Files.readString(Paths.get(fileName))
}
runBlocking {
val c = Context()
for( i in 1..1) {
val start = Clock.System.now()
c.eval(sample)
val time = Clock.System.now() - start
println("$time: $fileName")
// delay(100)
}
}
}
class SamplesTest {
@Test
fun testSamples() = runBlocking {
for (s in Files.list(Paths.get("../docs/samples"))) {
if (s.extension == "lyng") executeSampleTests(s.toString())
}
}
}