docs+doctest, while loop with break and labels for non-local breaks
This commit is contained in:
parent
0569be3b21
commit
22fbd5584b
158
docs/tutorial.md
158
docs/tutorial.md
@ -36,15 +36,31 @@ If you don't want block to return anything, use `void`:
|
|||||||
|
|
||||||
Every construction is an expression that returns something (or `void`):
|
Every construction is an expression that returns something (or `void`):
|
||||||
|
|
||||||
|
val x = 111 // or autotest will fail!
|
||||||
val limited = if( x > 100 ) 100 else x
|
val limited = if( x > 100 ) 100 else x
|
||||||
|
limited
|
||||||
|
>>> 100
|
||||||
|
|
||||||
You can use blocks in if statement, as expected:
|
You can use blocks in if statement, as expected:
|
||||||
|
|
||||||
|
val x = 200
|
||||||
val limited = if( x > 100 ) {
|
val limited = if( x > 100 ) {
|
||||||
100 + x * 0.1
|
100 + x * 0.1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
x
|
x
|
||||||
|
limited
|
||||||
|
>>> 120.0
|
||||||
|
|
||||||
|
When putting multiple statments in the same line it is convenient and recommended to use `;`:
|
||||||
|
|
||||||
|
var from; var to;
|
||||||
|
from = 0; to = 100
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Notice: returned value is `void` as assignment operator does not return its value. We might decide to change it.
|
||||||
|
|
||||||
|
Most often you can omit `;`, but improves readability and prevent some hardly seen bugs.
|
||||||
|
|
||||||
So the principles are:
|
So the principles are:
|
||||||
|
|
||||||
@ -55,18 +71,54 @@ So the principles are:
|
|||||||
|
|
||||||
It is rather simple, like everywhere else:
|
It is rather simple, like everywhere else:
|
||||||
|
|
||||||
|
val x = 2.0
|
||||||
|
//
|
||||||
sin(x * π/4) / 2.0
|
sin(x * π/4) / 2.0
|
||||||
|
>>> 0.5
|
||||||
|
|
||||||
See [math](math.md) for more on it.
|
See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed.
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
|
||||||
|
Much like in kotlin, there are _variables_:
|
||||||
|
|
||||||
|
var name = "Sergey"
|
||||||
|
|
||||||
|
Variables can be not initialized at declaration, in which case they must be assigned before use, or an exception
|
||||||
|
will be thrown:
|
||||||
|
|
||||||
|
var foo
|
||||||
|
// WRONG! Exception will be thrown at next line:
|
||||||
|
foo + "bar"
|
||||||
|
|
||||||
|
Correct pattern is:
|
||||||
|
|
||||||
|
foo = "foo"
|
||||||
|
// now is OK:
|
||||||
|
foo + bar
|
||||||
|
|
||||||
|
This is though a rare case when you need uninitialized variables, most often you can use conditional operatorss
|
||||||
|
and even loops to assign results (see below).
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
|
||||||
|
Same as in kotlin:
|
||||||
|
|
||||||
|
val HalfPi = π / 2
|
||||||
|
|
||||||
|
Note using greek characters in identifiers! All letters allowed, but remember who might try to read your script, most likely will know some English, the rest is the pure uncertainty.
|
||||||
|
|
||||||
# Defining functions
|
# Defining functions
|
||||||
|
|
||||||
fun check(amount) {
|
fun check(amount) {
|
||||||
if( amount > 100 )
|
if( amount > 100 )
|
||||||
"anough"
|
"enough"
|
||||||
else
|
else
|
||||||
"more"
|
"more"
|
||||||
}
|
}
|
||||||
|
>>> Callable@...
|
||||||
|
|
||||||
|
Notice how function definition return a value, instance of `Callable`.
|
||||||
|
|
||||||
You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_.
|
You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_.
|
||||||
|
|
||||||
@ -74,10 +126,11 @@ There are default parameters in Ling:
|
|||||||
|
|
||||||
fn check(amount, prefix = "answer: ") {
|
fn check(amount, prefix = "answer: ") {
|
||||||
prefix + if( amount > 100 )
|
prefix + if( amount > 100 )
|
||||||
"anough"
|
"enough"
|
||||||
else
|
else
|
||||||
"more"
|
"more"
|
||||||
}
|
}
|
||||||
|
>>> Callable@...
|
||||||
|
|
||||||
## Closures
|
## Closures
|
||||||
|
|
||||||
@ -86,12 +139,12 @@ Each __block has an isolated context that can be accessed from closures__. For e
|
|||||||
var counter = 1
|
var counter = 1
|
||||||
|
|
||||||
// this is ok: coumter is incremented
|
// this is ok: coumter is incremented
|
||||||
def increment(amount=1) {
|
fun increment(amount=1) {
|
||||||
// use counter from a closure:
|
// use counter from a closure:
|
||||||
counter = counter + amount
|
counter = counter + amount
|
||||||
}
|
}
|
||||||
|
|
||||||
val taskAlias = def someTask() {
|
val taskAlias = fun someTask() {
|
||||||
// this obscures global outer var with a local one
|
// this obscures global outer var with a local one
|
||||||
var counter = 0
|
var counter = 0
|
||||||
// ...
|
// ...
|
||||||
@ -99,6 +152,7 @@ Each __block has an isolated context that can be accessed from closures__. For e
|
|||||||
// ...
|
// ...
|
||||||
counter
|
counter
|
||||||
}
|
}
|
||||||
|
>>> void
|
||||||
|
|
||||||
As was told, `def` statement return callable for the function, it could be used as a parameter, or elsewhere
|
As was told, `def` statement return callable for the function, it could be used as a parameter, or elsewhere
|
||||||
to call it:
|
to call it:
|
||||||
@ -132,28 +186,94 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
|||||||
|
|
||||||
String literal could be multiline:
|
String literal could be multiline:
|
||||||
|
|
||||||
"
|
"Hello
|
||||||
Hello,
|
|
||||||
World!
|
|
||||||
"
|
|
||||||
>>> "Hello
|
|
||||||
World"
|
World"
|
||||||
|
|
||||||
In that case compiler removes left margin and first/last empty lines. Note that it won't remove margin:
|
though multiline literals is yet work in progress.
|
||||||
|
|
||||||
"Hello,
|
# Flow control operators
|
||||||
World
|
|
||||||
"
|
|
||||||
>>> "Hello,
|
|
||||||
World
|
|
||||||
"
|
|
||||||
|
|
||||||
because the first line has no margin in the literal.
|
## if-then-else
|
||||||
|
|
||||||
|
As everywhere else, and as expression:
|
||||||
|
|
||||||
|
val count = 11
|
||||||
|
if( count > 10 )
|
||||||
|
println("too much")
|
||||||
|
else {
|
||||||
|
// do something else
|
||||||
|
println("just "+count)
|
||||||
|
}
|
||||||
|
>>> too much
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Notice returned value `void`: it is because of `println` have no return value, e.g., `void`.
|
||||||
|
|
||||||
|
|
||||||
|
Or, more neat:
|
||||||
|
|
||||||
|
var count = 3
|
||||||
|
println( if( count > 10 ) "too much" else "just " + count )
|
||||||
|
>>> just 3
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## while
|
||||||
|
|
||||||
|
Regular pre-condition while loop, as expression, loop returns it's last line result:
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
while( count < 5 ) {
|
||||||
|
count = count + 1
|
||||||
|
count * 10
|
||||||
|
}
|
||||||
|
>>> 50
|
||||||
|
|
||||||
|
We can break as usual:
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
while( count < 5 ) {
|
||||||
|
if( count < 5 ) break
|
||||||
|
count = count + 1
|
||||||
|
count * 10
|
||||||
|
}
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Why `void`? Because `break` drops out without the chute, not providing anything to return. Indeed, we should provide exit value in the case:
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
while( count < 5 ) {
|
||||||
|
if( count > 3 ) break "too much"
|
||||||
|
count = count + 1
|
||||||
|
count * 10
|
||||||
|
}
|
||||||
|
>>> too much
|
||||||
|
|
||||||
|
## Breaking nested loops
|
||||||
|
|
||||||
|
If you have several loops and want to exit not the inner one, use labels:
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
// notice the label:
|
||||||
|
outerLoop@ while( count < 5 ) {
|
||||||
|
var innerCount = 0
|
||||||
|
while( innerCount < 100 ) {
|
||||||
|
innerCount = innerCount + 1
|
||||||
|
|
||||||
|
if( innerCount == 5 && count == 2 )
|
||||||
|
// and here we break the labelled loop:
|
||||||
|
break@outerLoop "5/2 situation"
|
||||||
|
}
|
||||||
|
count = count + 1
|
||||||
|
count * 10
|
||||||
|
}
|
||||||
|
>>> 5/2 situation
|
||||||
|
|
||||||
|
The label can be any valid identifier, even a keyword, labels exist in their own, isolated world, so no risk of occasional clash. Labels are also scoped to their context and do not exist outside it.
|
||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
|
|
||||||
// single line comment
|
// single line comment
|
||||||
var result = null // here we will store the result
|
var result = null // here we will store the result
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.sergeych.ling
|
package net.sergeych.ling
|
||||||
|
|
||||||
data class Arguments(val callerPos: Pos,val list: List<Info>) {
|
data class Arguments(val callerPos: Pos,val list: List<Info>): Iterable<Obj> {
|
||||||
|
|
||||||
data class Info(val value: Obj,val pos: Pos)
|
data class Info(val value: Obj,val pos: Pos)
|
||||||
|
|
||||||
@ -16,4 +16,8 @@ data class Arguments(val callerPos: Pos,val list: List<Info>) {
|
|||||||
companion object {
|
companion object {
|
||||||
val EMPTY = Arguments("".toSource().startPos,emptyList())
|
val EMPTY = Arguments("".toSource().startPos,emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun iterator(): Iterator<Obj> {
|
||||||
|
return list.map { it.value }.iterator()
|
||||||
|
}
|
||||||
}
|
}
|
@ -45,13 +45,23 @@ type alias has target type name. So we have to have something that denotes a _ty
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
class CompilerContext(tokens: List<Token>) : ListIterator<Token> by tokens.listIterator() {
|
||||||
|
val labels = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun ensureLabelIsValid(pos: Pos, label: String) {
|
||||||
|
if (label !in labels)
|
||||||
|
throw ScriptError(pos, "Undefined label '$label'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Compiler {
|
class Compiler {
|
||||||
|
|
||||||
fun compile(source: Source): Script {
|
fun compile(source: Source): Script {
|
||||||
return parseScript(source.startPos, parseLing(source).listIterator())
|
return parseScript(source.startPos, CompilerContext(parseLing(source)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseScript(start: Pos, tokens: ListIterator<Token>): Script {
|
private fun parseScript(start: Pos, tokens: CompilerContext): Script {
|
||||||
val statements = mutableListOf<Statement>()
|
val statements = mutableListOf<Statement>()
|
||||||
while (parseStatement(tokens)?.also {
|
while (parseStatement(tokens)?.also {
|
||||||
statements += it
|
statements += it
|
||||||
@ -60,7 +70,7 @@ class Compiler {
|
|||||||
return Script(start, statements)
|
return Script(start, statements)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseStatement(tokens: ListIterator<Token>): Statement? {
|
private fun parseStatement(tokens: CompilerContext): Statement? {
|
||||||
while (true) {
|
while (true) {
|
||||||
val t = tokens.next()
|
val t = tokens.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
@ -71,7 +81,7 @@ class Compiler {
|
|||||||
// this _is_ assignment statement
|
// this _is_ assignment statement
|
||||||
return AssignStatement(
|
return AssignStatement(
|
||||||
t.pos, t.value,
|
t.pos, t.value,
|
||||||
parseExpression(tokens) ?: throw ScriptError(
|
parseStatement(tokens) ?: throw ScriptError(
|
||||||
t.pos,
|
t.pos,
|
||||||
"Expecting expression for assignment operator"
|
"Expecting expression for assignment operator"
|
||||||
)
|
)
|
||||||
@ -88,8 +98,11 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Token.Type.LABEL -> continue
|
||||||
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
|
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
|
||||||
|
|
||||||
|
Token.Type.NEWLINE -> continue
|
||||||
|
|
||||||
Token.Type.SEMICOLON -> continue
|
Token.Type.SEMICOLON -> continue
|
||||||
|
|
||||||
Token.Type.LBRACE -> {
|
Token.Type.LBRACE -> {
|
||||||
@ -113,7 +126,7 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseExpression(tokens: ListIterator<Token>, level: Int = 0): Statement? {
|
private fun parseExpression(tokens: CompilerContext, level: Int = 0): Statement? {
|
||||||
if (level == lastLevel)
|
if (level == lastLevel)
|
||||||
return parseTerm(tokens)
|
return parseTerm(tokens)
|
||||||
var lvalue = parseExpression(tokens, level + 1)
|
var lvalue = parseExpression(tokens, level + 1)
|
||||||
@ -136,7 +149,7 @@ class Compiler {
|
|||||||
return lvalue
|
return lvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseTerm(tokens: ListIterator<Token>): Statement? {
|
fun parseTerm(tokens: CompilerContext): Statement? {
|
||||||
// call op
|
// call op
|
||||||
// index op
|
// index op
|
||||||
// unary op
|
// unary op
|
||||||
@ -187,7 +200,7 @@ class Compiler {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseVarAccess(id: Token, tokens: ListIterator<Token>, path: List<String> = emptyList()): Statement {
|
fun parseVarAccess(id: Token, tokens: CompilerContext, path: List<String> = emptyList()): Statement {
|
||||||
val nt = tokens.next()
|
val nt = tokens.next()
|
||||||
|
|
||||||
fun resolve(context: Context): Context {
|
fun resolve(context: Context): Context {
|
||||||
@ -219,7 +232,7 @@ class Compiler {
|
|||||||
Token.Type.RPAREN, Token.Type.COMMA -> {}
|
Token.Type.RPAREN, Token.Type.COMMA -> {}
|
||||||
else -> {
|
else -> {
|
||||||
tokens.previous()
|
tokens.previous()
|
||||||
parseExpression(tokens)?.let { args += Arguments.Info(it, t.pos) }
|
parseStatement(tokens)?.let { args += Arguments.Info(it, t.pos) }
|
||||||
?: throw ScriptError(t.pos, "Expecting arguments list")
|
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,6 +260,14 @@ class Compiler {
|
|||||||
else -> {
|
else -> {
|
||||||
// just access the var
|
// just access the var
|
||||||
tokens.previous()
|
tokens.previous()
|
||||||
|
// val t = tokens.next()
|
||||||
|
// tokens.previous()
|
||||||
|
// println(t)
|
||||||
|
// if( path.isEmpty() ) {
|
||||||
|
// statement?
|
||||||
|
// tokens.previous()
|
||||||
|
// parseStatement(tokens) ?: throw ScriptError(id.pos, "Expecting expression/statement")
|
||||||
|
// } else
|
||||||
statement(id.pos) {
|
statement(id.pos) {
|
||||||
val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: ${id.value}")
|
val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: ${id.value}")
|
||||||
v.value ?: throw ScriptError(id.pos, "Variable $id is not initialized")
|
v.value ?: throw ScriptError(id.pos, "Variable $id is not initialized")
|
||||||
@ -256,7 +277,7 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun parseNumber(isPlus: Boolean, tokens: ListIterator<Token>): Obj {
|
fun parseNumber(isPlus: Boolean, tokens: CompilerContext): Obj {
|
||||||
val t = tokens.next()
|
val t = tokens.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
Token.Type.INT, Token.Type.HEX -> {
|
Token.Type.INT, Token.Type.HEX -> {
|
||||||
@ -279,43 +300,131 @@ class Compiler {
|
|||||||
* Parse keyword-starting statenment.
|
* Parse keyword-starting statenment.
|
||||||
* @return parsed statement or null if, for example. [id] is not among keywords
|
* @return parsed statement or null if, for example. [id] is not among keywords
|
||||||
*/
|
*/
|
||||||
private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): Statement? = when (id.value) {
|
private fun parseKeywordStatement(id: Token, cc: CompilerContext): Statement? = when (id.value) {
|
||||||
"val" -> parseVarDeclaration(id.value, false, tokens)
|
"val" -> parseVarDeclaration(id.value, false, cc)
|
||||||
"var" -> parseVarDeclaration(id.value, true, tokens)
|
"var" -> parseVarDeclaration(id.value, true, cc)
|
||||||
"fn", "fun" -> parseFunctionDeclaration(tokens)
|
"while" -> parseWhileStatement(cc)
|
||||||
"if" -> parseIfStatement(tokens)
|
"break" -> parseBreakStatement(id.pos, cc)
|
||||||
|
"fn", "fun" -> parseFunctionDeclaration(cc)
|
||||||
|
"if" -> parseIfStatement(cc)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseIfStatement(tokens: ListIterator<Token>): Statement {
|
fun getLabel(cc: CompilerContext, maxDepth: Int = 2): String? {
|
||||||
var t = tokens.next()
|
var cnt = 0
|
||||||
val start = t.pos
|
var found: String? = null
|
||||||
|
while (cc.hasPrevious() && cnt < maxDepth) {
|
||||||
|
val t = cc.previous()
|
||||||
|
cnt++
|
||||||
|
if (t.type == Token.Type.LABEL) {
|
||||||
|
found = t.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (cnt-- > 0) cc.next()
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseWhileStatement(cc: CompilerContext): Statement {
|
||||||
|
val label = getLabel(cc)?.also { cc.labels += it }
|
||||||
|
val start = ensureLparen(cc)
|
||||||
|
val condition = parseExpression(cc) ?: throw ScriptError(start, "Bad while statement: expected expression")
|
||||||
|
ensureRparen(cc)
|
||||||
|
|
||||||
|
val body = parseStatement(cc) ?: throw ScriptError(start, "Bad while statement: expected statement")
|
||||||
|
label?.also { cc.labels -= it }
|
||||||
|
|
||||||
|
return statement(body.pos) {
|
||||||
|
var result: Obj = ObjVoid
|
||||||
|
while (condition.execute(it).toBool()) {
|
||||||
|
try {
|
||||||
|
result = body.execute(it)
|
||||||
|
} catch (lbe: LoopBreakContinueException) {
|
||||||
|
if (lbe.label == label || lbe.label == null) {
|
||||||
|
if (lbe.doContinue) continue
|
||||||
|
else {
|
||||||
|
result = lbe.result
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
throw lbe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseBreakStatement(start: Pos, cc: CompilerContext): Statement {
|
||||||
|
var t = cc.next()
|
||||||
|
|
||||||
|
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
|
||||||
|
cc.previous()
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
t.value
|
||||||
|
}?.also {
|
||||||
|
// check that label is defined
|
||||||
|
cc.ensureLabelIsValid(start, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expression?
|
||||||
|
t = cc.next()
|
||||||
|
cc.previous()
|
||||||
|
val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
|
||||||
|
t.type != Token.Type.SEMICOLON &&
|
||||||
|
t.type != Token.Type.NEWLINE)
|
||||||
|
) {
|
||||||
|
// we have something on this line, could be expression
|
||||||
|
parseStatement(cc)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
return statement(start) {
|
||||||
|
val returnValue = resultExpr?.execute(it)// ?: ObjVoid
|
||||||
|
throw LoopBreakContinueException(
|
||||||
|
doContinue = false,
|
||||||
|
label = label,
|
||||||
|
result = returnValue ?: ObjVoid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureRparen(tokens: CompilerContext): Pos {
|
||||||
|
val t = tokens.next()
|
||||||
|
if (t.type != Token.Type.RPAREN)
|
||||||
|
throw ScriptError(t.pos, "expected ')'")
|
||||||
|
return t.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureLparen(tokens: CompilerContext): Pos {
|
||||||
|
val t = tokens.next()
|
||||||
if (t.type != Token.Type.LPAREN)
|
if (t.type != Token.Type.LPAREN)
|
||||||
throw ScriptError(t.pos, "Bad if statement: expected '('")
|
throw ScriptError(t.pos, "expected '('")
|
||||||
|
return t.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseIfStatement(tokens: CompilerContext): Statement {
|
||||||
|
val start = ensureLparen(tokens)
|
||||||
|
|
||||||
val condition = parseExpression(tokens)
|
val condition = parseExpression(tokens)
|
||||||
?: throw ScriptError(t.pos, "Bad if statement: expected expression")
|
?: throw ScriptError(start, "Bad if statement: expected expression")
|
||||||
|
|
||||||
t = tokens.next()
|
val pos = ensureRparen(tokens)
|
||||||
if( t.type != Token.Type.RPAREN)
|
|
||||||
throw ScriptError(t.pos, "Bad if statement: expected ')' after condition expression")
|
|
||||||
|
|
||||||
val ifBody = parseStatement(tokens) ?: throw ScriptError(t.pos, "Bad if statement: expected statement")
|
val ifBody = parseStatement(tokens) ?: throw ScriptError(pos, "Bad if statement: expected statement")
|
||||||
|
|
||||||
// could be else block:
|
// could be else block:
|
||||||
val t2 = tokens.next()
|
val t2 = tokens.next()
|
||||||
|
|
||||||
// we generate different statements: optimization
|
// we generate different statements: optimization
|
||||||
return if (t2.type == Token.Type.ID && t2.value == "else") {
|
return if (t2.type == Token.Type.ID && t2.value == "else") {
|
||||||
val elseBody = parseStatement(tokens) ?: throw ScriptError(t.pos, "Bad else statement: expected statement")
|
val elseBody = parseStatement(tokens) ?: throw ScriptError(pos, "Bad else statement: expected statement")
|
||||||
return statement(start) {
|
return statement(start) {
|
||||||
if (condition.execute(it).toBool())
|
if (condition.execute(it).toBool())
|
||||||
ifBody.execute(it)
|
ifBody.execute(it)
|
||||||
else
|
else
|
||||||
elseBody.execute(it)
|
elseBody.execute(it)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
tokens.previous()
|
tokens.previous()
|
||||||
statement(start) {
|
statement(start) {
|
||||||
if (condition.execute(it).toBool())
|
if (condition.execute(it).toBool())
|
||||||
@ -332,7 +441,7 @@ class Compiler {
|
|||||||
val defaultValue: Statement? = null
|
val defaultValue: Statement? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun parseFunctionDeclaration(tokens: ListIterator<Token>): Statement {
|
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement {
|
||||||
var t = tokens.next()
|
var t = tokens.next()
|
||||||
val start = t.pos
|
val start = t.pos
|
||||||
val name = if (t.type != Token.Type.ID)
|
val name = if (t.type != Token.Type.ID)
|
||||||
@ -392,7 +501,7 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseBlock(tokens: ListIterator<Token>): Statement {
|
private fun parseBlock(tokens: CompilerContext): Statement {
|
||||||
val t = tokens.next()
|
val t = tokens.next()
|
||||||
if (t.type != Token.Type.LBRACE)
|
if (t.type != Token.Type.LBRACE)
|
||||||
throw ScriptError(t.pos, "Expected block body start: {")
|
throw ScriptError(t.pos, "Expected block body start: {")
|
||||||
@ -407,8 +516,9 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: ListIterator<Token>): Statement {
|
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement {
|
||||||
val nameToken = tokens.next()
|
val nameToken = tokens.next()
|
||||||
|
val start = nameToken.pos
|
||||||
if (nameToken.type != Token.Type.ID)
|
if (nameToken.type != Token.Type.ID)
|
||||||
throw ScriptError(nameToken.pos, "Expected identifier after '$kind'")
|
throw ScriptError(nameToken.pos, "Expected identifier after '$kind'")
|
||||||
val name = nameToken.value
|
val name = nameToken.value
|
||||||
@ -416,13 +526,13 @@ class Compiler {
|
|||||||
var setNull = false
|
var setNull = false
|
||||||
if (eqToken.type != Token.Type.ASSIGN) {
|
if (eqToken.type != Token.Type.ASSIGN) {
|
||||||
if (!mutable)
|
if (!mutable)
|
||||||
throw ScriptError(eqToken.pos, "Expected initializer: '=' after '$kind ${name}'")
|
throw ScriptError(start, "val must be initialized")
|
||||||
else {
|
else {
|
||||||
tokens.previous()
|
tokens.previous()
|
||||||
setNull = true
|
setNull = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val initialExpression = if (setNull) null else parseExpression(tokens)
|
val initialExpression = if (setNull) null else parseStatement(tokens)
|
||||||
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
|
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
|
||||||
return statement(nameToken.pos) { context ->
|
return statement(nameToken.pos) { context ->
|
||||||
if (context.containsLocal(name))
|
if (context.containsLocal(name))
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package net.sergeych.ling
|
||||||
|
|
||||||
|
class LoopBreakContinueException(
|
||||||
|
val doContinue: Boolean,
|
||||||
|
val result: Obj = ObjVoid,
|
||||||
|
val label: String? = null
|
||||||
|
) : RuntimeException()
|
@ -64,6 +64,8 @@ object ObjVoid : Obj() {
|
|||||||
override fun compareTo(other: Obj): Int {
|
override fun compareTo(other: Obj): Int {
|
||||||
return if (other === this) 0 else -1
|
return if (other === this) 0 else -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "void"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -127,6 +129,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
return value.compareTo(other.doubleValue)
|
return value.compareTo(other.doubleValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -142,6 +145,8 @@ data class ObjInt(val value: Long) : Obj(), Numeric {
|
|||||||
if( other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other")
|
if( other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other")
|
||||||
return value.compareTo(other.doubleValue)
|
return value.compareTo(other.doubleValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -153,7 +158,7 @@ data class ObjBool(val value: Boolean) : Obj() {
|
|||||||
if( other !is ObjBool) throw IllegalArgumentException("cannot compare $this with $other")
|
if( other !is ObjBool) throw IllegalArgumentException("cannot compare $this with $other")
|
||||||
return value.compareTo(other.value)
|
return value.compareTo(other.value)
|
||||||
}
|
}
|
||||||
|
override fun toString(): String = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ObjNamespace(val name: String, val context: Context) : Obj() {
|
data class ObjNamespace(val name: String, val context: Context) : Obj() {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package net.sergeych.ling
|
package net.sergeych.ling
|
||||||
|
|
||||||
private val idFirstChars: Set<Char> = (
|
val digitsSet = ('0'..'9').toSet()
|
||||||
('a'..'z') + ('A'..'Z') + '_' + ('а'..'я') + ('А'..'Я')
|
val digits = { d: Char -> d in digitsSet }
|
||||||
).toSet()
|
val hexDigits = digitsSet + ('a'..'f') + ('A'..'F')
|
||||||
val idNextChars: Set<Char> = idFirstChars + ('0'..'9')
|
val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit()}
|
||||||
val digits = ('0'..'9').toSet()
|
|
||||||
val hexDigits = digits + ('a'..'f') + ('A'..'F')
|
@Suppress("unused")
|
||||||
|
val idFirstChars = { d: Char -> d.isLetter() || d == '_' }
|
||||||
|
|
||||||
fun parseLing(source: Source): List<Token> {
|
fun parseLing(source: Source): List<Token> {
|
||||||
val p = Parser(fromPos = source.startPos)
|
val p = Parser(fromPos = source.startPos)
|
||||||
@ -100,16 +101,38 @@ private class Parser(fromPos: Pos) {
|
|||||||
} else
|
} else
|
||||||
Token("&", from, Token.Type.BITAND)
|
Token("&", from, Token.Type.BITAND)
|
||||||
}
|
}
|
||||||
|
'@' -> {
|
||||||
|
val label = loadChars(idNextChars)
|
||||||
|
if( label.isNotEmpty()) Token(label, from, Token.Type.ATLABEL)
|
||||||
|
else raise("unexpected @ character")
|
||||||
|
}
|
||||||
|
'\n' -> Token("\n", from, Token.Type.NEWLINE)
|
||||||
|
|
||||||
'"' -> loadStringToken()
|
'"' -> loadStringToken()
|
||||||
in digits -> {
|
in digitsSet -> {
|
||||||
pos.back()
|
pos.back()
|
||||||
decodeNumber(loadChars(digits), from)
|
decodeNumber(loadChars(digits), from)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (ch.isLetter() || ch == '_')
|
// Labels processing is complicated!
|
||||||
Token(ch + loadChars(idNextChars), from, Token.Type.ID)
|
// some@ statement: label 'some', ID 'statement'
|
||||||
|
// statement@some: ID 'statement', LABEL 'some'!
|
||||||
|
if (ch.isLetter() || ch == '_') {
|
||||||
|
val text = ch + loadChars(idNextChars)
|
||||||
|
if( currentChar == '@') {
|
||||||
|
advance()
|
||||||
|
if( currentChar.isLetter()) {
|
||||||
|
// break@label or like
|
||||||
|
pos.back()
|
||||||
|
Token(text, from, Token.Type.ID)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Token(text, from, Token.Type.LABEL)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Token(text, from, Token.Type.ID)
|
||||||
|
}
|
||||||
else
|
else
|
||||||
raise("can't parse token")
|
raise("can't parse token")
|
||||||
}
|
}
|
||||||
@ -122,7 +145,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
else if (currentChar == '.') {
|
else if (currentChar == '.') {
|
||||||
// could be decimal
|
// could be decimal
|
||||||
advance()
|
advance()
|
||||||
if (currentChar in digits) {
|
if (currentChar in digitsSet) {
|
||||||
// decimal part
|
// decimal part
|
||||||
val p2 = loadChars(digits)
|
val p2 = loadChars(digits)
|
||||||
// with exponent?
|
// with exponent?
|
||||||
@ -152,7 +175,7 @@ private class Parser(fromPos: Pos) {
|
|||||||
// could be integer, also hex:
|
// could be integer, also hex:
|
||||||
if (currentChar == 'x' && p1 == "0") {
|
if (currentChar == 'x' && p1 == "0") {
|
||||||
advance()
|
advance()
|
||||||
Token(loadChars(hexDigits), start, Token.Type.HEX).also {
|
Token(loadChars({ it in hexDigits}), start, Token.Type.HEX).also {
|
||||||
if (currentChar.isLetter())
|
if (currentChar.isLetter())
|
||||||
raise("invalid hex literal")
|
raise("invalid hex literal")
|
||||||
}
|
}
|
||||||
@ -197,14 +220,19 @@ private class Parser(fromPos: Pos) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load characters from the set until it reaches EOF or invalid character found.
|
* Load characters from the set until it reaches EOF or invalid character found.
|
||||||
* stop at EOF on character not in [validChars].
|
* stop at EOF on character filtered by [isValidChar].
|
||||||
|
*
|
||||||
|
* Note this function loads only on one string. Multiline texts are not supported by
|
||||||
|
* this method.
|
||||||
|
*
|
||||||
* @return the string of valid characters, could be empty
|
* @return the string of valid characters, could be empty
|
||||||
*/
|
*/
|
||||||
private fun loadChars(validChars: Set<Char>): String {
|
private fun loadChars(isValidChar: (Char)->Boolean): String {
|
||||||
|
val startLine = pos.line
|
||||||
val result = StringBuilder()
|
val result = StringBuilder()
|
||||||
while (!pos.end) {
|
while (!pos.end && pos.line == startLine) {
|
||||||
val ch = pos.currentChar
|
val ch = pos.currentChar
|
||||||
if (ch in validChars) {
|
if (isValidChar(ch)) {
|
||||||
result.append(ch)
|
result.append(ch)
|
||||||
advance()
|
advance()
|
||||||
} else
|
} else
|
||||||
|
@ -36,15 +36,13 @@ class MutablePos(private val from: Pos) {
|
|||||||
fun advance(): Char? {
|
fun advance(): Char? {
|
||||||
if (end) return null
|
if (end) return null
|
||||||
val current = lines[line]
|
val current = lines[line]
|
||||||
return if (column+1 < current.length) {
|
return if (column < current.length) {
|
||||||
current[column++]
|
column++
|
||||||
|
currentChar
|
||||||
} else {
|
} else {
|
||||||
column = 0
|
column = 0
|
||||||
while( ++line < lines.size && lines[line].isEmpty() ) {
|
if(++line >= lines.size) null
|
||||||
// skip empty lines
|
else currentChar
|
||||||
}
|
|
||||||
if(line >= lines.size) null
|
|
||||||
else lines[line][column]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,9 +53,12 @@ class MutablePos(private val from: Pos) {
|
|||||||
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) 0.toChar()
|
if (end) return 0.toChar()
|
||||||
else lines[line][column]
|
val current = lines[line]
|
||||||
|
return if (column >= current.length) '\n'
|
||||||
|
else current[column]
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString() = "($line:$column)"
|
override fun toString() = "($line:$column)"
|
||||||
|
|
||||||
|
@ -21,8 +21,12 @@ class Script(
|
|||||||
companion object {
|
companion object {
|
||||||
val defaultContext: Context = Context(null).apply {
|
val defaultContext: Context = Context(null).apply {
|
||||||
addFn("println") {
|
addFn("println") {
|
||||||
require(args.size == 1)
|
print("yn: ")
|
||||||
println(args[0].asStr.value)
|
for( (i,a) in args.withIndex() ) {
|
||||||
|
if( i > 0 ) print(' ' + a.asStr.value)
|
||||||
|
else print(a.asStr.value)
|
||||||
|
}
|
||||||
|
println()
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
addFn("floor") {
|
addFn("floor") {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package net.sergeych.ling
|
package net.sergeych.ling
|
||||||
|
|
||||||
data class Token(val value: String, val pos: Pos, val type: Type) {
|
data class Token(val value: String, val pos: Pos, val type: Type) {
|
||||||
|
val isComment: Boolean by lazy { type == Type.SINLGE_LINE_COMMENT || type == Type.MULTILINE_COMMENT }
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
enum class Type {
|
enum class Type {
|
||||||
ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
|
ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
|
||||||
@ -9,6 +11,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
EQ, NEQ, LT, LTE, GT, GTE,
|
EQ, NEQ, LT, LTE, GT, GTE,
|
||||||
AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT,
|
AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT,
|
||||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||||
|
LABEL,ATLABEL, // label@ at@label
|
||||||
|
NEWLINE,
|
||||||
EOF,
|
EOF,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ abstract class Statement(
|
|||||||
throw UnsupportedOperationException("not comparable")
|
throw UnsupportedOperationException("not comparable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Callable@${this.hashCode()}"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Statement.raise(text: String): Nothing {
|
fun Statement.raise(text: String): Nothing {
|
||||||
|
@ -47,6 +47,15 @@ class ScriptTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parserLabelsTest() {
|
||||||
|
val src = "label@ break@label".toSource()
|
||||||
|
val tt = parseLing(src)
|
||||||
|
assertEquals(Token("label", src.posAt(0, 0), Token.Type.LABEL), tt[0])
|
||||||
|
assertEquals(Token("break", src.posAt(0, 7), Token.Type.ID), tt[1])
|
||||||
|
assertEquals(Token("label", src.posAt(0, 12), Token.Type.ATLABEL), tt[2])
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parse0Test() {
|
fun parse0Test() {
|
||||||
val src = """
|
val src = """
|
||||||
@ -109,10 +118,14 @@ class ScriptTest {
|
|||||||
@Test
|
@Test
|
||||||
fun varsAndConstsTest() = runTest {
|
fun varsAndConstsTest() = runTest {
|
||||||
val context = Context()
|
val context = Context()
|
||||||
assertEquals(ObjVoid,context.eval("""
|
assertEquals(
|
||||||
|
ObjVoid, context.eval(
|
||||||
|
"""
|
||||||
val a = 17
|
val a = 17
|
||||||
var b = 3
|
var b = 3
|
||||||
""".trimIndent()))
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
assertEquals(17, context.eval("a").toInt())
|
assertEquals(17, context.eval("a").toInt())
|
||||||
assertEquals(20, context.eval("b + a").toInt())
|
assertEquals(20, context.eval("b + a").toInt())
|
||||||
assertFailsWith<ScriptError> {
|
assertFailsWith<ScriptError> {
|
||||||
@ -137,11 +150,13 @@ class ScriptTest {
|
|||||||
assertEquals(17, context.eval("foo(3)").toInt())
|
assertEquals(17, context.eval("foo(3)").toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
context.eval("""
|
context.eval(
|
||||||
|
"""
|
||||||
fn bar(a, b=10) {
|
fn bar(a, b=10) {
|
||||||
a + b + 1
|
a + b + 1
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
assertEquals(10, context.eval("bar(3, 6)").toInt())
|
assertEquals(10, context.eval("bar(3, 6)").toInt())
|
||||||
assertEquals(14, context.eval("bar(3)").toInt())
|
assertEquals(14, context.eval("bar(3)").toInt())
|
||||||
}
|
}
|
||||||
@ -238,20 +253,23 @@ class ScriptTest {
|
|||||||
fun ifTest() = runTest {
|
fun ifTest() = runTest {
|
||||||
// if - single line
|
// if - single line
|
||||||
var context = Context()
|
var context = Context()
|
||||||
context.eval("""
|
context.eval(
|
||||||
|
"""
|
||||||
fn test1(n) {
|
fn test1(n) {
|
||||||
var result = "more"
|
var result = "more"
|
||||||
if( n >= 10 )
|
if( n >= 10 )
|
||||||
result = "enough"
|
result = "enough"
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
assertEquals("enough", context.eval("test1(11)").toString())
|
assertEquals("enough", context.eval("test1(11)").toString())
|
||||||
assertEquals("more", context.eval("test1(1)").toString())
|
assertEquals("more", context.eval("test1(1)").toString())
|
||||||
|
|
||||||
// if - multiline (block)
|
// if - multiline (block)
|
||||||
context = Context()
|
context = Context()
|
||||||
context.eval("""
|
context.eval(
|
||||||
|
"""
|
||||||
fn test1(n) {
|
fn test1(n) {
|
||||||
var prefix = "answer: "
|
var prefix = "answer: "
|
||||||
var result = "more"
|
var result = "more"
|
||||||
@ -262,26 +280,30 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
prefix + result
|
prefix + result
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
assertEquals("answer: enough", context.eval("test1(11)").toString())
|
assertEquals("answer: enough", context.eval("test1(11)").toString())
|
||||||
assertEquals("answer: more", context.eval("test1(1)").toString())
|
assertEquals("answer: more", context.eval("test1(1)").toString())
|
||||||
|
|
||||||
// else single line1
|
// else single line1
|
||||||
context = Context()
|
context = Context()
|
||||||
context.eval("""
|
context.eval(
|
||||||
|
"""
|
||||||
fn test1(n) {
|
fn test1(n) {
|
||||||
if( n >= 10 )
|
if( n >= 10 )
|
||||||
"enough"
|
"enough"
|
||||||
else
|
else
|
||||||
"more"
|
"more"
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
assertEquals("enough", context.eval("test1(11)").toString())
|
assertEquals("enough", context.eval("test1(11)").toString())
|
||||||
assertEquals("more", context.eval("test1(1)").toString())
|
assertEquals("more", context.eval("test1(1)").toString())
|
||||||
|
|
||||||
// if/else with blocks
|
// if/else with blocks
|
||||||
context = Context()
|
context = Context()
|
||||||
context.eval("""
|
context.eval(
|
||||||
|
"""
|
||||||
fn test1(n) {
|
fn test1(n) {
|
||||||
if( n > 20 ) {
|
if( n > 20 ) {
|
||||||
"too much"
|
"too much"
|
||||||
@ -292,10 +314,150 @@ class ScriptTest {
|
|||||||
"more"
|
"more"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
assertEquals("enough", context.eval("test1(11)").toString())
|
assertEquals("enough", context.eval("test1(11)").toString())
|
||||||
assertEquals("more", context.eval("test1(1)").toString())
|
assertEquals("more", context.eval("test1(1)").toString())
|
||||||
assertEquals("too much", context.eval("test1(100)").toString())
|
assertEquals("too much", context.eval("test1(100)").toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lateInitTest() = runTest {
|
||||||
|
assertEquals(
|
||||||
|
"ok", eval(
|
||||||
|
"""
|
||||||
|
|
||||||
|
var late
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
late = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
late
|
||||||
|
""".trimIndent()
|
||||||
|
).toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun whileTest() = runTest {
|
||||||
|
assertEquals(
|
||||||
|
5.0,
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
var acc = 0
|
||||||
|
while( acc < 5 ) acc = acc + 0.5
|
||||||
|
acc
|
||||||
|
"""
|
||||||
|
).toDouble()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
5.0,
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
var acc = 0
|
||||||
|
// return from while
|
||||||
|
while( acc < 5 ) {
|
||||||
|
acc = acc + 0.5
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).toDouble()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
3.0,
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
var acc = 0
|
||||||
|
while( acc < 5 ) {
|
||||||
|
acc = acc + 0.5
|
||||||
|
if( acc >= 3 ) break
|
||||||
|
}
|
||||||
|
|
||||||
|
acc
|
||||||
|
|
||||||
|
"""
|
||||||
|
).toDouble()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
17.0,
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
var acc = 0
|
||||||
|
while( acc < 5 ) {
|
||||||
|
acc = acc + 0.5
|
||||||
|
if( acc >= 3 ) break 17
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).toDouble()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun whileNonLocalBreakTest() = runTest {
|
||||||
|
assertEquals(
|
||||||
|
"ok2:3:7", eval(
|
||||||
|
"""
|
||||||
|
var t1 = 10
|
||||||
|
outer@ while( t1 > 0 ) {
|
||||||
|
var t2 = 10
|
||||||
|
while( t2 > 0 ) {
|
||||||
|
t2 = t2 - 1
|
||||||
|
if( t2 == 3 && t1 == 7) {
|
||||||
|
break@outer "ok2:"+t2+":"+t1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t1 = t1 - 1
|
||||||
|
t1
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bookTest0() = runTest {
|
||||||
|
assertEquals(
|
||||||
|
"just 3",
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val count = 3
|
||||||
|
val res = if( count > 10 ) "too much" else "just " + count
|
||||||
|
println(res)
|
||||||
|
res
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"just 3",
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val count = 3
|
||||||
|
var res = if( count > 10 ) "too much" else "it's " + count
|
||||||
|
res = if( count > 10 ) "too much" else "just " + count
|
||||||
|
println(res)
|
||||||
|
res
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun bookTest1() = runTest {
|
||||||
|
// assertEquals(
|
||||||
|
// "just 3",
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val count = 3
|
||||||
|
println(
|
||||||
|
if( count > 10 ) "too much" else "just " + count
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
// .toString()
|
||||||
|
// )
|
||||||
|
}
|
||||||
}
|
}
|
177
library/src/jvmTest/kotlin/BookTest.kt
Normal file
177
library/src/jvmTest/kotlin/BookTest.kt
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.ling.Context
|
||||||
|
import net.sergeych.ling.ObjVoid
|
||||||
|
import java.nio.file.Files.readAllLines
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
fun leftMargin(s: String): Int {
|
||||||
|
var cnt = 0
|
||||||
|
for (c in s) {
|
||||||
|
when (c) {
|
||||||
|
' ' -> cnt++
|
||||||
|
'\t' -> cnt = (cnt / 4.0 + 0.9).toInt() * 4
|
||||||
|
else -> break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cnt
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DocTest(
|
||||||
|
val fileName: String,
|
||||||
|
val line: Int,
|
||||||
|
val code: String,
|
||||||
|
val expectedOutput: String,
|
||||||
|
val expectedResult: String,
|
||||||
|
val expectedError: String? = null
|
||||||
|
) {
|
||||||
|
val sourceLines by lazy { code.lines() }
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "DocTest:$fileName:${line+1}..${line + sourceLines.size}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val detailedString by lazy {
|
||||||
|
val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line}: $s" }.joinToString("\n")
|
||||||
|
"$this\n" +
|
||||||
|
codeWithLines + "\n" +
|
||||||
|
"--------expected output--------\n" +
|
||||||
|
expectedOutput +
|
||||||
|
"-----expected return value-----\n" +
|
||||||
|
expectedResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseDocTests(name: String): Flow<DocTest> = flow {
|
||||||
|
val book = readAllLines(Paths.get("../docs/tutorial.md"))
|
||||||
|
var startOffset = 0
|
||||||
|
val block = mutableListOf<String>()
|
||||||
|
var startIndex = 0
|
||||||
|
for ((index, l) in book.withIndex()) {
|
||||||
|
val off = leftMargin(l)
|
||||||
|
when {
|
||||||
|
off < startOffset && startOffset != 0 -> {
|
||||||
|
if (l.isBlank()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// end of block or just text:
|
||||||
|
if (block.isNotEmpty()) {
|
||||||
|
// check/create block
|
||||||
|
// 2 lines min
|
||||||
|
if (block.size > 1) {
|
||||||
|
// remove prefix
|
||||||
|
for ((i, s) in block.withIndex()) {
|
||||||
|
var x = s
|
||||||
|
// could be tabs :(
|
||||||
|
val initial = leftMargin(x)
|
||||||
|
do {
|
||||||
|
x = x.drop(1)
|
||||||
|
} while (initial - leftMargin(x) != startOffset)
|
||||||
|
block[i] = x
|
||||||
|
}
|
||||||
|
// println(block.joinToString("\n") { "${startIndex + ii++}: $it" })
|
||||||
|
val outStart = block.indexOfFirst { it.startsWith(">>>") }
|
||||||
|
if (outStart < 0) {
|
||||||
|
// println("No output at block from line ${startIndex+1}")
|
||||||
|
} else {
|
||||||
|
var isValid = true
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
while (block.size > outStart) {
|
||||||
|
val line = block.removeAt(outStart)
|
||||||
|
if (!line.startsWith(">>> ")) {
|
||||||
|
println("invalid output line, must start with '>>> ', block from ${startIndex + 1}: $line")
|
||||||
|
isValid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result.add(line.drop(4))
|
||||||
|
}
|
||||||
|
if (isValid) {
|
||||||
|
emit(
|
||||||
|
DocTest(
|
||||||
|
name, startIndex,
|
||||||
|
block.joinToString("\n"),
|
||||||
|
if (result.size > 1)
|
||||||
|
result.dropLast(1).joinToString { it + "\n" }
|
||||||
|
else "",
|
||||||
|
result.last()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// last line '>>>'
|
||||||
|
}
|
||||||
|
block.clear()
|
||||||
|
startOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
off != 0 && startOffset == 0 -> {
|
||||||
|
// start
|
||||||
|
block.clear()
|
||||||
|
startIndex = index
|
||||||
|
block.add(l)
|
||||||
|
startOffset = off
|
||||||
|
}
|
||||||
|
|
||||||
|
off != 0 -> {
|
||||||
|
block.add(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
off == 0 && startOffset == 0 -> {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw RuntimeException("Unexpected line: ($off/$startOffset) $l")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
suspend fun DocTest.test() {
|
||||||
|
val collectedOutput = StringBuilder()
|
||||||
|
val context = Context().apply {
|
||||||
|
addFn("println") {
|
||||||
|
for ((i, a) in args.withIndex()) {
|
||||||
|
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a)
|
||||||
|
collectedOutput.append('\n')
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var error: Throwable? = null
|
||||||
|
val result = try {
|
||||||
|
context.eval(code)
|
||||||
|
}
|
||||||
|
catch (e: Throwable) {
|
||||||
|
error = e
|
||||||
|
null
|
||||||
|
}?.toString()?.replace(Regex("@\\d+"), "@...")
|
||||||
|
|
||||||
|
if (error != null || expectedOutput != collectedOutput.toString() ||
|
||||||
|
expectedResult != result
|
||||||
|
) {
|
||||||
|
println("Test failed: ${this.detailedString}")
|
||||||
|
}
|
||||||
|
error?.let { fail(it.toString()) }
|
||||||
|
assertEquals(expectedOutput, collectedOutput.toString(), "script output do not match")
|
||||||
|
assertEquals(expectedResult, result.toString(), "script result does not match")
|
||||||
|
// println("OK: $this")
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testsFromTutorial() = runTest {
|
||||||
|
parseDocTests("../docs/tutorial.md").collect { dt ->
|
||||||
|
dt.test()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user