diff --git a/docs/tutorial.md b/docs/tutorial.md index be489b1..697e510 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1385,6 +1385,7 @@ Typical set of String functions includes: | trim() | trim space chars from both ends | | startsWith(prefix) | true if starts with a prefix | | endsWith(prefix) | true if ends with a prefix | +| last() | get last character of a string or throw | | take(n) | get a new string from up to n first characters | | takeLast(n) | get a new string from up to n last characters | | drop(n) | get a new string dropping n first chars, or empty string | diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index 93c8c4f..416329e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -213,25 +213,40 @@ private class Parser(fromPos: Pos) { '!' -> { if (currentChar == 'i') { + // Potentially !in / !is, but only if a word boundary follows pos.advance() when (currentChar) { 'n' -> { pos.advance() - Token("!in", from, Token.Type.NOTIN) + // if next char continues an identifier, it's actually '!'+identifier starting with "in..." + if (idNextChars(currentChar)) { + // backtrack to right after '!' + pos.back() + pos.back() + Token("!", from, Token.Type.NOT) + } else + Token("!in", from, Token.Type.NOTIN) } 's' -> { pos.advance() - Token("!is", from, Token.Type.NOTIS) + // if next char continues an identifier, it's actually '!'+identifier starting with "is..." + if (idNextChars(currentChar)) { + // backtrack to right after '!' + pos.back() + pos.back() + Token("!", from, Token.Type.NOT) + } else + Token("!is", from, Token.Type.NOTIS) } else -> { + // it was just '!i' followed by something else; revert one step and return '!' pos.back() Token("!", from, Token.Type.NOT) } } - } else - if (currentChar == '=') { + } else if (currentChar == '=') { pos.advance() if (currentChar == '=') { pos.advance() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt index f97a648..b99a594 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt @@ -173,6 +173,9 @@ data class ObjString(val value: String) : Obj() { thisAs().value.map { ObjChar(it) }.toMutableList() ) } + addFn("last") { + ObjChar(thisAs().value.lastOrNull() ?: raiseNoSuchElement("empty string")) + } addFn("encodeUtf8") { ObjBuffer(thisAs().value.encodeToByteArray().asUByteArray()) } addFn("size") { ObjInt(thisAs().value.length.toLong()) } addFn("toReal") { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index e7577ef..8bc04c2 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4012,4 +4012,41 @@ class ScriptTest { """.trimIndent()).decodeSerializable() assertEquals( TestJson4(TestEnum.One), x) } + + @Test + fun testLogicalNot() = runTest { + eval(""" + val vf = false + fun f() { false } + assert( !false ) + assert( !vf ) + assert( !f() ) + + val vt = true + fun ft() { true } + if( !true ) + throw "impossible" + + if( !ft() ) + throw "impossible" + + if( !vt ) + throw "impossible" + + // real world sample + + fun isSignedByAdmin() { + // just ok + true + } + + fun requireAdmin() { + // this caused compilation error: + if( !isSignedByAdmin() ) + throw "Admin signature required" + } + + """.trimIndent() + ) + } }