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
|
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
|
||||||
|
@ -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" }
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
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