parentheses, more operators, some docs
This commit is contained in:
parent
166b1fa0d5
commit
3dd98131e7
@ -2,6 +2,8 @@
|
||||
|
||||
in the form of multiplatform library.
|
||||
|
||||
__current state of implementation and docs__: [docs/math.md].
|
||||
|
||||
## Why?
|
||||
|
||||
Designed to add scripting to kotlin multiplatform application in easy and efficient way. This is attempt to achieve what Lua is for C/++.
|
||||
|
56
docs/math.md
Normal file
56
docs/math.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Operators
|
||||
|
||||
## Precedence
|
||||
|
||||
Same as in C++.
|
||||
|
||||
| Priority | Operations |
|
||||
|:----------------:|--------------------------------------|
|
||||
| **Highest**<br>0 | power, not, calls, indexing, dot,... |
|
||||
| 1 | `%` `*` `/` |
|
||||
| 2 | `+` `-` |
|
||||
| 3 | bit shifts (NI) |
|
||||
| 4 | `<=>` (NI) |
|
||||
| 5 | `<=` `>=` `<` `>` (NI) |
|
||||
| 6 | `==` `!=` (NI) |
|
||||
| 7 | `&` (NI) |
|
||||
| 9 | `\|` (NI) |
|
||||
| 10 | `&&` |
|
||||
| 11<br/>lowest | `\|\|` |
|
||||
|
||||
- (NI) stands for not yet implemented.
|
||||
|
||||
## Operators
|
||||
|
||||
`+ - * / % `: if both operand is `Int`, calculates as int. Otherwise, as real.
|
||||
|
||||
## Round and range
|
||||
|
||||
The following functions return its argument if it is `Int`,
|
||||
or transformed `Real` otherwise.
|
||||
|
||||
| name | description |
|
||||
|----------|--------------------------------------------------------|
|
||||
| floor(x) | Computes the largest integer value not greater than x |
|
||||
| ceil(x) | Computes the least integer value value not less than x |
|
||||
| round(x) | Rounds x |
|
||||
| | |
|
||||
| | |
|
||||
|
||||
## Scientific functions
|
||||
|
||||
| name | meaning |
|
||||
|---------------------|---------|
|
||||
| `sin(x:Real): Real` | sine |
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
|
||||
## Scientific constant
|
||||
|
||||
| name | meaning |
|
||||
|----------------------|--------------|
|
||||
| `Math.PI: Real` or π | 3.1415926... |
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
@ -139,32 +139,34 @@ class Compiler {
|
||||
// todoL var?
|
||||
return when (t.type) {
|
||||
Token.Type.ID -> {
|
||||
parseVarAccess(t, tokens)
|
||||
when (t.value) {
|
||||
"void" -> statement(t.pos, true) { ObjVoid }
|
||||
"null" -> statement(t.pos, true) { ObjNull }
|
||||
else -> parseVarAccess(t, tokens)
|
||||
}
|
||||
}
|
||||
Token.Type.LPAREN -> {
|
||||
// ( subexpr )
|
||||
parseExpression(tokens)?.also {
|
||||
val tl = tokens.next()
|
||||
if( tl.type != Token.Type.RPAREN )
|
||||
throw ScriptError(t.pos, "unbalanced parenthesis: no ')' for it")
|
||||
}
|
||||
}
|
||||
// todoL: check if it's a function call
|
||||
// todoL: check if it's a field access
|
||||
// todoL: check if it's a var
|
||||
// todoL: check if it's a const
|
||||
// todoL: check if it's a type
|
||||
|
||||
// "+" -> statement { parseNumber(true,tokens) }??????
|
||||
// "-" -> statement { parseNumber(false,tokens) }
|
||||
// "~" -> statement(t.pos) { ObjInt( parseLong(tokens)) }
|
||||
|
||||
Token.Type.PLUS -> {
|
||||
val n = parseNumber(true, tokens)
|
||||
statement(t.pos) { n }
|
||||
statement(t.pos, true) { n }
|
||||
}
|
||||
|
||||
Token.Type.MINUS -> {
|
||||
val n = parseNumber(false, tokens)
|
||||
statement(t.pos) { n }
|
||||
statement(t.pos, true) { n }
|
||||
}
|
||||
|
||||
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
|
||||
tokens.previous()
|
||||
val n = parseNumber(true, tokens)
|
||||
statement(t.pos) { n }
|
||||
statement(t.pos, true) { n }
|
||||
}
|
||||
|
||||
else -> null
|
||||
@ -211,7 +213,7 @@ class Compiler {
|
||||
} while (t.type != Token.Type.RPAREN)
|
||||
|
||||
statement(id.pos) { context ->
|
||||
val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id")
|
||||
val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined function: ${id.value}")
|
||||
(v.value as? Statement)?.execute(
|
||||
context.copy(
|
||||
Arguments(
|
||||
@ -232,7 +234,7 @@ class Compiler {
|
||||
// just access the var
|
||||
tokens.previous()
|
||||
statement(id.pos) {
|
||||
val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id")
|
||||
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")
|
||||
}
|
||||
}
|
||||
@ -393,27 +395,26 @@ class Compiler {
|
||||
)
|
||||
|
||||
val allOps = listOf(
|
||||
// Operator("||", 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) })
|
||||
Operator("&&", 1, 2) { pos, a, b ->
|
||||
LogicalAndStatement(pos, a, b)
|
||||
},
|
||||
Operator("||", 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) },
|
||||
Operator("&&", 1, 2) { pos, a, b -> LogicalAndStatement(pos, a, b) },
|
||||
// bitwise or 2
|
||||
// bitwise and 3
|
||||
// equality/ne 4
|
||||
// relational <=,... 5
|
||||
// shuttle <=> 6
|
||||
// bitshhifts 7
|
||||
// + - : 7
|
||||
Operator("+", 7, 2) { pos, a, b ->
|
||||
Operator("+", 8, 2) { pos, a, b ->
|
||||
PlusStatement(pos, a, b)
|
||||
},
|
||||
Operator("-", 7, 2) { pos, a, b ->
|
||||
Operator("-", 8, 2) { pos, a, b ->
|
||||
MinusStatement(pos, a, b)
|
||||
},
|
||||
// * / %: 8
|
||||
Operator("*", 9, 2) { pos, a, b -> MulStatement(pos, a, b) },
|
||||
Operator("/", 9, 2) { pos, a, b -> DivStatement(pos, a, b) },
|
||||
Operator("%", 9, 2) { pos, a, b -> ModStatement(pos, a, b) },
|
||||
)
|
||||
val lastLevel = 9
|
||||
val byLevel: List<Map<String, Operator>> = (0..<lastLevel).map { l ->
|
||||
val lastLevel = 10
|
||||
val byLevel: List<Map<String, Operator>> = (0..< lastLevel).map { l ->
|
||||
allOps.filter { it.priority == l }
|
||||
.map { it.name to it }.toMap()
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ sealed class Obj {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun <reified T> T.toObj(): Obj = Obj.from(this)
|
||||
|
||||
@Serializable
|
||||
@SerialName("void")
|
||||
object ObjVoid: Obj() {
|
||||
|
@ -13,7 +13,7 @@ fun parseLing(source: Source): List<Token> {
|
||||
do {
|
||||
val t = p.nextToken()
|
||||
tokens += t
|
||||
} while(t.type != Token.Type.EOF)
|
||||
} while (t.type != Token.Type.EOF)
|
||||
return tokens
|
||||
}
|
||||
|
||||
@ -42,28 +42,45 @@ private class Parser(fromPos: Pos) {
|
||||
',' -> Token(",", from, Token.Type.COMMA)
|
||||
';' -> Token(";", from, Token.Type.SEMICOLON)
|
||||
'=' -> {
|
||||
if( pos.currentChar == '=') {
|
||||
if (pos.currentChar == '=') {
|
||||
advance()
|
||||
Token("==", from, Token.Type.EQ)
|
||||
}
|
||||
else
|
||||
} else
|
||||
Token("=", from, Token.Type.ASSIGN)
|
||||
}
|
||||
|
||||
'+' -> Token("+", from, Token.Type.PLUS)
|
||||
'-' -> Token("-", from, Token.Type.MINUS)
|
||||
'*' -> Token("*", from, Token.Type.STAR)
|
||||
'/' -> Token("/", from, Token.Type.SLASH)
|
||||
'%' -> Token("%", from, Token.Type.PERCENT)
|
||||
'.' -> Token(".", from, Token.Type.DOT)
|
||||
'<' -> Token("<", from, Token.Type.LT)
|
||||
'>' -> Token(">", from, Token.Type.GT)
|
||||
'!' -> Token("!", from, Token.Type.NOT)
|
||||
'|' -> {
|
||||
if (currentChar == '|') {
|
||||
advance()
|
||||
Token("||", from, Token.Type.OR)
|
||||
} else
|
||||
Token("|", from, Token.Type.BITOR)
|
||||
}
|
||||
'&' -> {
|
||||
if (currentChar == '&') {
|
||||
advance()
|
||||
Token("&&", from, Token.Type.AND)
|
||||
} else
|
||||
Token("&", from, Token.Type.BITAND)
|
||||
}
|
||||
|
||||
'"' -> loadStringToken()
|
||||
in digits -> {
|
||||
pos.back()
|
||||
decodeNumber(loadChars(digits), from)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if( ch.isLetter() || ch == '_' )
|
||||
if (ch.isLetter() || ch == '_')
|
||||
Token(ch + loadChars(idNextChars), from, Token.Type.ID)
|
||||
else
|
||||
raise("can't parse token")
|
||||
@ -72,46 +89,43 @@ private class Parser(fromPos: Pos) {
|
||||
}
|
||||
|
||||
private fun decodeNumber(p1: String, start: Pos): Token =
|
||||
if( pos.end )
|
||||
if (pos.end)
|
||||
Token(p1, start, Token.Type.INT)
|
||||
else if( currentChar == '.' ) {
|
||||
else if (currentChar == '.') {
|
||||
// could be decimal
|
||||
advance()
|
||||
if( currentChar in digits ) {
|
||||
if (currentChar in digits) {
|
||||
// decimal part
|
||||
val p2 = loadChars(digits)
|
||||
// with exponent?
|
||||
if( currentChar == 'e' || currentChar == 'E') {
|
||||
if (currentChar == 'e' || currentChar == 'E') {
|
||||
advance()
|
||||
var negative = false
|
||||
if(currentChar == '+' )
|
||||
if (currentChar == '+')
|
||||
advance()
|
||||
else if(currentChar == '-') {
|
||||
else if (currentChar == '-') {
|
||||
negative = true
|
||||
advance()
|
||||
}
|
||||
var p3 = loadChars(digits)
|
||||
if( negative ) p3 = "-$p3"
|
||||
if (negative) p3 = "-$p3"
|
||||
Token("$p1.${p2}e$p3", start, Token.Type.REAL)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// no exponent
|
||||
Token("$p1.$p2", start, Token.Type.REAL)
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// not decimal
|
||||
// something like 10.times, method call on integer number
|
||||
pos.back()
|
||||
Token(p1, start, Token.Type.INT)
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// could be integer, also hex:
|
||||
if (currentChar == 'x' && p1 == "0") {
|
||||
advance()
|
||||
Token(loadChars(hexDigits), start, Token.Type.HEX).also {
|
||||
if( currentChar.isLetter() )
|
||||
if (currentChar.isLetter())
|
||||
raise("invalid hex literal")
|
||||
}
|
||||
} else {
|
||||
@ -130,7 +144,7 @@ private class Parser(fromPos: Pos) {
|
||||
|
||||
val sb = StringBuilder()
|
||||
while (currentChar != '"') {
|
||||
if( pos.end ) raise("unterminated string")
|
||||
if (pos.end) raise("unterminated string")
|
||||
when (currentChar) {
|
||||
'\\' -> {
|
||||
advance() ?: raise("unterminated string")
|
||||
@ -142,6 +156,7 @@ private class Parser(fromPos: Pos) {
|
||||
else -> sb.append('\\').append(currentChar)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
sb.append(currentChar)
|
||||
advance()
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.*
|
||||
|
||||
class Script(
|
||||
override val pos: Pos,
|
||||
@ -26,6 +25,21 @@ class Script(
|
||||
println(args[0].asStr.value)
|
||||
ObjVoid
|
||||
}
|
||||
addFn("floor") {
|
||||
val x = args.firstAndOnly()
|
||||
if( x is ObjInt ) x
|
||||
else ObjReal(floor(x.toDouble()))
|
||||
}
|
||||
addFn("ceil") {
|
||||
val x = args.firstAndOnly()
|
||||
if( x is ObjInt ) x
|
||||
else ObjReal(ceil(x.toDouble()))
|
||||
}
|
||||
addFn("round") {
|
||||
val x = args.firstAndOnly()
|
||||
if( x is ObjInt ) x
|
||||
else ObjReal(round(x.toDouble()))
|
||||
}
|
||||
addFn("sin") {
|
||||
sin(args.firstAndOnly().toDouble())
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
||||
enum class Type {
|
||||
ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
|
||||
SEMICOLON, COLON, EQ, PLUS, MINUS, STAR, SLASH, ASSIGN, EQEQ, NEQ, LT, LTEQ, GT,
|
||||
GTEQ, AND, OR, NOT, DOT, ARROW, QUESTION, COLONCOLON, EOF,
|
||||
GTEQ, AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT,
|
||||
EOF,
|
||||
}
|
||||
companion object {
|
||||
// fun eof(parser: Parser) = Token("", parser.currentPos, Type.EOF)
|
||||
|
@ -3,7 +3,9 @@ package net.sergeych.ling
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
|
||||
|
||||
abstract class Statement : Obj() {
|
||||
abstract class Statement(
|
||||
@Suppress("unused") val isStaticConst: Boolean = false
|
||||
) : Obj() {
|
||||
abstract val pos: Pos
|
||||
abstract suspend fun execute(context: Context): Obj
|
||||
}
|
||||
@ -17,10 +19,11 @@ fun Statement.require(cond: Boolean, message: () -> String) {
|
||||
if (!cond) raise(message())
|
||||
}
|
||||
|
||||
fun statement(pos: Pos, f: suspend (Context) -> Obj): Statement = object : Statement() {
|
||||
override val pos: Pos = pos
|
||||
override suspend fun execute(context: Context): Obj = f(context)
|
||||
}
|
||||
fun statement(pos: Pos, isStaticConst: Boolean = false, f: suspend (Context) -> Obj): Statement =
|
||||
object : Statement(isStaticConst) {
|
||||
override val pos: Pos = pos
|
||||
override suspend fun execute(context: Context): Obj = f(context)
|
||||
}
|
||||
|
||||
class LogicalAndStatement(
|
||||
override val pos: Pos,
|
||||
@ -29,15 +32,31 @@ class LogicalAndStatement(
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
|
||||
val l = left.execute(context).let {
|
||||
(it as? ObjBool) ?: raise("logical and: left operand is not boolean: $it")
|
||||
(it as? ObjBool) ?: raise("left operand is not boolean: $it")
|
||||
}
|
||||
val r = right.execute(context).let {
|
||||
(it as? ObjBool) ?: raise("logical and: right operand is not boolean: $it")
|
||||
(it as? ObjBool) ?: raise("right operand is not boolean: $it")
|
||||
}
|
||||
return ObjBool(l.value && r.value)
|
||||
}
|
||||
}
|
||||
|
||||
class LogicalOrStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
|
||||
val l = left.execute(context).let {
|
||||
(it as? ObjBool) ?: raise("left operand is not boolean: $it")
|
||||
}
|
||||
val r = right.execute(context).let {
|
||||
(it as? ObjBool) ?: raise("right operand is not boolean: $it")
|
||||
}
|
||||
return ObjBool(l.value || r.value)
|
||||
}
|
||||
}
|
||||
|
||||
class PlusStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
@ -84,11 +103,73 @@ class MinusStatement(
|
||||
}
|
||||
}
|
||||
|
||||
class MulStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val l = left.execute(context)
|
||||
if (l !is Numeric)
|
||||
raise("left operand is not number: $l")
|
||||
|
||||
val r = right.execute(context)
|
||||
if (r !is Numeric)
|
||||
raise("right operand is not number: $r")
|
||||
|
||||
return if (l is ObjInt && r is ObjInt)
|
||||
ObjInt(l.longValue * r.longValue)
|
||||
else
|
||||
ObjReal(l.doubleValue * r.doubleValue)
|
||||
}
|
||||
}
|
||||
|
||||
class DivStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val l = left.execute(context)
|
||||
if (l !is Numeric)
|
||||
raise("left operand is not number: $l")
|
||||
|
||||
val r = right.execute(context)
|
||||
if (r !is Numeric)
|
||||
raise("right operand is not number: $r")
|
||||
|
||||
return if (l is ObjInt && r is ObjInt)
|
||||
ObjInt(l.longValue / r.longValue)
|
||||
else
|
||||
ObjReal(l.doubleValue / r.doubleValue)
|
||||
}
|
||||
}
|
||||
|
||||
class ModStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val l = left.execute(context)
|
||||
if (l !is Numeric)
|
||||
raise("left operand is not number: $l")
|
||||
|
||||
val r = right.execute(context)
|
||||
if (r !is Numeric)
|
||||
raise("right operand is not number: $r")
|
||||
|
||||
return if (l is ObjInt && r is ObjInt)
|
||||
ObjInt(l.longValue % r.longValue)
|
||||
else
|
||||
ObjReal(l.doubleValue % r.doubleValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AssignStatement(override val pos: Pos, val name: String, val value: Statement) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val variable = context[name] ?: raise("can't assign: variable does not exist: $name")
|
||||
if( !variable.isMutable )
|
||||
throw ScriptError(pos,"can't reassign val $name")
|
||||
if (!variable.isMutable)
|
||||
throw ScriptError(pos, "can't reassign val $name")
|
||||
variable.value = value.execute(context)
|
||||
return ObjVoid
|
||||
}
|
||||
|
@ -163,4 +163,39 @@ class ScriptTest {
|
||||
assertEquals(37, context.eval("foo(3,14)").toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullAndVoidTest() = runTest {
|
||||
val context = Context()
|
||||
assertEquals(ObjVoid,context.eval("void"))
|
||||
assertEquals(ObjNull,context.eval("null"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testArithmeticOperators() = runTest {
|
||||
assertEquals(2, eval("5/2").toInt())
|
||||
assertEquals(2.5, eval("5.0/2").toDouble())
|
||||
assertEquals(2.5, eval("5/2.0").toDouble())
|
||||
assertEquals(2.5, eval("5.0/2.0").toDouble())
|
||||
|
||||
assertEquals(1, eval("5%2").toInt())
|
||||
assertEquals(1.0, eval("5.0%2").toDouble())
|
||||
|
||||
assertEquals(77, eval("11 * 7").toInt())
|
||||
|
||||
assertEquals(2.0, eval("floor(5.0/2)").toDouble())
|
||||
assertEquals(3, eval("ceil(5.0/2)").toInt())
|
||||
|
||||
assertEquals(2.0, eval("round(4.7/2)").toDouble())
|
||||
assertEquals(3.0, eval("round(5.1/2)").toDouble())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testArithmeticParenthesis() = runTest {
|
||||
assertEquals(17, eval("2 + 3 * 5").toInt())
|
||||
assertEquals(17, eval("2 + (3 * 5)").toInt())
|
||||
assertEquals(25, eval("(2 + 3) * 5").toInt())
|
||||
assertEquals(24, eval("(2 + 3) * 5 -1").toInt())
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user