diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..f6a2fc6 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,159 @@ +# Ling tutorial + +Ling is a very simple language, where we take only most important and popular features from +other scripts and languages. In particular, we adopt _principle of minimal confusion_[^1]. +In other word, the code usually works as expected when you see it. So, nothing unusual. + +# Expressions and blocks. + +Everything is an expression in Ling. Even an empty block: + + { + // empty block + } + >>> void + +Block returns it last expression as "return value": + + { + 2 + 2 + 3 + 3 + } + >>> 6 + +Same is without block: + + 3 + 3 + >>> 6 + +If you don't want block to return anything, use `void`: + + { + 3 + 4 + void + } + >>> void + +Every construction is an expression that returns something (or `void`): + + val limited = if( x > 100 ) 100 else x + +You can use blocks in if statement, as expected: + + val limited = if( x > 100 ) { + 100 + x * 0.1 + } + else + x + +So the principles are: + +- everything is an expression returning its last calculated value or `void` +- expression could be a `{ block }` + +## Expression details + +It is rather simple, like everywhere else: + + sin(x * π/4) / 2.0 + +See [math](math.md) for more on it. + +# Defining functions + + fun check(amount) { + if( amount > 100 ) + "anough" + else + "more" + } + +You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_. + +There are default parameters in Ling: + + fn check(amount, prefix = "answer: ") { + prefix + if( amount > 100 ) + "anough" + else + "more" + } + +## Closures + +Each __block has an isolated context that can be accessed from closures__. For example: + + var counter = 1 + + // this is ok: coumter is incremented + def increment(amount=1) { + // use counter from a closure: + counter = counter + amount + } + + val taskAlias = def someTask() { + // this obscures global outer var with a local one + var counter = 0 + // ... + counter = 1 + // ... + counter + } + +As was told, `def` statement return callable for the function, it could be used as a parameter, or elsewhere +to call it: + + // call the callable stored in the var + taskAlias() + // or directly: + someTask() + +If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?) + +# Integral data types + +| type | description | literal samples | +|--------|---------------------------------|---------------------| +| Int | 64 bit signed | `1` `-22` `0x1FF` | +| Real | 64 bit double | `1.0`, `2e-11` | +| Bool | boolean | `true` `false` | +| String | unicode string, no limits | "hello" (see below) | +| Void | no value could exist, singleton | void | +| Null | missing value, singleton | null | +| Fn | callable type | | + +## String details + +### String operations + +Concatenation is a `+`: `"hello " + name` works as expected. No confusion. + +### Literals + +String literal could be multiline: + + " + Hello, + World! + " + >>> "Hello + World" + +In that case compiler removes left margin and first/last empty lines. Note that it won't remove margin: + + "Hello, + World + " + >>> "Hello, + World + " + +because the first line has no margin in the literal. + +# Comments + + // single line comment + var result = null // here we will store the result + + + diff --git a/library/build.gradle.kts b/library/build.gradle.kts index c44fff3..6ad1cb7 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -1,5 +1,6 @@ import com.vanniktech.maven.publish.SonatypeHost import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -29,6 +30,11 @@ kotlin { browser() nodejs() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs() { + browser() + nodejs() + } sourceSets { all { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index ba891b7..f1fcdb7 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -88,8 +88,15 @@ class Compiler { } } + Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue + Token.Type.SEMICOLON -> continue + Token.Type.LBRACE -> { + tokens.previous() + parseBlock(tokens) + } + Token.Type.RBRACE -> { tokens.previous() return null @@ -276,9 +283,49 @@ class Compiler { "val" -> parseVarDeclaration(id.value, false, tokens) "var" -> parseVarDeclaration(id.value, true, tokens) "fn", "fun" -> parseFunctionDeclaration(tokens) + "if" -> parseIfStatement(tokens) else -> null } + private fun parseIfStatement(tokens: ListIterator): Statement { + var t = tokens.next() + val start = t.pos + if( t.type != Token.Type.LPAREN) + throw ScriptError(t.pos, "Bad if statement: expected '('") + + val condition = parseExpression(tokens) + ?: throw ScriptError(t.pos, "Bad if statement: expected expression") + + t = tokens.next() + 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") + + // could be else block: + val t2 = tokens.next() + + // we generate different statements: optimization + return if( t2.type == Token.Type.ID && t2.value == "else") { + val elseBody = parseStatement(tokens) ?: throw ScriptError(t.pos, "Bad else statement: expected statement") + return statement(start) { + if (condition.execute(it).toBool()) + ifBody.execute(it) + else + elseBody.execute(it) + } + } + else { + tokens.previous() + statement(start) { + if (condition.execute(it).toBool()) + ifBody.execute(it) + else + ObjVoid + } + } + } + data class FnParamDef( val name: String, val pos: Pos, @@ -317,8 +364,6 @@ class Compiler { params.add(FnParamDef(t.value, t.pos, defaultValue)) } while (true) - println("arglist: $params") - // Here we should be at open body val fnStatements = parseBlock(tokens) @@ -351,7 +396,11 @@ class Compiler { val t = tokens.next() if (t.type != Token.Type.LBRACE) throw ScriptError(t.pos, "Expected block body start: {") - return parseScript(t.pos, tokens).also { + val block = parseScript(t.pos, tokens) + return statement(t.pos) { + // block run on inner context: + block.execute(it.copy()) + }.also { val t1 = tokens.next() if (t1.type != Token.Type.RBRACE) throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }") @@ -367,7 +416,7 @@ class Compiler { var setNull = false if (eqToken.type != Token.Type.ASSIGN) { if (!mutable) - throw ScriptError(eqToken.pos, "Expected initializator: '=' after '$kind ${name}'") + throw ScriptError(eqToken.pos, "Expected initializer: '=' after '$kind ${name}'") else { tokens.previous() setNull = true diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index 0a2fdfd..b1584c2 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -52,7 +52,14 @@ private class Parser(fromPos: Pos) { '+' -> Token("+", from, Token.Type.PLUS) '-' -> Token("-", from, Token.Type.MINUS) '*' -> Token("*", from, Token.Type.STAR) - '/' -> Token("/", from, Token.Type.SLASH) + '/' -> { + if( currentChar == '/') { + advance() + Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT) + } + else + Token("/", from, Token.Type.SLASH) + } '%' -> Token("%", from, Token.Type.PERCENT) '.' -> Token(".", from, Token.Type.DOT) '<' -> { @@ -206,6 +213,31 @@ private class Parser(fromPos: Pos) { return result.toString() } + @Suppress("unused") + private fun loadUntil(endChars: Set): String { + return if (pos.end) "" + else { + val result = StringBuilder() + while (!pos.end) { + val ch = pos.currentChar + if (ch in endChars) break + result.append(ch) + pos.advance() + } + result.toString() + } + } + + private fun loadToEnd(): String { + val result = StringBuilder() + val l = pos.line + do { + result.append(pos.currentChar) + advance() + } while (pos.line == l) + return result.toString() + } + /** * next non-whitespace char (newline are skipped too) or null if EOF */ diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index f2bde45..fd7f58a 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -8,6 +8,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) { PLUS, MINUS, STAR, SLASH, ASSIGN, EQ, NEQ, LT, LTE, GT, GTE, AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT, + SINLGE_LINE_COMMENT, MULTILINE_COMMENT, EOF, } diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 35e1eee..32abce6 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -236,7 +236,66 @@ class ScriptTest { @Test fun ifTest() = runTest { + // if - single line + var context = Context() + context.eval(""" + fn test1(n) { + var result = "more" + if( n >= 10 ) + result = "enough" + result + } + """.trimIndent()) + assertEquals("enough", context.eval("test1(11)").toString()) + assertEquals("more", context.eval("test1(1)").toString()) + // if - multiline (block) + context = Context() + context.eval(""" + fn test1(n) { + var prefix = "answer: " + var result = "more" + if( n >= 10 ) { + var prefix = "bad:" // local prefix + prefix = "too bad:" + result = "enough" + } + prefix + result + } + """.trimIndent()) + assertEquals("answer: enough", context.eval("test1(11)").toString()) + assertEquals("answer: more", context.eval("test1(1)").toString()) + + // else single line1 + context = Context() + context.eval(""" + fn test1(n) { + if( n >= 10 ) + "enough" + else + "more" + } + """.trimIndent()) + assertEquals("enough", context.eval("test1(11)").toString()) + assertEquals("more", context.eval("test1(1)").toString()) + + // if/else with blocks + context = Context() + context.eval(""" + fn test1(n) { + if( n > 20 ) { + "too much" + } else if( n >= 10 ) { + "enough" + } + else { + "more" + } + } + """.trimIndent()) + assertEquals("enough", context.eval("test1(11)").toString()) + assertEquals("more", context.eval("test1(1)").toString()) + assertEquals("too much", context.eval("test1(100)").toString()) } } \ No newline at end of file