parentheses, more operators, some docs

This commit is contained in:
Sergey Chernov 2025-05-18 18:32:59 +04:00
parent 166b1fa0d5
commit 3dd98131e7
9 changed files with 266 additions and 58 deletions

View File

@ -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
View 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... |
| | |
| | |
| | |

View File

@ -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()
}

View File

@ -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() {

View File

@ -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()

View File

@ -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())
}

View File

@ -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)

View File

@ -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
}

View File

@ -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())
}
}