Compare commits

..

4 Commits

Author SHA1 Message Date
d0d79d2f07 docs: add LegacyDigest module documentation
- New docs/LegacyDigest.md: full reference for lyng.legacy_digest,
  covering the sha1() API, input types (String / Buffer), FIPS compliance
  note, and explicit guidance on appropriate vs. inappropriate use.

- docs/ai_stdlib_reference.md: entry in section 5 so AI agents know
  LegacyDigest.sha1() exists and is intentionally named as legacy-only.

- docs/whats_new.md: release-note section alongside Complex, Decimal,
  and Matrix, with a minimal runnable example and cross-link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 19:53:01 +03:00
69013392d3 Add lyng.legacy_digest module with LegacyDigest.sha1()
Provides a pure Kotlin/KMP SHA-1 implementation with no extra dependencies,
exposed as `LegacyDigest.sha1(data)` in the `lyng.legacy_digest` package.

The naming deliberately signals that SHA-1 is cryptographically broken:
the object name `LegacyDigest` and prominent doc-comment warnings steer
users away from security-sensitive use while still enabling interoperability
with legacy protocols and file formats that require SHA-1.

API:
  import lyng.legacy_digest
  val hex: String = LegacyDigest.sha1("some string")   // UTF-8 input
  val hex: String = LegacyDigest.sha1(someBuffer)       // raw-byte input

Tests cover FIPS 180-4 known-answer vectors (empty, "abc", long message,
quick-brown-fox) at both the Kotlin and Lyng integration levels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 19:51:04 +03:00
f145a90845 Fix method slot ID collision between instance and static methods
When createField() was called with a pre-assigned methodId, the ID was
used but methodIdMap was not updated and nextMethodId was not advanced.
This caused assignMethodId() for static methods to reuse slot IDs
already occupied by instance methods.

In complex.lyng, fromInt/imaginary got IDs 12/14 (same as plus/mul),
causing binary operator dispatch via CALL_MEMBER_SLOT to call the wrong
function (e.g. a*b would invoke imaginary instead of mul).

Fix: after computing effectiveMethodId in createField, always register
it in methodIdMap and advance nextMethodId past it so subsequent
auto-assignments start from a clean range.

Also pre-assigns method IDs for non-static fun/fn declarations during
class body pre-scan so forward references resolve correctly in class
bodies, and adds ComplexModuleTest coverage for operator slot dispatch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 19:41:18 +03:00
2f145a0ea7 Fix nullable let member inference 2026-04-07 09:33:40 +03:00
13 changed files with 589 additions and 23 deletions

66
docs/LegacyDigest.md Normal file
View File

@ -0,0 +1,66 @@
# Legacy Digest Functions (`lyng.legacy_digest`)
> ⚠️ **Security warning:** The functions in this module use cryptographically broken
> algorithms. Do **not** use them for passwords, digital signatures, integrity
> verification against adversarial tampering, or any other security-sensitive
> purpose. They exist solely for compatibility with legacy protocols and file
> formats that require specific hash values.
Import when you need to produce a SHA-1 digest for an existing protocol or format:
```lyng
import lyng.legacy_digest
```
## `LegacyDigest` Object
### `sha1(data): String`
Computes the SHA-1 digest of `data` and returns it as a 40-character lowercase
hex string.
`data` can be:
| Type | Behaviour |
|----------|----------------------------------------|
| `String` | Encoded as UTF-8, then hashed |
| `Buffer` | Raw bytes hashed directly |
| anything | Falls back to `toString()` then UTF-8 |
```lyng
import lyng.legacy_digest
// String input
val h = LegacyDigest.sha1("abc")
assertEquals("a9993e364706816aba3e25717850c26c9cd0d89d", h)
// Empty string
assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", LegacyDigest.sha1(""))
```
```lyng
import lyng.legacy_digest
import lyng.buffer
// Buffer input (raw bytes)
val buf = Buffer.decodeHex("616263") // 0x61 0x62 0x63 = "abc"
assertEquals("a9993e364706816aba3e25717850c26c9cd0d89d", LegacyDigest.sha1(buf))
```
## Implementation Notes
- Pure Kotlin/KMP — no native libraries or extra dependencies.
- Follows FIPS 180-4.
- The output is always lowercase hex, never uppercase or binary.
## When to Use
Use `lyng.legacy_digest` only when an external system you cannot change requires
a SHA-1 value, for example:
- old git-style content addresses
- some OAuth 1.0 / HMAC-SHA1 signature schemes
- legacy file checksums defined in published specs
For any new design choose a current hash function (SHA-256 or better) once
Lyng adds a `lyng.digest` module.

View File

@ -75,6 +75,9 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- `Matrix`, `Vector`, `matrix(rows)`, `vector(values)`, dense linear algebra, inversion, solving, and matrix slicing with `m[row, col]`.
- `import lyng.buffer`
- `Buffer`, `MutableBuffer`.
- `import lyng.legacy_digest`
- `LegacyDigest.sha1(data): String` — SHA-1 hex digest; `data` may be `String` (UTF-8) or `Buffer` (raw bytes).
- ⚠️ Cryptographically broken. Use only for legacy protocol / file-format compatibility.
- `import lyng.serialization`
- `Lynon` serialization utilities.
- `import lyng.time`

View File

@ -164,6 +164,31 @@ println(z.exp())
See [Complex](Complex.md).
### Legacy Digest Module (`lyng.legacy_digest`)
For situations where an external protocol or file format requires a SHA-1 value,
Lyng now ships a `lyng.legacy_digest` module backed by a pure Kotlin/KMP
implementation with no extra dependencies.
> ⚠️ SHA-1 is cryptographically broken. Use only for legacy-compatibility work.
```lyng
import lyng.legacy_digest
val hex = LegacyDigest.sha1("abc")
// → "a9993e364706816aba3e25717850c26c9cd0d89d"
// Also accepts raw bytes:
import lyng.buffer
val buf = Buffer.decodeHex("616263")
assertEquals(hex, LegacyDigest.sha1(buf))
```
The name `LegacyDigest` is intentional: it signals that these algorithms belong
to a compatibility layer, not to a current security toolkit.
See [LegacyDigest](LegacyDigest.md).
### Binary Operator Interop Registry
Lyng now provides a general mechanism for mixed binary operators through `lyng.operators`.

View File

@ -31,6 +31,7 @@ sealed class CodeContext {
var typeParamDecls: List<TypeDecl.TypeParam> = emptyList()
val pendingInitializations = mutableMapOf<String, Pos>()
val declaredMembers = mutableSetOf<String>()
val declaredMethodNames = mutableSetOf<String>()
val classScopeMembers = mutableSetOf<String>()
val memberOverrides = mutableMapOf<String, Boolean>()
val memberFieldIds = mutableMapOf<String, Int>()

View File

@ -456,7 +456,7 @@ class Compiler(
}
}
private fun predeclareClassMembers(target: MutableSet<String>, overrides: MutableMap<String, Boolean>) {
private fun predeclareClassMembers(target: MutableSet<String>, overrides: MutableMap<String, Boolean>, methodNames: MutableSet<String>? = null) {
val saved = cc.savePos()
var depth = 0
val modifiers = setOf(
@ -478,18 +478,22 @@ class Compiler(
Token.Type.RBRACE -> if (depth == 0) break else depth--
Token.Type.ID -> if (depth == 0) {
var sawOverride = false
var sawStatic = false
while (t.type == Token.Type.ID && t.value in modifiers) {
if (t.value == "override") sawOverride = true
if (t.value == "static") sawStatic = true
t = nextNonWs()
}
when (t.value) {
"fun", "fn", "val", "var" -> {
val isMethod = t.value == "fun" || t.value == "fn"
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
val afterName = cc.peekNextNonWhitespace()
if (afterName.type != Token.Type.DOT) {
target.add(nameToken.value)
overrides[nameToken.value] = sawOverride
if (isMethod && !sawStatic) methodNames?.add(nameToken.value)
}
}
}
@ -2783,7 +2787,7 @@ class Compiler(
Token.Type.LPAREN -> {
cc.next()
if (shouldTreatAsClassScopeCall(left, next.value)) {
val parsed = parseArgs(null, implicitItTypeNameForMemberLambda(left, next.value))
val parsed = parseArgs(null, implicitItTypeForMemberLambda(left, next.value))
val args = parsed.first
val tailBlock = parsed.second
isCall = true
@ -2800,7 +2804,7 @@ class Compiler(
val receiverType = if (next.value == "apply" || next.value == "run") {
inferReceiverTypeFromRef(left)
} else null
val parsed = parseArgs(receiverType, implicitItTypeNameForMemberLambda(left, next.value))
val parsed = parseArgs(receiverType, implicitItTypeForMemberLambda(left, next.value))
val args = parsed.first
val tailBlock = parsed.second
if (left is LocalVarRef && left.name == "scope") {
@ -2879,7 +2883,7 @@ class Compiler(
val receiverType = if (next.value == "apply" || next.value == "run") {
inferReceiverTypeFromRef(left)
} else null
val itType = implicitItTypeNameForMemberLambda(left, next.value)
val itType = implicitItTypeForMemberLambda(left, next.value)
val lambda = parseLambdaExpression(receiverType, implicitItType = itType)
val argPos = next.pos
val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos))
@ -3287,7 +3291,7 @@ class Compiler(
private suspend fun parseLambdaExpression(
expectedReceiverType: String? = null,
wrapAsExtensionCallable: Boolean = false,
implicitItType: String? = null
implicitItType: TypeDecl? = null
): ObjRef {
// lambda args are different:
val startPos = cc.currentPos()
@ -3304,14 +3308,15 @@ class Compiler(
val slotParamNames = if (hasImplicitIt) paramNames + "it" else paramNames
val paramSlotPlan = buildParamSlotPlan(slotParamNames)
if (implicitItType != null) {
val cls = resolveClassByName(implicitItType)
?: resolveTypeDeclObjClass(TypeDecl.Simple(implicitItType, false))
val cls = resolveTypeDeclObjClass(implicitItType)
val itSlot = paramSlotPlan.slots["it"]?.index
if (cls != null && itSlot != null) {
val paramTypeMap = slotTypeByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() }
paramTypeMap[itSlot] = cls
if (itSlot != null) {
if (cls != null) {
val paramTypeMap = slotTypeByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() }
paramTypeMap[itSlot] = cls
}
val paramTypeDeclMap = slotTypeDeclByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() }
paramTypeDeclMap[itSlot] = TypeDecl.Simple(implicitItType, false)
paramTypeDeclMap[itSlot] = implicitItType
}
}
@ -4745,6 +4750,13 @@ class Compiler(
return null
}
private fun classMethodReturnTypeDecl(typeName: String?, name: String): TypeDecl? {
if (typeName == null) return null
classMethodReturnTypeDeclByName[typeName]?.get(name)?.let { return it }
classMethodReturnTypeByName[typeName]?.get(name)?.let { return TypeDecl.Simple(it.className, false) }
return null
}
private fun classMethodReturnClass(targetClass: ObjClass?, name: String): ObjClass? {
if (targetClass == null) return null
if (targetClass == ObjDynamic.type) return ObjDynamic.type
@ -4858,6 +4870,26 @@ class Compiler(
classMethodReturnTypeDecl(targetClass, "getAt")
}
is MethodCallRef -> methodReturnTypeDeclByRef[ref] ?: inferMethodCallReturnTypeDecl(ref)
is ImplicitThisMethodCallRef -> {
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
val receiverDecl = typeName?.let { TypeDecl.Simple(it, false) }
inferMethodCallReturnTypeDecl(ref.methodName(), receiverDecl, ref.arguments())
?: classMethodReturnTypeDecl(typeName, ref.methodName())
?: typeName?.let { resolveClassByName(it) }?.let { classMethodReturnTypeDecl(it, ref.methodName()) }
}
is ThisMethodSlotCallRef -> {
val typeName = currentImplicitThisTypeName()
val receiverDecl = typeName?.let { TypeDecl.Simple(it, false) }
inferMethodCallReturnTypeDecl(ref.methodName(), receiverDecl, ref.arguments())
?: classMethodReturnTypeDecl(typeName, ref.methodName())
?: typeName?.let { resolveClassByName(it) }?.let { classMethodReturnTypeDecl(it, ref.methodName()) }
}
is QualifiedThisMethodSlotCallRef -> {
val receiverDecl = TypeDecl.Simple(ref.receiverTypeName(), false)
inferMethodCallReturnTypeDecl(ref.methodName(), receiverDecl, ref.arguments())
?: classMethodReturnTypeDecl(ref.receiverTypeName(), ref.methodName())
?: resolveClassByName(ref.receiverTypeName())?.let { classMethodReturnTypeDecl(it, ref.methodName()) }
}
is CallRef -> callReturnTypeDeclByRef[ref] ?: inferCallReturnTypeDecl(ref)
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
@ -5122,6 +5154,7 @@ class Compiler(
private fun inferMethodCallReturnTypeDecl(ref: MethodCallRef): TypeDecl? {
methodReturnTypeDeclByRef[ref]?.let { return it }
val inferred = inferMethodCallReturnTypeDecl(ref.name, resolveReceiverTypeDecl(ref.receiver), ref.args)
?: classMethodReturnTypeDecl(resolveReceiverClassForMember(ref.receiver), ref.name)
if (inferred != null) {
methodReturnTypeDeclByRef[ref] = inferred
}
@ -5272,21 +5305,17 @@ class Compiler(
}
}
private fun implicitItTypeNameForMemberLambda(receiver: ObjRef, memberName: String): String? {
private fun implicitItTypeForMemberLambda(receiver: ObjRef, memberName: String): TypeDecl? {
if (memberName == "fill" && isListTypeRef(receiver)) {
return "Int"
return TypeDecl.Simple("Int", false)
}
if (memberName == "let" || memberName == "also") {
return inferReceiverTypeFromRef(receiver)
val receiverType = inferTypeDeclFromRef(receiver) ?: resolveReceiverTypeDecl(receiver)
return receiverType?.let { makeTypeDeclNonNullable(it) }
}
val typeDecl = when (memberName) {
return when (memberName) {
"forEach", "map" -> inferIterableElementTypeDecl(receiver)
else -> null
} ?: return null
return when (typeDecl) {
is TypeDecl.Simple -> typeDecl.name.substringAfterLast('.')
is TypeDecl.Generic -> typeDecl.name.substringAfterLast('.')
else -> resolveTypeDeclObjClass(typeDecl)?.className
}
}
@ -6293,7 +6322,7 @@ class Compiler(
*/
private suspend fun parseArgs(
expectedTailBlockReceiver: String? = null,
implicitItType: String? = null
implicitItType: TypeDecl? = null
): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
@ -7732,7 +7761,7 @@ class Compiler(
classCtx?.let { ctx ->
val callableMembers = classScopeCallableMembersByClassName.getOrPut(qualifiedName) { mutableSetOf() }
predeclareClassScopeMembers(qualifiedName, ctx.classScopeMembers, callableMembers)
predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides)
predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides, ctx.declaredMethodNames)
val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null
if (existingExternInfo != null) {
ctx.memberFieldIds.putAll(existingExternInfo.fieldIds)
@ -7781,6 +7810,13 @@ class Compiler(
ctx.memberFieldIds[param.name] = ctx.nextFieldId++
}
}
// Pre-assign method IDs for all declared methods so forward
// references within the class body resolve correctly.
for (method in ctx.declaredMethodNames) {
if (!ctx.memberMethodIds.containsKey(method)) {
ctx.memberMethodIds[method] = ctx.nextMethodId++
}
}
compileClassInfos[qualifiedName] = CompileClassInfo(
name = qualifiedName,
packageName = packageName,

View File

@ -0,0 +1,124 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng
/**
* Pure Kotlin/KMP implementation of legacy hash functions.
*
* SHA-1 is cryptographically broken and must not be used for security-sensitive
* purposes (password hashing, digital signatures, etc.). It is retained here
* solely for compatibility with legacy protocols and file formats that require it.
*/
internal object LegacyDigest {
/**
* Compute the SHA-1 digest of [input] and return it as a lowercase hex string.
*
* SHA-1 is **cryptographically insecure**. Use only for protocol compatibility.
*/
fun sha1Hex(input: ByteArray): String {
val digest = sha1(input)
return buildString(40) {
for (b in digest) {
val v = b.toInt() and 0xFF
if (v < 16) append('0')
append(v.toString(16))
}
}
}
private fun sha1(input: ByteArray): ByteArray {
// Initial hash values
var h0 = 0x67452301
var h1 = 0xEFCDAB89.toInt()
var h2 = 0x98BADCFE.toInt()
var h3 = 0x10325476
var h4 = 0xC3D2E1F0.toInt()
// Pre-processing: pad message to a multiple of 512 bits (64 bytes).
// Append 0x80, then zeros, then the 64-bit big-endian bit-length.
val msgLen = input.size
val bitLen = msgLen.toLong() * 8L
// Minimum padding: 1 byte (0x80) + 8 bytes (length) = 9 bytes.
// Total length must be ≡ 0 (mod 64).
val padded = run {
val totalLen = ((msgLen + 1 + 8 + 63) / 64) * 64
ByteArray(totalLen).also { buf ->
input.copyInto(buf)
buf[msgLen] = 0x80.toByte()
// Big-endian 64-bit bit-length in the last 8 bytes
for (i in 0..7) {
buf[totalLen - 8 + i] = ((bitLen ushr (56 - i * 8)) and 0xFF).toByte()
}
}
}
val w = IntArray(80)
var blockStart = 0
while (blockStart < padded.size) {
// Build the 80-word message schedule
for (i in 0..15) {
val off = blockStart + i * 4
w[i] = ((padded[off].toInt() and 0xFF) shl 24) or
((padded[off + 1].toInt() and 0xFF) shl 16) or
((padded[off + 2].toInt() and 0xFF) shl 8) or
(padded[off + 3].toInt() and 0xFF)
}
for (i in 16..79) {
val x = w[i - 3] xor w[i - 8] xor w[i - 14] xor w[i - 16]
w[i] = (x shl 1) or (x ushr 31) // ROTL-1
}
var a = h0; var b = h1; var c = h2; var d = h3; var e = h4
for (t in 0..19) {
val f = (b and c) or (b.inv() and d)
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x5A827999 + w[t]
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
}
for (t in 20..39) {
val f = b xor c xor d
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x6ED9EBA1 + w[t]
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
}
for (t in 40..59) {
val f = (b and c) or (b and d) or (c and d)
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x8F1BBCDC.toInt() + w[t]
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
}
for (t in 60..79) {
val f = b xor c xor d
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0xCA62C1D6.toInt() + w[t]
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
}
h0 += a; h1 += b; h2 += c; h3 += d; h4 += e
blockStart += 64
}
return ByteArray(20).also { out ->
fun putInt(off: Int, v: Int) {
out[off] = (v ushr 24).toByte()
out[off + 1] = (v ushr 16).toByte()
out[off + 2] = (v ushr 8).toByte()
out[off + 3] = v.toByte()
}
putInt(0, h0); putInt(4, h1); putInt(8, h2); putInt(12, h3); putInt(16, h4)
}
}
}

View File

@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.stdlib_included.complexLyng
import net.sergeych.lyng.stdlib_included.decimalLyng
import net.sergeych.lyng.stdlib_included.legacyDigestLyng
import net.sergeych.lyng.stdlib_included.matrixLyng
import net.sergeych.lyng.stdlib_included.observableLyng
import net.sergeych.lyng.stdlib_included.operatorsLyng
@ -896,6 +897,19 @@ class Script(
module.eval(Source("lyng.complex", complexLyng))
ObjComplexSupport.bindTo(module)
}
addPackage("lyng.legacy_digest") { module ->
module.eval(Source("lyng.legacy_digest", legacyDigestLyng))
module.bindObject("LegacyDigest") {
addFun("sha1") {
val data = requiredArg<Obj>(0)
val bytes = when (data) {
is ObjBuffer -> data.byteArray.toByteArray()
else -> data.toString().encodeToByteArray()
}
ObjString(LegacyDigest.sha1Hex(bytes))
}
}
}
addPackage("lyng.buffer") {
it.addConstDoc(
name = "Buffer",

View File

@ -926,11 +926,16 @@ open class ObjClass(
candidate.methodId
} else null
}
methodId ?: inherited ?: methodIdMap[name]?.let { it } ?: run {
val id = methodId ?: inherited ?: methodIdMap[name]?.let { it } ?: run {
methodIdMap[name] = nextMethodId
nextMethodId++
methodIdMap[name]!!
}
// Register the resolved ID so subsequent assignMethodId calls (e.g. for static
// methods) don't reuse the same numeric slot for a different member.
methodIdMap[name] = id
if (id >= nextMethodId) nextMethodId = id + 1
id
} else {
methodId
}

View File

@ -1102,4 +1102,119 @@ class OOTest {
""".trimIndent())
}
@Test
fun testExtendingObjectWithExternals2() = runTest {
val s = EvalSession()
s.eval("""
import lyng.serialization
object Storage {
extern val spaceUsed: Int
extern val spaceAvailable: Int
/*
Return packed binary data or null
*/
extern fun getPacked(key: String): Buffer?
/*
Upsert packed binary data
*/
extern fun putPacked(key: String,value: Buffer)
/*
Delete data.
@return true if data were actually deleted, false means
there were no data for the key.
*/
extern fun delete(key: String): Bool
override fun putAt(key: String,value: Object) {
putPacked(key, Lynon.encode(value).toBuffer())
}
override fun getAt(key: String): Object? =
getPacked(key)?.let { Lynon.decode(it.toBitInput()) }
}
""".trimIndent()
)
val scope = s.getScope() as ModuleScope
scope.bindObject("Storage") {
init { _ ->
data = mutableMapOf<String, ObjBuffer>()
}
addVal("spaceUsed") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
ObjInt(storage.values.sumOf { it.size }.toLong())
}
addVal("spaceAvailable") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val capacity = 1_024
ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
}
addFun("getPacked") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
storage[key] ?: ObjNull
}
addFun("putPacked") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
val value = args.list[1] as ObjBuffer
storage[key] = value
ObjVoid
}
addFun("delete") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
ObjBool(storage.remove(key) != null)
}
}
s.eval("""
assertEquals(0, Storage.spaceUsed)
assertEquals(1024, Storage.spaceAvailable)
val missing: String? = Storage["missing"]
assertEquals(null, missing)
Storage["name"] = "alice"
Storage["count"] = 42
val name: String? = Storage["name"]
val count: Int? = Storage["count"]
assertEquals("alice", name)
assertEquals(42, count)
assert(Storage.spaceUsed > 0)
assert(Storage.spaceAvailable < 1024)
val wrappedName: String? = Storage.getAt("name")
assertEquals("alice", wrappedName)
Storage.putAt("flag", true)
val flag: Bool? = Storage["flag"]
assertEquals(true, flag)
assert(Storage.delete("name"))
val deletedName: String? = Storage["name"]
assertEquals(null, deletedName)
assert(!Storage.delete("name"))
""".trimIndent())
}
@Test
fun testForwardSymbolsUsageMustBeAllowed() = runTest {
eval("""
class Foo(x) {
fn fn2() {
fn1()
println("fn2")
}
fn fn1() {
println("fn1")
}
}
val foo = Foo(33)
foo.fn2()
""".trimIndent())
}
}

View File

@ -109,6 +109,19 @@ class ScriptTest {
assertTrue(res is ObjString && res.value.isNotEmpty())
}
@Test
fun testNullableLetKeepsReceiverMemberType() = runTest {
Script.newScope().eval(
"""
import lyng.serialization
val packed: Buffer? = Lynon.encode("alice").toBuffer()
val decoded = packed?.let { Lynon.decode(it.toBitInput()) }
assertEquals("alice", decoded)
""".trimIndent()
)
}
@Test
fun testNoInfiniteRecursionOnUnknownInNestedClosure() = runTest {
val scope = Script.newScope()

View File

@ -111,4 +111,22 @@ class ComplexModuleTest {
)
}
@Test
fun testOperatorSlotDispatch() = runTest {
val scope = Script.newScope()
scope.eval(
"""
import lyng.complex
val a = Complex(1.0, 2.0)
val b = Complex(3.0, -1.0)
val product = a * b
assertEquals(5.0, product.re)
assertEquals(5.0, product.im)
val sum = a + Complex(0.0, 0.0)
assertEquals(1.0, sum.re)
assertEquals(2.0, sum.im)
""".trimIndent()
)
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class LegacyDigestTest {
// --- Kotlin-level unit tests for the SHA-1 implementation ---
@Test
fun sha1KotlinEmptyString() {
// SHA-1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709
assertEquals(
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
LegacyDigest.sha1Hex(ByteArray(0))
)
}
@Test
fun sha1KotlinAbc() {
// SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d
assertEquals(
"a9993e364706816aba3e25717850c26c9cd0d89d",
LegacyDigest.sha1Hex("abc".encodeToByteArray())
)
}
@Test
fun sha1KotlinLongerMessage() {
// SHA-1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
// = 84983e441c3bd26ebaae4aa1f95129e5e54670f1
val msg = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
assertEquals(
"84983e441c3bd26ebaae4aa1f95129e5e54670f1",
LegacyDigest.sha1Hex(msg.encodeToByteArray())
)
}
@Test
fun sha1KotlinExactlyOneBlock() {
// "The quick brown fox jumps over the lazy dog"
// SHA-1 = 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
val msg = "The quick brown fox jumps over the lazy dog"
assertEquals(
"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
LegacyDigest.sha1Hex(msg.encodeToByteArray())
)
}
// --- Lyng-level integration tests ---
@Test
fun sha1LyngStringInput() = runTest {
eval(
"""
import lyng.legacy_digest
assertEquals(
"a9993e364706816aba3e25717850c26c9cd0d89d",
LegacyDigest.sha1("abc")
)
assertEquals(
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
LegacyDigest.sha1("")
)
""".trimIndent()
)
}
@Test
fun sha1LyngBufferInput() = runTest {
eval(
"""
import lyng.legacy_digest
import lyng.buffer
val buf = Buffer.decodeHex("616263") // "abc" in hex
assertEquals(
"a9993e364706816aba3e25717850c26c9cd0d89d",
LegacyDigest.sha1(buf)
)
""".trimIndent()
)
}
@Test
fun sha1LyngReturnType() = runTest {
eval(
"""
import lyng.legacy_digest
val h = LegacyDigest.sha1("hello")
assert(h is String)
assertEquals(40, h.length)
""".trimIndent()
)
}
}

View File

@ -0,0 +1,33 @@
package lyng.legacy_digest
/*
Legacy cryptographic digest functions.
⚠️ WARNING: The functions in this module use algorithms that are
CRYPTOGRAPHICALLY BROKEN and must NOT be used for any security-sensitive
purpose, including:
- Password hashing
- Digital signatures or MACs
- Integrity verification against adversarial tampering
Use only for interoperability with legacy protocols, file formats, or
external systems that require these specific hash values.
For secure hashing, use SHA-256 or better (not yet available in this stdlib).
*/
extern object LegacyDigest {
/*
Compute the SHA-1 digest of `data` and return it as a lowercase hex string.
`data` may be a String (hashed as UTF-8) or a Buffer (hashed as raw bytes).
⚠️ SHA-1 is cryptographically broken. Use only for protocol compatibility.
Example:
import lyng.legacy_digest
val digest = LegacyDigest.sha1("hello world")
// "2aae6c69a64d7c8b96eacbbe0ced5df20f3a8e89" (note: known SHA-1 of "hello world")
*/
extern fun sha1(data): String
}