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
include_toc: true
---
# Lyng tutorial
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"
kotlinx-coroutines = "1.10.1"
mp_bintools = "0.1.12"
firebaseCrashlyticsBuildtools = "3.0.3"
[libraries]
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-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
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]
androidLibrary = { id = "com.android.library", version.ref = "agp" }

View File

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

View File

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

View File

@ -3,15 +3,31 @@ package net.sergeych.lyng
internal class CompilerContext(val tokens: List<Token>) {
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
fun hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0
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 restorePos(pos: Int) { currentIndex = pos }
fun savePos() = currentIndex
fun restorePos(pos: Int) {
currentIndex = pos
}
fun ensureLabelIsValid(pos: Pos, label: String) {
if (label !in labels)
@ -42,7 +58,11 @@ internal class CompilerContext(val tokens: List<Token>) {
* @return `true` if the token was skipped
* @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()
return if (t.type != tokenType) {
if (!isOptional) {
@ -57,7 +77,8 @@ internal class CompilerContext(val tokens: List<Token>) {
@Suppress("unused")
fun skipTokens(vararg tokenTypes: Token.Type) {
while( next().type in tokenTypes ) { /**/ }
while (next().type in tokenTypes) { /**/
}
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 =
Context(this, args, pos, newThisObj ?: thisObj)
fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj?): StoredObj {
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)
}
'*' -> {
pos.advance()
Token(
loadTo("*/")?.trim()
?: throw ScriptError(from, "Unterminated multiline comment"),
from,
Token.Type.MULTILINE_COMMENT
)
}
'=' -> {
pos.advance()
Token("/=", from, Token.Type.SLASHASSIGN)
@ -164,10 +174,12 @@ private class Parser(fromPos: Pos) {
pos.advance()
Token("!in", from, Token.Type.NOTIN)
}
's' -> {
pos.advance()
Token("!is", from, Token.Type.NOTIS)
}
else -> {
pos.back()
Token("!", from, Token.Type.NOT)
@ -397,6 +409,15 @@ private class Parser(fromPos: Pos) {
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
*/

View File

@ -46,12 +46,15 @@ class MutablePos(private val from: Pos) {
}
}
fun resetTo(pos: Pos) { line = pos.line; column = pos.column }
fun back() {
if (column > 0) column--
else if (line > 0)
column = lines[--line].length - 1
else throw IllegalStateException("can't go back from line 0, column 0")
}
val currentChar: Char
get() {
if (end) return 0.toChar()
@ -60,6 +63,18 @@ class MutablePos(private val from: Pos) {
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)"
init {

View File

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

View File

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