arrays started: literals, size, splats, index access RW

This commit is contained in:
Sergey Chernov 2025-05-30 01:13:40 +04:00
parent f881faf89f
commit c18345823b
12 changed files with 361 additions and 137 deletions

24
docs/List.md Normal file
View File

@ -0,0 +1,24 @@
# List built-in class
Mutable list of any objects.
It's class in Ling is `List`:
[1,2,3]::class
>>> List
you can use it's class to ensure type:
[]::class == List
>>> true
## Members
| name | meaning | type |
|---------|-------------------------------|------|
| `.size` | property returns current size | Int |
| | | |
| | | |
| | | |
| | | |
| | | |

View File

@ -15,11 +15,11 @@ you can use it's class to ensure type:
## Member functions
| name | meaning | type |
|--------------|------------------------------------|------|
| `roundToInt` | round to nearest int like round(x) | Int |
| | | |
| | | |
| | | |
| | | |
| | | |
| name | meaning | type |
|-----------------|------------------------------------|------|
| `.roundToInt()` | round to nearest int like round(x) | Int |
| | | |
| | | |
| | | |
| | | |
| | | |

View File

@ -103,8 +103,7 @@ Notice the parentheses here: the assignment has low priority!
These operators return rvalue, unmodifiable.
## Assignemnt return r-value!
## Assignment return r-value!
## Math
@ -205,7 +204,7 @@ There are default parameters in Ling:
}
assert( "do: more" == check(10, "do: ") )
check(120)
>>> answer: enough
>>> "answer: enough"
## Closures
@ -245,6 +244,43 @@ to call it:
If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?)
# Lists (arrays)
Ling has built-in mutable array class `List` with simple literals:
[1, "two", 3.33].size
>>> 3
Lists can contain any type of objects, lists too:
val list = [1, [2, 3], 4]
assert(list.size == 3)
// second element is a list too:
assert(list[1].size == 2)
>>> void
Notice usage of indexing.
When you want to "flatten" it to single array, you can use splat syntax:
[1, ...[2,3], 4]
>>> [1, 2, 3, 4]
Of course, you can splat from anything that is List (or list-like, but it will be defined later):
val a = ["one", "two"]
val b = [10.1, 20.2]
["start", ...b, ...a, "end"]
>>> ["start", 10.1, 20.2, "one", "two", "end"]
Of course, you can set any array element:
val a = [1, 2, 3]
a[1] = 200
a
>>> [1, 200, 3]
# Flow control operators
## if-then-else
@ -299,7 +335,7 @@ exit value in the case:
count = ++count * 10
"wrong "+count
}
>>> too much
>>> "too much"
### Breaking nested loops
@ -319,7 +355,7 @@ If you have several loops and want to exit not the inner one, use labels:
count = count + 1
count * 10
}
>>> 5/2 situation
>>> "5/2 situation"
### and continue
@ -333,7 +369,7 @@ We can skip the rest of the loop and restart it, as usual, with `continue` opera
countEven = countEven + 1
}
"found even numbers: " + countEven
>>> found even numbers: 5
>>> "found even numbers: 5"
`continue` can't "return" anything: it just restarts the loop. It can use labeled loops to restart outer ones:

View File

@ -91,7 +91,7 @@ class Compiler(
private fun parseExpressionLevel(tokens: CompilerContext, level: Int = 0): Accessor? {
if (level == lastLevel)
return parseTerm3(tokens)
return parseTerm(tokens)
var lvalue = parseExpressionLevel(tokens, level + 1)
if (lvalue == null) return null
@ -113,80 +113,21 @@ class Compiler(
}
/*
Term compiler
Fn calls could be:
1) Fn(...)
2) thisObj.method(...)
1 is a shortcut to this.Fn(...)
In general, we can assume any Fn to be of the same king, with `this` that always exist, and set by invocation.
In the case of (1), so called regular, or not bound function, it takes current this from the context.
In the case of (2), bound function, it creates sub-context binding thisObj to `this` in it.
Suppose we do regular parsing. THen we get lparam = statement, and parse to the `(`. Now we have to
compile the invocation of lparam, which can be thisObj.method or just method(). Unfortunately we
already compiled it and can't easily restore its type, so we have to parse it different way.
EBNF to parse term having lparam.
boundcall = "." , identifier, "("
We then call instance method bound to `lparam`.
call = "(', args, ")
we treat current lparam as callable and invoke it on the current context with current value of 'this.
Just traversing fields:
traverse = ".", not (identifier , ".")
Other cases to parse:
index = lparam, "[" , ilist , "]"
*/
/**
* Lower level of expr:
*
* assigning expressions:
*
* expr = expr: assignment
* ++expr, expr++, --expr, expr--,
*
* update-assigns:
* expr += expr, ...
*
* Dot!: expr , '.', ID
* Lambda: { <expr> }
* index: expr[ ilist ]
* call: <expr>( ilist )
* self updating: ++expr, expr++, --expr, expr--, expr+=<expr>,
* expr-=<expr>, expr*=<expr>, expr/=<expr>
* read expr: <expr>
*/
private fun parseTerm3(cc: CompilerContext): Accessor? {
private fun parseTerm(cc: CompilerContext): Accessor? {
var operand: Accessor? = null
while (true) {
val t = cc.next()
val startPos = t.pos
when (t.type) {
Token.Type.NEWLINE, Token.Type.SEMICOLON, Token.Type.EOF -> {
Token.Type.NEWLINE, Token.Type.SEMICOLON, Token.Type.EOF, Token.Type.RBRACE, Token.Type.COMMA -> {
cc.previous()
return operand
}
Token.Type.NOT -> {
if (operand != null) throw ScriptError(t.pos, "unexpected operator not '!'")
val op = parseTerm3(cc) ?: throw ScriptError(t.pos, "Expecting expression")
val op = parseTerm(cc) ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly }
}
@ -243,6 +184,45 @@ class Compiler(
}
}
Token.Type.LBRACKET -> {
operand?.let { left ->
// array access
val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression")
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
operand = Accessor({ cxt ->
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
?: cxt.raiseError("index must be integer")
left.getter(cxt).value.getAt(cxt, i).asMutable
}) { cxt, newValue ->
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
?: cxt.raiseError("index must be integer")
left.getter(cxt).value.putAt(cxt, i, newValue)
}
} ?: run {
// array literal
val entries = parseArrayLiteral(cc)
// if it didn't throw, ot parsed ot and consumed it all
operand = Accessor { cxt ->
val list = mutableListOf<Obj>()
for (e in entries) {
when(e) {
is ListEntry.Element -> {
list += e.accessor.getter(cxt).value
}
is ListEntry.Spread -> {
val elements=e.accessor.getter(cxt).value
when {
elements is ObjList -> list.addAll(elements.list)
else -> cxt.raiseError("Spread element must be list")
}
}
}
}
ObjList(list).asReadonly
}
}
}
Token.Type.ID -> {
// there could be terminal operators or keywords:// variable to read or like
when (t.value) {
@ -276,9 +256,6 @@ class Compiler(
operand = parseAccessor(cc)
}
}
// selector: <lvalue>, '.' , <id>
// we replace operand with selector code, that
// is RW:
}
Token.Type.PLUS2 -> {
@ -335,6 +312,28 @@ class Compiler(
}
}
private fun parseArrayLiteral(cc: CompilerContext): List<ListEntry> {
// it should be called after LBRACKET is consumed
val entries = mutableListOf<ListEntry>()
while(true) {
val t = cc.next()
when(t.type) {
Token.Type.COMMA -> {
// todo: check commas sequences like [,] [,,] before, after or instead of expressions
}
Token.Type.RBRACKET -> return entries
Token.Type.ELLIPSIS -> {
parseExpressionLevel(cc)?.let { entries += ListEntry.Spread(it) }
}
else -> {
cc.previous()
parseExpressionLevel(cc)?.let { entries += ListEntry.Element(it) }
?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
}
}
}
}
private fun parseScopeOperator(operand: Accessor?, cc: CompilerContext): Accessor {
// implement global scope maybe?
if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::")
@ -389,7 +388,7 @@ class Compiler(
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
cc.previous()
val n = parseNumber(true, cc)
Accessor{
Accessor {
n.asReadonly
}
}

View File

@ -12,7 +12,7 @@ class Context(
)
: this(Script.defaultContext, args, pos)
fun raiseNotImplemented(): Nothing = raiseError("operation not implemented")
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this))

View File

@ -0,0 +1,7 @@
package net.sergeych.ling
sealed class ListEntry {
data class Element(val accessor: Accessor) : ListEntry()
data class Spread(val accessor: Accessor) : ListEntry()
}

View File

@ -148,7 +148,19 @@ sealed class Obj {
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
fun readField(context: Context, name: String): WithAccess<Obj> = getInstanceMember(context.pos, name)
suspend fun readField(context: Context, name: String): WithAccess<Obj> {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name)
val value = obj?.value
return when(value) {
is Statement -> {
// readonly property, important: call it on this
value.execute(context.copy(context.pos, newThisObj = this)).asReadonly
}
// could be writable property naturally
else -> getInstanceMember(context.pos, name)
}
}
fun writeField(context: Context, name: String, newValue: Obj) {
willMutate(context)
@ -156,6 +168,14 @@ sealed class Obj {
?: context.raiseError("Can't reassign member: $name")
}
open suspend fun getAt(context: Context, index: Int): Obj {
context.raiseNotImplemented("indexing")
}
open suspend fun putAt(context: Context, index: Int, newValue: Obj) {
context.raiseNotImplemented("indexing")
}
fun createField(name: String, initialValue: Obj, isMutable: Boolean = false, pos: Pos = Pos.builtIn) {
if (name in members || parentInstances.any { name in it.members })
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
@ -235,7 +255,7 @@ data class ObjString(val value: String) : Obj() {
return this.value.compareTo(other.value)
}
override fun toString(): String = value
override fun toString(): String = "\"$value\""
override val objClass: ObjClass
get() = type
@ -418,6 +438,40 @@ data class ObjBool(val value: Boolean) : Obj() {
}
}
//open class ObjProperty(var value: Obj =ObjVoid) {
// open suspend fun get(context: Context): Obj = value
// open suspend fun set(context: Context,newValue: Obj): Obj {
// return value.also { value = newValue }
// }
//}
class ObjList(val list: MutableList<Obj>) : Obj() {
override fun toString(): String = "[${list.joinToString(separator = ", ")}]"
override suspend fun getAt(context: Context, index: Int): Obj {
return list[index]
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
list[index] = newValue
}
override val objClass: ObjClass
get() = type
companion object {
val type = ObjClass("List").apply {
createField("size",
statement(Pos.builtIn) {
(it.thisObj as ObjList).list.size.toObj()
},
false
)
}
}
}
data class ObjNamespace(val name: String) : Obj() {
override fun toString(): String {
return "namespace ${name}"

View File

@ -33,7 +33,7 @@ private class Parser(fromPos: Pos) {
skipws()
if (pos.end) return Token("", currentPos, Token.Type.EOF)
val from = currentPos
return when (val ch = pos.currentChar.also { advance() }) {
return when (val ch = pos.currentChar.also { pos.advance() }) {
'(' -> Token("(", from, Token.Type.LPAREN)
')' -> Token(")", from, Token.Type.RPAREN)
'{' -> Token("{", from, Token.Type.LBRACE)
@ -44,7 +44,7 @@ private class Parser(fromPos: Pos) {
';' -> Token(";", from, Token.Type.SEMICOLON)
'=' -> {
if (pos.currentChar == '=') {
advance()
pos.advance()
Token("==", from, Token.Type.EQ)
} else
Token("=", from, Token.Type.ASSIGN)
@ -53,12 +53,12 @@ private class Parser(fromPos: Pos) {
'+' -> {
when (currentChar) {
'+' -> {
advance()
pos.advance()
Token("+", from, Token.Type.PLUS2)
}
'=' -> {
advance()
pos.advance()
Token("+", from, Token.Type.PLUSASSIGN)
}
@ -70,12 +70,12 @@ private class Parser(fromPos: Pos) {
'-' -> {
when (currentChar) {
'-' -> {
advance()
pos.advance()
Token("--", from, Token.Type.MINUS2)
}
'=' -> {
advance()
pos.advance()
Token("-", from, Token.Type.MINUSASSIGN)
}
@ -85,7 +85,7 @@ private class Parser(fromPos: Pos) {
'*' -> {
if (currentChar == '=') {
advance()
pos.advance()
Token("*=", from, Token.Type.STARASSIGN)
} else
Token("*", from, Token.Type.STAR)
@ -93,24 +93,47 @@ private class Parser(fromPos: Pos) {
'/' -> when (currentChar) {
'/' -> {
advance()
pos.advance()
Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT)
}
'=' -> {
advance()
pos.advance()
Token("/=", from, Token.Type.SLASHASSIGN)
}
else -> Token("/", from, Token.Type.SLASH)
}
'%' -> when(currentChar) {
'=' -> { advance(); Token("%=", from, Token.Type.PERCENTASSIGN) }
else -> Token("%", from, Token.Type.PERCENT) }
'%' -> when (currentChar) {
'=' -> {
pos.advance(); Token("%=", from, Token.Type.PERCENTASSIGN)
}
else -> Token("%", from, Token.Type.PERCENT)
}
'.' -> {
// could be: dot, range .. or ..<, or ellipsis ...:
if (currentChar == '.') {
pos.advance()
// .. already parsed:
if (currentChar == '.') {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
} else if (currentChar == '<') {
Token("..<", from, Token.Type.DOTDOTLT)
} else {
pos.back()
Token("..", from, Token.Type.DOTDOT)
}
} else
Token(".", from, Token.Type.DOT)
}
'.' -> Token(".", from, Token.Type.DOT)
'<' -> {
if (currentChar == '=') {
advance()
pos.advance()
Token("<=", from, Token.Type.LTE)
} else
Token("<", from, Token.Type.LT)
@ -118,7 +141,7 @@ private class Parser(fromPos: Pos) {
'>' -> {
if (currentChar == '=') {
advance()
pos.advance()
Token(">=", from, Token.Type.GTE)
} else
Token(">", from, Token.Type.GT)
@ -126,7 +149,7 @@ private class Parser(fromPos: Pos) {
'!' -> {
if (currentChar == '=') {
advance()
pos.advance()
Token("!=", from, Token.Type.NEQ)
} else
Token("!", from, Token.Type.NOT)
@ -134,7 +157,7 @@ private class Parser(fromPos: Pos) {
'|' -> {
if (currentChar == '|') {
advance()
pos.advance()
Token("||", from, Token.Type.OR)
} else
Token("|", from, Token.Type.BITOR)
@ -142,7 +165,7 @@ private class Parser(fromPos: Pos) {
'&' -> {
if (currentChar == '&') {
advance()
pos.advance()
Token("&&", from, Token.Type.AND)
} else
Token("&", from, Token.Type.BITAND)
@ -158,7 +181,7 @@ private class Parser(fromPos: Pos) {
':' -> {
if (currentChar == ':') {
advance()
pos.advance()
Token("::", from, Token.Type.COLONCOLON)
} else
Token(":", from, Token.Type.COLON)
@ -177,7 +200,7 @@ private class Parser(fromPos: Pos) {
if (ch.isLetter() || ch == '_') {
val text = ch + loadChars(idNextChars)
if (currentChar == '@') {
advance()
pos.advance()
if (currentChar.isLetter()) {
// break@label or like
pos.back()
@ -197,19 +220,19 @@ private class Parser(fromPos: Pos) {
Token(p1, start, Token.Type.INT)
else if (currentChar == '.') {
// could be decimal
advance()
pos.advance()
if (currentChar in digitsSet) {
// decimal part
val p2 = loadChars(digits)
// with exponent?
if (currentChar == 'e' || currentChar == 'E') {
advance()
pos.advance()
var negative = false
if (currentChar == '+')
advance()
pos.advance()
else if (currentChar == '-') {
negative = true
advance()
pos.advance()
}
var p3 = loadChars(digits)
if (negative) p3 = "-$p3"
@ -227,7 +250,7 @@ private class Parser(fromPos: Pos) {
} else {
// could be integer, also hex:
if (currentChar == 'x' && p1 == "0") {
advance()
pos.advance()
Token(loadChars({ it in hexDigits }), start, Token.Type.HEX).also {
if (currentChar.isLetter())
raise("invalid hex literal")
@ -243,15 +266,15 @@ private class Parser(fromPos: Pos) {
private fun loadStringToken(): Token {
var start = currentPos
if (currentChar == '"') advance()
if (currentChar == '"') pos.advance()
else start = start.back()
val sb = StringBuilder()
while (currentChar != '"') {
if (pos.end) raise("unterminated string")
if (pos.end) throw ScriptError(start, "unterminated string started there")
when (currentChar) {
'\\' -> {
advance() ?: raise("unterminated string")
pos.advance() ?: raise("unterminated string")
when (currentChar) {
'n' -> sb.append('\n')
'r' -> sb.append('\r')
@ -263,11 +286,11 @@ private class Parser(fromPos: Pos) {
else -> {
sb.append(currentChar)
advance()
pos.advance()
}
}
}
advance()
pos.advance()
return Token(sb.toString(), start, Token.Type.STRING)
}
@ -287,7 +310,7 @@ private class Parser(fromPos: Pos) {
val ch = pos.currentChar
if (isValidChar(ch)) {
result.append(ch)
advance()
pos.advance()
} else
break
}
@ -314,7 +337,7 @@ private class Parser(fromPos: Pos) {
val l = pos.line
do {
result.append(pos.currentChar)
advance()
pos.advance()
} while (pos.line == l)
return result.toString()
}
@ -327,12 +350,11 @@ private class Parser(fromPos: Pos) {
val ch = pos.currentChar
if (ch == '\n') break
if (ch.isWhitespace())
advance()
pos.advance()
else
return ch
}
return null
}
private fun advance() = pos.advance()
}

View File

@ -57,9 +57,8 @@ class Script(
addConst("String", ObjString.type)
addConst("Int", ObjInt.type)
addConst("Bool", ObjBool.type)
addConst("List", ObjList.type)
val pi = ObjReal(PI)
val z = pi.objClass
println("PI class $z")
addConst("π", pi)
getOrCreateNamespace("Math").apply {
addConst("PI", pi)

View File

@ -13,7 +13,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
EQ, NEQ, LT, LTE, GT, GTE,
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL,ATLABEL, // label@ at@label
LABEL, ATLABEL, // label@ at@label
ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE,
EOF,
}

View File

@ -485,7 +485,8 @@ class ScriptTest {
@Test
fun testWhileBlockIsolation3() = runTest {
eval("""
eval(
"""
var outer = 7
var sum = 0
var cnt1 = 0
@ -504,7 +505,7 @@ class ScriptTest {
}
println("sum "+sum)
""".trimIndent()
)
)
}
@Test
@ -713,20 +714,96 @@ class ScriptTest {
assertEquals(11, cxt.eval("x").toInt())
}
@Test
fun testValVarConverting() = runTest {
eval(
"""
val x = 5
var y = x
y = 1
assert(x == 5)
""".trimIndent()
)
assertFails {
eval(
"""
val x = 5
fun fna(t) {
t = 11
}
fna(1)
""".trimIndent()
)
}
eval(
"""
var x = 5
val y = x
x = 10
assert(y == 5)
assert(x == 10)
""".trimIndent()
)
}
@Test
fun testListLiteral() = runTest {
eval("""
val list = [1,22,3]
assert(list[0] == 1)
assert(list[1] == 22)
assert(list[2] == 3)
""".trimIndent())
eval("""
val x0 = 100
val list = [x0 + 1, x0 * 10, 3]
assert(list[0] == 101)
assert(list[1] == 1000)
assert(list[2] == 3)
""".trimIndent())
eval("""
val x0 = 100
val list = [x0 + 1, x0 * 10, if(x0 < 100) "low" else "high", 5]
assert(list[0] == 101)
assert(list[1] == 1000)
assert(list[2] == "high")
assert(list[3] == 5)
""".trimIndent())
}
@Test
fun testListLiteralSpread() = runTest {
eval("""
val list1 = [1,22,3]
val list = ["start", ...list1, "end"]
assert(list[0] == "start")
assert(list[1] == 1)
assert(list[2] == 22)
assert(list[3] == 3)
assert(list[4] == "end")
""".trimIndent())
}
@Test
fun testListSize() = runTest {
eval("""
val a = [4,3]
assert(a.size == 2)
""".trimIndent())
}
// @Test
// fun testMultiAssign() = runTest {
// assertEquals(
// 7,
// eval("""
// var x = 10
// var y = 2
// (x = 1) = 5
// println(x)
// println(y)
// x + y
// """.trimIndent()).toInt()
// )
// fun testLambda1() = runTest {
// val l = eval("""
// x = {
// 122
// }
// x
// """.trimIndent())
// println(l)
// }
//
}

View File

@ -139,7 +139,7 @@ suspend fun DocTest.test() {
val context = Context().apply {
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a)
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.asStr.value)
collectedOutput.append('\n')
}
ObjVoid
@ -198,4 +198,9 @@ class BookTest {
runDocTests("../docs/Real.md")
}
@Test
fun testFromList() = runTest {
runDocTests("../docs/List.md")
}
}