fixed many bugs with variable visibility in mixed scopes

This commit is contained in:
Sergey Chernov 2025-08-17 12:18:51 +03:00
parent eca746b189
commit 464a6dcb99
14 changed files with 285 additions and 162 deletions

View File

@ -0,0 +1,14 @@
Provide:
fun outer(a1)
// a1 is caller.a1:arg
val a1_local = a1 + 1
// we return lambda:
{ it ->
// a1_local
a1_lcoal + it
}
}

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.8.10-SNAPSHOT"
version = "0.8.11-SNAPSHOT"
buildscript {
repositories {

View File

@ -48,13 +48,12 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public,
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) {
fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = defaultRecordType)
recordType = ObjRecord.Type.Argument)
}
// will be used with last lambda arg fix

View File

@ -24,10 +24,33 @@ import net.sergeych.lyng.obj.ObjRecord
* Inherits [Scope.args] and [Scope.thisObj] from [callScope] and adds lookup for symbols
* from [closureScope] with proper precedence
*/
class ClosureScope(val callScope: Scope,val closureScope: Scope) : Scope(callScope, callScope.args, thisObj = callScope.thisObj) {
class ClosureScope(val callScope: Scope, val closureScope: Scope) :
Scope(callScope, callScope.args, thisObj = callScope.thisObj) {
override fun get(name: String): ObjRecord? {
// closure should be treated below callScope
return super.get(name) ?: closureScope.get(name)
// we take arguments from the callerScope, the rest
// from the closure.
// note using super, not callScope, as arguments are assigned by the constructor
// and are not assigned yet to vars in callScope self:
super.objects[name]?.let {
// if( name == "predicate" ) {
// println("predicate: ${it.type.isArgument}: ${it.value}")
// }
if( it.type.isArgument ) return it
}
return closureScope.get(name)
}
}
class ApplyScope(_parent: Scope,val applied: Scope) : Scope(_parent, thisObj = applied.thisObj) {
override fun get(name: String): ObjRecord? {
return applied.get(name) ?: super.get(name)
}
override fun applyClosure(closure: Scope): Scope {
return this
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2025 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
sealed class CodeContext {
class Module(@Suppress("unused") val packageName: String?): CodeContext()
class Function(val name: String): CodeContext()
class ClassBody(val name: String): CodeContext()
}

View File

@ -45,6 +45,17 @@ class Compiler(
private fun popInitScope(): MutableList<Statement> = initStack.removeLast()
private val codeContexts = mutableListOf<CodeContext>(CodeContext.Module(null))
private suspend fun <T>inCodeContext(context: CodeContext,f: suspend ()->T): T {
return try {
codeContexts.add(context)
f()
} finally {
codeContexts.removeLast()
}
}
private suspend fun parseScript(): Script {
val statements = mutableListOf<Statement>()
val start = cc.currentPos()
@ -504,7 +515,7 @@ class Compiler(
val callStatement = statement {
// and the source closure of the lambda which might have other thisObj.
val context = ClosureScope(this, closure!!) //AppliedScope(closure!!, args, this)
val context = this.applyClosure(closure!!)
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.list
@ -516,7 +527,7 @@ class Compiler(
// more args: it is a list of args
else -> ObjList(l.toMutableList())
}
context.addItem("it", false, itValue)
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
} else {
// assign vars as declared the standard way
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
@ -525,7 +536,7 @@ class Compiler(
}
return Accessor { x ->
if (closure == null) closure = x
closure = x
callStatement.asReadonly
}
}
@ -1202,73 +1213,76 @@ class Compiler(
private suspend fun parseClassDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true)
else null
return inCodeContext(CodeContext.ClassBody(nameToken.value)) {
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true)
else null
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
nameToken.pos,
"Bad class declaration: expected ')' at the end of the primary constructor"
)
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
nameToken.pos,
"Bad class declaration: expected ')' at the end of the primary constructor"
)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next()
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next()
pushInitScope()
pushInitScope()
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body
parseScript().also {
cc.skipTokens(Token.Type.RBRACE)
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body
parseScript().also {
cc.skipTokens(Token.Type.RBRACE)
}
} else {
cc.previous()
null
}
} else {
cc.previous()
null
}
val initScope = popInitScope()
val initScope = popInitScope()
// create class
val className = nameToken.value
// create class
val className = nameToken.value
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Var else AccessType.Initialization
// @Suppress("UNUSED_VARIABLE") val defaultVisibility = Visibility.Public
// create instance constructor
// create custom objClass with all fields and instance constructor
// create instance constructor
// create custom objClass with all fields and instance constructor
val constructorCode = statement {
// constructor code is registered with class instance and is called over
// new `thisObj` already set by class to ObjInstance.instanceContext
thisObj as ObjInstance
val constructorCode = statement {
// constructor code is registered with class instance and is called over
// new `thisObj` already set by class to ObjInstance.instanceContext
thisObj as ObjInstance
// the context now is a "class creation context", we must use its args to initialize
// fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(this)
// the context now is a "class creation context", we must use its args to initialize
// fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(this)
thisObj
}
// inheritance must alter this code:
val newClass = ObjInstanceClass(className).apply {
instanceConstructor = constructorCode
constructorMeta = constructorArgsDeclaration
}
return statement {
// the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc.
addItem(className, false, newClass)
if (initScope.isNotEmpty()) {
val classScope = copy(newThisObj = newClass)
newClass.classScope = classScope
for (s in initScope)
s.execute(classScope)
thisObj
}
// inheritance must alter this code:
val newClass = ObjInstanceClass(className).apply {
instanceConstructor = constructorCode
constructorMeta = constructorArgsDeclaration
}
statement {
// the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc.
addItem(className, false, newClass)
if (initScope.isNotEmpty()) {
val classScope = copy(newThisObj = newClass)
newClass.classScope = classScope
for (s in initScope)
s.execute(classScope)
}
newClass
}
newClass
}
}
@ -1639,8 +1653,7 @@ class Compiler(
}
}
private suspend fun
parseFunctionDeclaration(
private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false,
@ -1654,7 +1667,7 @@ class Compiler(
else t.value
val annotation = lastAnnotation
val parentContext = codeContexts.last()
t = cc.next()
// Is extension?
@ -1679,61 +1692,66 @@ class Compiler(
if (cc.current().type == Token.Type.COLON) parseTypeDeclaration()
// Here we should be at open body
val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") }
else
parseBlock()
return inCodeContext(CodeContext.Function(name)) {
var closure: Scope? = null
// Here we should be at open body
val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") }
else
parseBlock()
val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start
var closure: Scope? = null
// restore closure where the function was defined, and making a copy of it
// for local space (otherwise it will write local stuff to closure!)
val context = closure?.let { ClosureScope(callerContext, it) }
?: callerContext.raiseError("bug: closure not set")
val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start
// load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
if (extTypeName != null) {
context.thisObj = callerContext.thisObj
}
fnStatements.execute(context)
}
val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure
// for the function
closure = context
// restore closure where the function was defined, and making a copy of it
// for local space. If there is no closure, we are in, say, class context where
// the closure is in the class initialization and we needn't more:
val context = closure?.let { ClosureScope(callerContext, it) }
?: callerContext
val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody)
?: fnBody
extTypeName?.let { typeName ->
// class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true) {
// ObjInstance has a fixed instance scope, so we need to build a closure
(thisObj as? ObjInstance)?.let { i ->
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
}
// other classes can create one-time scope for this rare case:
?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
// load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
if (extTypeName != null) {
context.thisObj = callerContext.thisObj
}
fnStatements.execute(context)
}
// regular function/method
?: context.addItem(name, false, annotatedFnBody, visibility)
// as the function can be called from anywhere, we have
// saved the proper context in the closure
annotatedFnBody
val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure
// for the function, unless we're in the class scope:
if( isStatic || parentContext !is CodeContext.ClassBody)
closure = context
val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody)
?: fnBody
extTypeName?.let { typeName ->
// class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true) {
// ObjInstance has a fixed instance scope, so we need to build a closure
(thisObj as? ObjInstance)?.let { i ->
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
}
// other classes can create one-time scope for this rare case:
?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
}
}
// regular function/method
?: context.addItem(name, false, annotatedFnBody, visibility)
// as the function can be called from anywhere, we have
// saved the proper context in the closure
annotatedFnBody
}
if (isStatic) {
currentInitScope += fnCreateStatement
NopStatement
} else
fnCreateStatement
}
return if (isStatic) {
currentInitScope += fnCreateStatement
NopStatement
} else
fnCreateStatement
}
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {

View File

@ -131,9 +131,16 @@ open class Scope(
open operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
objects[name]
(objects[name]
?: parent?.get(name)
?: thisObj.objClass.getInstanceMemberOrNull(name)
?: thisObj.objClass
.getInstanceMemberOrNull(name)
)
// ?.also {
// if( name == "predicate") {
// println("got predicate $it")
// }
// }
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
@ -242,6 +249,8 @@ open class Scope(
}
open fun applyClosure(closure: Scope): Scope = ClosureScope(this, closure)
companion object {
fun new(): Scope =

View File

@ -296,9 +296,14 @@ open class Obj {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
}
addFn("apply") {
val newContext = (thisObj as? ObjInstance)?.instanceScope ?: this
args.firstAndOnly()
.callOn(newContext)
val body = args.firstAndOnly()
(thisObj as? ObjInstance)?.let {
println("apply in ${thisObj is ObjInstance}, ${it.instanceScope}")
body.callOn(ApplyScope(this, it.instanceScope))
} ?: run {
println("apply on non-instance $thisObj")
body.callOn(this)
}
thisObj
}
addFn("also") {

View File

@ -17,31 +17,26 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
import net.sergeych.lyng.statement
class ObjDelegateContext()
class ObjDelegate(
val getter: Statement,
val setter: Statement = statement { raiseNotImplemented("setter is not implemented") }
): Obj() {
override suspend fun assign(scope: Scope, other: Obj): Obj? {
setter.execute(scope.copy(Arguments(other)))
return other
}
companion object {
val type = object: ObjClass("Delegate") {
override suspend fun callOn(scope: Scope): Obj {
scope.raiseError("Delegate should not be constructed directly")
}
}.apply {
}
}
}
//class ObjDelegateContext()
//
//class ObjDelegate(
// val getter: Statement,
// val setter: Statement = statement { raiseNotImplemented("setter is not implemented") }
//): Obj() {
//
// override suspend fun assign(scope: Scope, other: Obj): Obj? {
// setter.execute(scope.copy(Arguments(other)))
// return other
// }
//
// companion object {
// val type = object: ObjClass("Delegate") {
// override suspend fun callOn(scope: Scope): Obj {
// scope.raiseError("Delegate should not be constructed directly")
// }
// }.apply {
//
// }
// }
//
//}

View File

@ -40,12 +40,12 @@ class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
val data = requireOnlyArg<Obj>()
try {
val channel = thisAs<ObjFlowBuilder>().output
if( !channel.isClosedForSend )
if (!channel.isClosedForSend)
channel.send(data)
else
throw ScriptFlowIsNoMoreCollected()
} catch (x: Exception) {
if( x !is CancellationException )
if (x !is CancellationException)
x.printStackTrace()
throw ScriptFlowIsNoMoreCollected()
}
@ -62,12 +62,10 @@ private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChann
globalLaunch {
try {
producer.execute(builderScope)
}
catch(x: ScriptFlowIsNoMoreCollected) {
} catch (x: ScriptFlowIsNoMoreCollected) {
x.printStackTrace()
// premature flow closing, OK
}
catch(x: Exception) {
} catch (x: Exception) {
x.printStackTrace()
}
channel.close()
@ -87,7 +85,11 @@ class ObjFlow(val producer: Statement, val scope: Scope) : Obj() {
}.apply {
addFn("iterator") {
val objFlow = thisAs<ObjFlow>()
ObjFlowIterator( statement { objFlow.producer.execute(ClosureScope(this,objFlow.scope)) } )
ObjFlowIterator(statement {
objFlow.producer.execute(
ClosureScope(this, objFlow.scope)
)
})
}
}
}
@ -105,9 +107,10 @@ class ObjFlowIterator(val producer: Statement) : Obj() {
private var isCancelled = false
private fun checkNotCancelled(scope: Scope) {
if( isCancelled )
if (isCancelled)
scope.raiseIllegalState("iteration is cancelled")
}
suspend fun hasNext(scope: Scope): ObjBool {
checkNotCancelled(scope)
// cold start:

View File

@ -35,11 +35,15 @@ data class ObjRecord(
Field(true, true),
@Suppress("unused")
Fun,
@Suppress("unused")
ConstructorField(true, true),
Argument(true, true),
@Suppress("unused")
Class,
Enum,
Other
Other;
val isArgument get() = this == Argument
}
@Suppress("unused")
fun qualifiedName(name: String): String =

View File

@ -35,7 +35,7 @@ fun Iterable.filter(predicate) {
val list = this
flow {
for( item in list ) {
if( predicate(item) ) {
if( predicate(item) ) {ln
emit(item)
}
}
@ -93,5 +93,23 @@ fun Iterable.joinToString(prefix=" ", transformer=null) {
result ?: ""
}
fun Iterable.any(predicate): Bool {
for( i in this ) {
if( predicate(i) ) {
break true
// todo: add cancelIteration() in for loop!
}
} else false
}
fun Iterable.all(predicate): Bool {
for( i in this ) {
if( !predicate(i) ) {
break false
}
}
else true
}
""".trimIndent()

View File

@ -1359,9 +1359,6 @@ class ScriptTest {
}
val prefix = ":"
val lambda = {
prefix + getText() + "!"
}
val text = "invalid"
val t1 = T("foo")
@ -1371,18 +1368,22 @@ class ScriptTest {
// it must take "text" from class t1:
assertEquals("foo", text)
assertEquals( "foo!", getText() )
assertEquals( ":foo!!", lambda() )
assertEquals( ":foo!!", {
prefix + getText() + "!"
}())
}
t2.apply {
assertEquals("bar", text)
assertEquals( "bar!", getText() )
assertEquals( ":bar!!", lambda() )
assertEquals( ":bar!!", {
prefix + getText() + "!"
}())
}
// worst case: names clash
fun badOne() {
val prefix = "&"
t1.apply {
assertEquals( ":foo!!", lambda() )
assertEquals( ":foo!!", prefix + getText() + "!" )
}
}
badOne()
@ -2941,5 +2942,4 @@ class ScriptTest {
}
}

View File

@ -23,8 +23,9 @@ class StdlibTest {
@Test
fun testIterableFilter() = runTest {
eval("""
assertEquals([1,3,5,7], (1..8).filter{ it % 2 == 1 }.toList() )
assertEquals([2,4,6,8], (1..8).filter{ it % 2 == 0 }.toList() )
assertEquals([2,4,6,8], (1..8).filter{ println("call2"); it % 2 == 0 }.toList() )
println("-------------------")
assertEquals([1,3,5,7], (1..8).filter{ println("call1"); it % 2 == 1 }.toList() )
""".trimIndent())
}
@ -46,6 +47,16 @@ class StdlibTest {
""".trimIndent())
}
@Test
fun testAnyAndAll() = runTest {
eval("""
assert( [1,2,3].any { it > 2 } )
assert( ![1,2,3].any { it > 4 } )
assert( [1,2,3].all { it <= 3 } )
""".trimIndent())
}
@Test
fun testRingBuffer() = runTest {
eval("""