fix #33 minimal Buffer with docs, in a separate package

This commit is contained in:
sergeych committed 2025-07-09 17:56:08 +03:00
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())
}
}
+5
View File
@@ -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()) {