fix #33 minimal Buffer with docs, in a separate package
This commit is contained in:
1 parent
26282d3e22
commit
23006b5caa
17 files changed
+420
-60
No files matched your search
@@ -274,9 +274,7 @@ class Compiler(
|
||||
if (x == ObjNull && isOptional) ObjNull.asReadonly
|
||||
else x.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)
|
||||
left.getter(cxt).value.putAt(cxt, index.execute(cxt), newValue)
|
||||
}
|
||||
} ?: run {
|
||||
// array literal
|
||||
|
||||
@@ -165,7 +165,6 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
*/
|
||||
fun skipWsTokens(): Token {
|
||||
while( current().type in wstokens ) {
|
||||
println("skipws ${current()}")
|
||||
next()
|
||||
}
|
||||
return next()
|
||||
|
||||
@@ -220,7 +220,7 @@ open class Obj {
|
||||
|
||||
suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong()))
|
||||
|
||||
open suspend fun putAt(scope: Scope, index: Int, newValue: Obj) {
|
||||
open suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
scope.raiseNotImplemented("indexing")
|
||||
}
|
||||
|
||||
@@ -280,7 +280,18 @@ open class Obj {
|
||||
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
|
||||
thisObj
|
||||
}
|
||||
addFn("getAt") {
|
||||
requireExactCount(1)
|
||||
thisObj.getAt(this, requiredArg<Obj>(0))
|
||||
}
|
||||
addFn("putAt") {
|
||||
requireExactCount(2)
|
||||
val newValue = args[1]
|
||||
thisObj.putAt(this, requiredArg<Obj>(0), newValue)
|
||||
newValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
inline fun from(obj: Any?): Obj {
|
||||
@@ -349,7 +360,7 @@ object ObjNull : Obj() {
|
||||
scope.raiseNPE()
|
||||
}
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Int, newValue: Obj) {
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
scope.raiseNPE()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlin.math.min
|
||||
|
||||
class ObjBuffer(val byteArray: UByteArray) : Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
fun checkIndex(scope: Scope, index: Obj): Int {
|
||||
if (index !is ObjInt)
|
||||
scope.raiseIllegalArgument("index must be Int")
|
||||
val i = index.value.toInt()
|
||||
if (i < 0) scope.raiseIllegalArgument("index must be positive")
|
||||
if (i >= byteArray.size)
|
||||
scope.raiseIndexOutOfBounds("index $i is out of bounds 0..<${byteArray.size}")
|
||||
return i
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
// notice: we create a copy if content, so we don't want it
|
||||
// to be treated as modifiable, or putAt will not be called:
|
||||
return if (index is ObjRange) {
|
||||
val start: Int = index.startInt(scope)
|
||||
val end: Int = index.exclusiveIntEnd(scope) ?: size
|
||||
ObjBuffer(byteArray.sliceArray(start..<end))
|
||||
}
|
||||
else ObjInt(byteArray[checkIndex(scope, index)].toLong(), true)
|
||||
}
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
byteArray[checkIndex(scope, index.toObj())] = when (newValue) {
|
||||
is ObjInt -> newValue.value.toUByte()
|
||||
is ObjChar -> newValue.value.code.toUByte()
|
||||
else -> scope.raiseIllegalArgument(
|
||||
"invalid byte value for buffer at index ${index.inspect()}: ${newValue.inspect()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val size by byteArray::size
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjBuffer) return -1
|
||||
val limit = min(size, other.size)
|
||||
for (i in 0..<limit) {
|
||||
val own = byteArray[i]
|
||||
val their = other.byteArray[i]
|
||||
if (own < their) return -1
|
||||
else if (own > their) return 1
|
||||
}
|
||||
if (size < other.size) return -1
|
||||
if (size > other.size) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return if( other is ObjBuffer)
|
||||
ObjBuffer(byteArray + other.byteArray)
|
||||
else if( other.isInstanceOf(ObjIterable)) {
|
||||
ObjBuffer(
|
||||
byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray().toUByteArray()
|
||||
)
|
||||
} else scope.raiseIllegalArgument("can't concatenate buffer with ${other.inspect()}")
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Buffer(${byteArray.toList()})"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer =
|
||||
when (obj) {
|
||||
is ObjBuffer -> ObjBuffer(obj.byteArray.copyOf())
|
||||
is ObjInt -> {
|
||||
if (obj.value < 0)
|
||||
scope.raiseIllegalArgument("buffer size must be positive")
|
||||
val data = UByteArray(obj.value.toInt())
|
||||
ObjBuffer(data)
|
||||
}
|
||||
|
||||
is ObjString -> ObjBuffer(obj.value.encodeToByteArray().asUByteArray())
|
||||
else -> {
|
||||
if (obj.isInstanceOf(ObjIterable)) {
|
||||
ObjBuffer(
|
||||
obj.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray().toUByteArray()
|
||||
)
|
||||
} else
|
||||
scope.raiseIllegalArgument(
|
||||
"can't construct buffer from ${obj.inspect()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val type = object : ObjClass("Buffer", ObjArray) {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val args = scope.args.list
|
||||
return when (args.size) {
|
||||
// empty buffer
|
||||
0 -> ObjBuffer(ubyteArrayOf())
|
||||
1 -> createBufferFrom(scope, args[0])
|
||||
else -> {
|
||||
// create buffer from array, each argument should be a byte then:
|
||||
val data = UByteArray(args.size)
|
||||
for ((i, b) in args.withIndex()) {
|
||||
val code = when (b) {
|
||||
is ObjChar -> b.value.code.toUByte()
|
||||
is ObjInt -> b.value.toUByte()
|
||||
else -> scope.raiseIllegalArgument(
|
||||
"invalid byte value for buffer constructor at index $i: ${b.inspect()}"
|
||||
)
|
||||
}
|
||||
data[i] = code
|
||||
}
|
||||
ObjBuffer(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
createField("size",
|
||||
statement {
|
||||
(thisObj as ObjBuffer).byteArray.size.toObj()
|
||||
}
|
||||
)
|
||||
addFn("decodeUtf8") {
|
||||
ObjString(
|
||||
thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString()
|
||||
)
|
||||
}
|
||||
// )
|
||||
// addFn("getAt") {
|
||||
// requireExactCount(1)
|
||||
// thisAs<ObjList>().getAt(this, requiredArg<Obj>(0))
|
||||
// }
|
||||
// addFn("putAt") {
|
||||
// requireExactCount(2)
|
||||
// val newValue = args[1]
|
||||
// thisAs<ObjList>().putAt(this, requiredArg<ObjInt>(0).value.toInt(), newValue)
|
||||
// newValue
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
data class ObjInt(var value: Long) : Obj(), Numeric {
|
||||
class ObjInt(var value: Long,val isConst: Boolean = false) : Obj(), Numeric {
|
||||
override val asStr get() = ObjString(value.toString())
|
||||
override val longValue get() = value
|
||||
override val doubleValue get() = value.toDouble()
|
||||
@@ -70,7 +70,7 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
|
||||
* assignment
|
||||
*/
|
||||
override suspend fun assign(scope: Scope, other: Obj): Obj? {
|
||||
return if (other is ObjInt) {
|
||||
return if (!isConst && other is ObjInt) {
|
||||
value = other.value
|
||||
this
|
||||
} else null
|
||||
@@ -90,8 +90,8 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Zero = ObjInt(0)
|
||||
val One = ObjInt(1)
|
||||
val Zero = ObjInt(0, true)
|
||||
val One = ObjInt(1, true)
|
||||
val type = ObjClass("Int")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@ package net.sergeych.lyng
|
||||
|
||||
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
|
||||
init {
|
||||
for (p in objClass.parents)
|
||||
parentInstances.add(p.defaultInstance())
|
||||
}
|
||||
|
||||
override fun toString(): String = "[${
|
||||
list.joinToString(separator = ", ") { it.inspect() }
|
||||
}]"
|
||||
@@ -51,9 +46,8 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Int, newValue: Obj) {
|
||||
val i = index
|
||||
list[i] = newValue
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
list[index.toInt()] = newValue
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
@@ -136,16 +130,6 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
(thisObj as ObjList).list.size.toObj()
|
||||
}
|
||||
)
|
||||
addFn("getAt") {
|
||||
requireExactCount(1)
|
||||
thisAs<ObjList>().getAt(this, requiredArg<Obj>(0))
|
||||
}
|
||||
addFn("putAt") {
|
||||
requireExactCount(2)
|
||||
val newValue = args[1]
|
||||
thisAs<ObjList>().putAt(this, requiredArg<ObjInt>(0).value.toInt(), newValue)
|
||||
newValue
|
||||
}
|
||||
createField("add",
|
||||
statement {
|
||||
val l = thisAs<ObjList>().list
|
||||
|
||||
@@ -39,7 +39,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
||||
override val objClass = type
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj =
|
||||
map.getOrElse(index) { scope.raiseNoSuchElement() }
|
||||
map.get(index) ?: ObjNull
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
map[index] = newValue
|
||||
}
|
||||
|
||||
override suspend fun contains(scope: Scope, other: Obj): Boolean {
|
||||
return other in map
|
||||
|
||||
@@ -15,6 +15,31 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* IF end is open (null/ObjNull), returns null
|
||||
* Otherwise, return correct value for the exclusive end
|
||||
* raises [ObjIllegalArgumentException] if end is not ObjInt
|
||||
*/
|
||||
fun exclusiveIntEnd(scope: Scope): Int? =
|
||||
if (end == null || end is ObjNull) null
|
||||
else {
|
||||
if (end !is ObjInt) scope.raiseIllegalArgument("end is not int")
|
||||
if (isEndInclusive) end.value.toInt() + 1 else end.value.toInt()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If start is null/ObjNull, returns 0
|
||||
* if start is not ObjInt, raises [ObjIllegalArgumentException]
|
||||
* otherwise returns start.value.toInt()
|
||||
*/
|
||||
fun startInt(scope: Scope): Int =
|
||||
if( start == null || start is ObjNull ) 0
|
||||
else {
|
||||
if( start is ObjInt ) start.value.toInt()
|
||||
else scope.raiseIllegalArgument("start is not Int: ${start.inspect()}")
|
||||
}
|
||||
|
||||
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
|
||||
if (start != null) {
|
||||
// our start is not -∞ so other start should be GTE or is not contained:
|
||||
|
||||
@@ -15,7 +15,6 @@ data class ObjString(val value: String) : Obj() {
|
||||
// return i
|
||||
// }
|
||||
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjString) return -2
|
||||
return this.value.compareTo(other.value)
|
||||
@@ -114,6 +113,11 @@ data class ObjString(val value: String) : Obj() {
|
||||
addFn("upper") {
|
||||
thisAs<ObjString>().value.uppercase().let(::ObjString)
|
||||
}
|
||||
addFn("characters") {
|
||||
ObjList(
|
||||
thisAs<ObjString>().value.map { ObjChar(it) }.toMutableList()
|
||||
)
|
||||
}
|
||||
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble())}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,14 @@ class Script(
|
||||
}
|
||||
}
|
||||
|
||||
val defaultImportManager: ImportManager by lazy { ImportManager(rootScope, SecurityManager.allowAll) }
|
||||
val defaultImportManager: ImportManager by lazy {
|
||||
ImportManager(rootScope, SecurityManager.allowAll).apply {
|
||||
addPackage("lyng.buffer") {
|
||||
it.addConst("Buffer", ObjBuffer.type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.sergeych.lyng.pacman
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.synctools.ProtectedOp
|
||||
import net.sergeych.synctools.withLock
|
||||
|
||||
/**
|
||||
* Import manager allow to register packages with builder lambdas and act as an
|
||||
@@ -17,7 +17,7 @@ import net.sergeych.lyng.*
|
||||
class ImportManager(
|
||||
rootScope: Scope = Script.defaultImportManager.newModule(),
|
||||
securityManager: SecurityManager = SecurityManager.allowAll
|
||||
): ImportProvider(rootScope, securityManager) {
|
||||
) : ImportProvider(rootScope, securityManager) {
|
||||
|
||||
private inner class Entry(
|
||||
val packageName: String,
|
||||
@@ -36,7 +36,7 @@ class ImportManager(
|
||||
|
||||
|
||||
/**
|
||||
* Inner provider does not lock [access], the only difference; it is meant to be used
|
||||
* Inner provider does not lock [op], the only difference; it is meant to be used
|
||||
* exclusively by the coroutine that starts actual import chain
|
||||
*/
|
||||
private inner class InternalProvider : ImportProvider(rootScope) {
|
||||
@@ -52,7 +52,9 @@ class ImportManager(
|
||||
|
||||
|
||||
private val imports = mutableMapOf<String, Entry>()
|
||||
private val access = Mutex()
|
||||
|
||||
val op = ProtectedOp()
|
||||
|
||||
|
||||
/**
|
||||
* Register new package that can be imported. It is not possible to unregister or
|
||||
@@ -65,8 +67,8 @@ class ImportManager(
|
||||
* @param name package name
|
||||
* @param builder lambda to create actual package using the given [ModuleScope]
|
||||
*/
|
||||
suspend fun addPackage(name: String, builder: suspend (ModuleScope) -> Unit) {
|
||||
access.withLock {
|
||||
fun addPackage(name: String, builder: suspend (ModuleScope) -> Unit) {
|
||||
op.withLock {
|
||||
if (name in imports)
|
||||
throw IllegalArgumentException("Package $name already exists")
|
||||
imports[name] = Entry(name, builder)
|
||||
@@ -77,8 +79,8 @@ class ImportManager(
|
||||
* Bulk [addPackage] with slightly better performance
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun addPackages(registrationData: List<Pair<String, suspend (ModuleScope) -> Unit>>) {
|
||||
access.withLock {
|
||||
fun addPackages(registrationData: List<Pair<String, suspend (ModuleScope) -> Unit>>) {
|
||||
op.withLock {
|
||||
for (pp in registrationData) {
|
||||
if (pp.first in imports)
|
||||
throw IllegalArgumentException("Package ${pp.first} already exists")
|
||||
@@ -89,7 +91,7 @@ class ImportManager(
|
||||
|
||||
/**
|
||||
* Perform actual import or return ready scope. __It must only be called when
|
||||
* [access] is locked__, e.g. only internally
|
||||
* [op] is locked__, e.g. only internally
|
||||
*/
|
||||
private suspend fun doImport(packageName: String, pos: Pos): ModuleScope {
|
||||
val entry = imports[packageName] ?: throw ImportException(pos, "package not found: $packageName")
|
||||
@@ -102,8 +104,8 @@ class ImportManager(
|
||||
/**
|
||||
* Add packages that only need to compile [Source].
|
||||
*/
|
||||
suspend fun addSourcePackages(vararg sources: Source) {
|
||||
for( s in sources) {
|
||||
fun addSourcePackages(vararg sources: Source) {
|
||||
for (s in sources) {
|
||||
addPackage(s.extractPackageName()) {
|
||||
it.eval(s)
|
||||
}
|
||||
@@ -114,12 +116,12 @@ class ImportManager(
|
||||
/**
|
||||
* Add source packages using package name as [Source.fileName], for simplicity
|
||||
*/
|
||||
suspend fun addTextPackages(vararg sourceTexts: String) {
|
||||
for( s in sourceTexts) {
|
||||
fun addTextPackages(vararg sourceTexts: String) {
|
||||
for (s in sourceTexts) {
|
||||
var source = Source("tmp", s)
|
||||
val packageName = source.extractPackageName()
|
||||
source = Source(packageName, s)
|
||||
addPackage(packageName) { it.eval(source)}
|
||||
addPackage(packageName) { it.eval(source) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2460,4 +2460,57 @@ class ScriptTest {
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaps() = runTest {
|
||||
eval(
|
||||
"""
|
||||
val map = Map( "a" => 1, "b" => 2 )
|
||||
assertEquals( 1, map["a"] )
|
||||
assertEquals( 2, map["b"] )
|
||||
assertEquals( null, map["c"] )
|
||||
map["c"] = 3
|
||||
assertEquals( 3, map["c"] )
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuffer() = runTest {
|
||||
eval("""
|
||||
import lyng.buffer
|
||||
|
||||
assertEquals( 0, Buffer().size )
|
||||
assertEquals( 3, Buffer(1, 2, 3).size )
|
||||
assertEquals( 5, Buffer("hello").size )
|
||||
|
||||
val buffer = Buffer("Hello")
|
||||
assertEquals( 5, buffer.size)
|
||||
assertEquals('l'.code, buffer[2] )
|
||||
assertEquals('l'.code, buffer[3] )
|
||||
assertEquals("Hello", buffer.decodeUtf8())
|
||||
|
||||
buffer[2] = 101
|
||||
assertEquals(101, buffer[2])
|
||||
assertEquals("Heelo", buffer.decodeUtf8())
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBufferCompare() = runTest {
|
||||
eval("""
|
||||
import lyng.buffer
|
||||
|
||||
println("Hello".characters())
|
||||
val b1 = Buffer("Hello")
|
||||
val b2 = Buffer("Hello".characters())
|
||||
|
||||
assertEquals( b1, b2 )
|
||||
val b3 = b1 + Buffer("!")
|
||||
assertEquals( "Hello!", b3.decodeUtf8())
|
||||
assert( b3 > b1 )
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -265,6 +265,11 @@ class BookTest {
|
||||
runDocTests("../docs/Map.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuffer() = runTest {
|
||||
runDocTests("../docs/Buffer.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSampleBooks() = runTest {
|
||||
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
||||
|
||||
Reference in new issue
Block a user