fix #5 loop optimization
fixed arguments modifying bug added samples and samplebooks w/tests
This commit is contained in:
		
							parent
							
								
									53eb1bc5e7
								
							
						
					
					
						commit
						512dda5984
					
				
							
								
								
									
										44
									
								
								docs/samples/combinatorics.lyng.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								docs/samples/combinatorics.lyng.md
									
									
									
									
									
										Normal 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...
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								docs/samples/happy_numbers.lyng
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								docs/samples/happy_numbers.lyng
									
									
									
									
									
										Normal 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 )
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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" }
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,9 @@ android {
 | 
			
		||||
        targetCompatibility = JavaVersion.VERSION_11
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation(libs.firebase.crashlytics.buildtools)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mavenPublishing {
 | 
			
		||||
    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
        """
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								library/src/jvmTest/kotlin/SamplesTest.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								library/src/jvmTest/kotlin/SamplesTest.kt
									
									
									
									
									
										Normal 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())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user