diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fe03481 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# AI Agent Notes + +## Kotlin/Wasm generation guardrails +- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`. +- Do not use `statement { ... }` or other inline suspend lambdas in compiler hot paths (e.g., parsing/var declarations, initializer thunks). +- If you need a wrapper for delegated properties, check for `getValue` explicitly and return a concrete `Statement` object when missing; avoid `onNotFoundResult` lambdas. +- If wasmJs browser tests hang, first run `:lynglib:wasmJsNodeTest` and look for wasm compilation errors; hangs usually mean module instantiation failed. +- Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead. diff --git a/README.md b/README.md index 3771229..cb79d83 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ fun swapEnds(first, args..., last, f) { - [Samples directory](docs/samples) - [Formatter (core + CLI + IDE)](docs/formatter.md) - [Books directory](docs) +- [AI agent guidance](AGENTS.md) ## Integration in Kotlin multiplatform @@ -231,4 +232,4 @@ __Sergey Chernov__ @sergeych: Initial idea and architecture, language concept, d __Yulia Nezhinskaya__ @AlterEgoJuliaN: System analysis, math and features design. -[parallelism]: docs/parallelism.md \ No newline at end of file +[parallelism]: docs/parallelism.md diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 07208ed..4dece17 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -20,6 +20,7 @@ package net.sergeych.lyng import net.sergeych.lyng.Compiler.Companion.compile import net.sergeych.lyng.miniast.* import net.sergeych.lyng.obj.* +import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportProvider /** @@ -95,6 +96,11 @@ class Compiler( } } + private var anonCounter = 0 + private fun generateAnonName(pos: Pos): String { + return "${"$"}${"Anon"}_${pos.line+1}_${pos.column}_${++anonCounter}" + } + private fun pushPendingDocToken(t: Token) { val s = stripCommentLexeme(t.value) if (pendingDocStart == null) pendingDocStart = t.pos @@ -110,14 +116,30 @@ class Compiler( private fun consumePendingDoc(): MiniDoc? { if (pendingDocLines.isEmpty()) return null - val raw = pendingDocLines.joinToString("\n").trimEnd() - val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim() val start = pendingDocStart ?: cc.currentPos() - val doc = MiniDoc(MiniRange(start, start), raw = raw, summary = summary) + val doc = MiniDoc.parse(MiniRange(start, start), pendingDocLines) clearPendingDoc() return doc } + private fun nextNonWhitespace(): Token { + while (true) { + val t = cc.next() + when (t.type) { + Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> { + pushPendingDocToken(t) + } + + Token.Type.NEWLINE -> { + if (!prevWasComment) clearPendingDoc() else prevWasComment = false + } + + Token.Type.EOF -> return t + else -> return t + } + } + } + // Set just before entering a declaration parse, taken from keyword token position private var pendingDeclStart: Pos? = null private var pendingDeclDoc: MiniDoc? = null @@ -163,14 +185,13 @@ class Compiler( miniSink?.onScriptStart(start) do { val t = cc.current() - if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINLGE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { + if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { when (t.type) { - Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t) + Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t) Token.Type.NEWLINE -> { // A standalone newline not immediately following a comment resets doc buffer if (!prevWasComment) clearPendingDoc() else prevWasComment = false } - else -> {} } cc.next() @@ -248,7 +269,7 @@ class Compiler( when (t.type) { Token.Type.RBRACE, Token.Type.EOF, Token.Type.SEMICOLON -> {} else -> - throw ScriptError(t.pos, "unexpeced `${t.value}` here") + throw ScriptError(t.pos, "unexpected `${t.value}` here") } break } @@ -280,9 +301,13 @@ class Compiler( } private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null + private var isTransientFlag: Boolean = false + private var lastLabel: String? = null private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { lastAnnotation = null + lastLabel = null + isTransientFlag = false while (true) { val t = cc.next() return when (t.type) { @@ -300,14 +325,28 @@ class Compiler( } Token.Type.ATLABEL -> { + val label = t.value + if (label == "Transient") { + isTransientFlag = true + continue + } + if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { + lastLabel = label + } lastAnnotation = parseAnnotation(t) continue } Token.Type.LABEL -> continue - Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue + Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> { + pushPendingDocToken(t) + continue + } - Token.Type.NEWLINE -> continue + Token.Type.NEWLINE -> { + if (!prevWasComment) clearPendingDoc() else prevWasComment = false + continue + } Token.Type.SEMICOLON -> continue @@ -396,7 +435,7 @@ class Compiler( val t = cc.next() val startPos = t.pos when (t.type) { -// Token.Type.NEWLINE, Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> { +// Token.Type.NEWLINE, Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> { // continue // } @@ -595,7 +634,7 @@ class Compiler( // to skip in parseExpression: val current = cc.current() val right = - if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINLGE_LINE_COMMENT) + if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINGLE_LINE_COMMENT) null else parseExpression() @@ -648,6 +687,7 @@ class Compiler( private suspend fun parseLambdaExpression(): ObjRef { // lambda args are different: val startPos = cc.currentPos() + val label = lastLabel val argsDeclaration = parseArgsDeclaration() if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW) throw ScriptError( @@ -655,17 +695,25 @@ class Compiler( "lambda must have either valid arguments declaration with '->' or no arguments" ) - val body = parseBlock(skipLeadingBrace = true) + val paramNames = argsDeclaration?.params?.map { it.name } ?: emptyList() + + label?.let { cc.labels.add(it) } + val body = inCodeContext(CodeContext.Function("")) { + withLocalNames(paramNames.toSet()) { + parseBlock(skipLeadingBrace = true) + } + } + label?.let { cc.labels.remove(it) } return ValueFnRef { closureScope -> - statement { + statement(body.pos) { scope -> // and the source closure of the lambda which might have other thisObj. - val context = this.applyClosure(closureScope) + val context = scope.applyClosure(closureScope) // Execute lambda body in a closure-aware context. Blocks inside the lambda // will create child scopes as usual, so re-declarations inside loops work. if (argsDeclaration == null) { // no args: automatic var 'it' - val l = args.list + val l = scope.args.list val itValue: Obj = when (l.size) { // no args: it == void 0 -> ObjVoid @@ -679,7 +727,12 @@ class Compiler( // assign vars as declared the standard way argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val) } - body.execute(context) + try { + body.execute(context) + } catch (e: ReturnException) { + if (e.label == null || e.label == label) e.result + else throw e + } }.asReadonly } } @@ -862,9 +915,17 @@ class Compiler( } Token.Type.NEWLINE -> {} - Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT -> {} + Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {} + + Token.Type.ID, Token.Type.ATLABEL -> { + var isTransient = false + if (t.type == Token.Type.ATLABEL) { + if (t.value == "Transient") { + isTransient = true + t = cc.next() + } else throw ScriptError(t.pos, "Unexpected label in argument list") + } - Token.Type.ID -> { // visibility val visibility = if (isClassDeclaration && t.value == "private") { t = cc.next() @@ -892,12 +953,13 @@ class Compiler( else -> null } + // type information (semantic + mini syntax) + val (typeInfo, miniType) = parseTypeDeclarationWithMini() + var defaultValue: Statement? = null cc.ifNextIs(Token.Type.ASSIGN) { defaultValue = parseExpression() } - // type information (semantic + mini syntax) - val (typeInfo, miniType) = parseTypeDeclarationWithMini() val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true) result += ArgsDeclaration.Item( t.value, @@ -907,7 +969,8 @@ class Compiler( isEllipsis, defaultValue, access, - visibility + visibility, + isTransient ) // important: valid argument list continues with ',' and ends with '->' or ')' @@ -955,7 +1018,10 @@ class Compiler( private fun parseTypeDeclarationWithMini(): Pair { // Only parse a type if a ':' follows; otherwise keep current behavior if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null) + return parseTypeExpressionWithMini() + } + private fun parseTypeExpressionWithMini(): Pair { // Parse a qualified base name: ID ('.' ID)* val segments = mutableListOf() var first = true @@ -991,41 +1057,28 @@ class Compiler( else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable) } - // Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now) - var args: MutableList? = null + // Optional generic arguments: '<' Type (',' Type)* '>' + var miniArgs: MutableList? = null + var semArgs: MutableList? = null val afterBasePos = cc.savePos() if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) { - args = mutableListOf() + miniArgs = mutableListOf() + semArgs = mutableListOf() do { - // Parse argument as simple or qualified type (single level), with optional nullable '?' - val argSegs = mutableListOf() - var argFirst = true - val argStart = cc.currentPos() - while (true) { - val idTok = if (argFirst) cc.requireToken( - Token.Type.ID, - "type argument name expected" - ) else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument") - argFirst = false - argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos)) - val p = cc.savePos() - val tt = cc.next() - if (tt.type == Token.Type.DOT) continue else { - cc.restorePos(p); break - } - } - val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true) - val argEnd = cc.currentPos() - val argRef = MiniTypeName(MiniRange(argStart, argEnd), argSegs.toList(), nullable = argNullable) - args += argRef + val (argSem, argMini) = parseTypeExpressionWithMini() + miniArgs += argMini + semArgs += argSem val sep = cc.next() - when (sep.type) { - Token.Type.COMMA -> { /* continue */ - } - - Token.Type.GT -> break - else -> sep.raiseSyntax("expected ',' or '>' in generic arguments") + if (sep.type == Token.Type.COMMA) { + // continue + } else if (sep.type == Token.Type.GT) { + break + } else if (sep.type == Token.Type.SHR) { + cc.pushPendingGT() + break + } else { + sep.raiseSyntax("expected ',' or '>' in generic arguments") } } while (true) lastEnd = cc.currentPos() @@ -1034,13 +1087,19 @@ class Compiler( } // Nullable suffix after base or generic - val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true) + val isNullable = if (cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)) { + true + } else if (cc.skipTokenOfType(Token.Type.IFNULLASSIGN, isOptional = true)) { + cc.pushPendingAssign() + true + } else false val endPos = cc.currentPos() - val miniRef = buildBaseRef(if (args != null) endPos else lastEnd, args, isNullable) + val miniRef = buildBaseRef(if (miniArgs != null) endPos else lastEnd, miniArgs, isNullable) // Semantic: keep simple for now, just use qualified base name with nullable flag val qualified = segments.joinToString(".") { it.name } - val sem = TypeDecl.Simple(qualified, isNullable) + val sem = if (semArgs != null) TypeDecl.Generic(qualified, semArgs, isNullable) + else TypeDecl.Simple(qualified, isNullable) return Pair(sem, miniRef) } @@ -1248,6 +1307,8 @@ class Compiler( } } + Token.Type.OBJECT -> StatementRef(parseObjectDeclaration()) + else -> null } } @@ -1307,8 +1368,8 @@ class Compiler( "private", "protected", "static", "abstract", "closed", "override", "extern", "open" -> { modifiers.add(currentToken.value) val next = cc.peekNextNonWhitespace() - if (next.type == Token.Type.ID) { - currentToken = cc.next() + if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) { + currentToken = nextNonWhitespace() } else { break } @@ -1336,43 +1397,66 @@ class Compiler( throw ScriptError(currentToken.pos, "abstract members cannot be private") pendingDeclStart = firstId.pos - pendingDeclDoc = consumePendingDoc() + // pendingDeclDoc might be already set by an annotation + if (pendingDeclDoc == null) + pendingDeclDoc = consumePendingDoc() val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody) - if (!isMember && (isOverride || isClosed)) - throw ScriptError(currentToken.pos, "modifiers override and closed are only allowed for class members") + if (!isMember && isClosed) + throw ScriptError(currentToken.pos, "modifier closed is only allowed for class members") + + if (!isMember && isOverride && currentToken.value != "fun" && currentToken.value != "fn") + throw ScriptError(currentToken.pos, "modifier override outside class is only allowed for extension functions") if (!isMember && isAbstract && currentToken.value != "class") throw ScriptError(currentToken.pos, "modifier abstract at top level is only allowed for classes") return when (currentToken.value) { - "val" -> parseVarDeclaration(false, visibility, isAbstract, isClosed, isOverride, isStatic) - "var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic) + "val" -> parseVarDeclaration(false, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern) + "var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern) "fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic) "class" -> { - if (isStatic || isClosed || isOverride || isExtern) - throw ScriptError(currentToken.pos, "unsupported modifiers for class: ${modifiers.joinToString(" ")}") - parseClassDeclaration(isAbstract) + if (isStatic || isClosed || isOverride) + throw ScriptError( + currentToken.pos, + "unsupported modifiers for class: ${modifiers.joinToString(" ")}" + ) + parseClassDeclaration(isAbstract, isExtern) } "object" -> { - if (isStatic || isClosed || isOverride || isExtern || isAbstract) - throw ScriptError(currentToken.pos, "unsupported modifiers for object: ${modifiers.joinToString(" ")}") - parseObjectDeclaration() + if (isStatic || isClosed || isOverride || isAbstract) + throw ScriptError( + currentToken.pos, + "unsupported modifiers for object: ${modifiers.joinToString(" ")}" + ) + parseObjectDeclaration(isExtern) } "interface" -> { - if (isStatic || isClosed || isOverride || isExtern || isAbstract) + if (isStatic || isClosed || isOverride || isAbstract) throw ScriptError( currentToken.pos, "unsupported modifiers for interface: ${modifiers.joinToString(" ")}" ) // interface is synonym for abstract class - parseClassDeclaration(isAbstract = true) + parseClassDeclaration(isAbstract = true, isExtern = isExtern) } - else -> throw ScriptError(currentToken.pos, "expected declaration after modifiers, found ${currentToken.value}") + "enum" -> { + if (isStatic || isClosed || isOverride || isAbstract) + throw ScriptError( + currentToken.pos, + "unsupported modifiers for enum: ${modifiers.joinToString(" ")}" + ) + parseEnumDeclaration(isExtern) + } + + else -> throw ScriptError( + currentToken.pos, + "expected declaration after modifiers, found ${currentToken.value}" + ) } } @@ -1381,7 +1465,7 @@ class Compiler( * @return parsed statement or null if, for example. [id] is not among keywords */ private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) { - "abstract", "closed", "override", "extern", "private", "protected", "static" -> { + "abstract", "closed", "override", "extern", "private", "protected", "static", "open" -> { parseDeclarationWithModifiers(id) } @@ -1418,6 +1502,7 @@ class Compiler( "while" -> parseWhileStatement() "do" -> parseDoWhileStatement() "for" -> parseForStatement() + "return" -> parseReturnStatement(id.pos) "break" -> parseBreakStatement(id.pos) "continue" -> parseContinueStatement(id.pos) "if" -> parseIfStatement() @@ -1435,12 +1520,21 @@ class Compiler( "init" -> { if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { + miniSink?.onEnterFunction(null) val block = parseBlock() + miniSink?.onExitFunction(cc.currentPos()) lastParsedBlockRange?.let { range -> miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos)) } val initStmt = statement(id.pos) { scp -> - block.execute(scp) + val cls = scp.thisObj.objClass + val saved = scp.currentClassCtx + scp.currentClassCtx = cls + try { + block.execute(scp) + } finally { + scp.currentClassCtx = saved + } ObjVoid } statement { @@ -1684,19 +1778,25 @@ class Compiler( var errorObject = throwStatement.execute(sc) // Rebind error scope to the throw-site position so ScriptError.pos is accurate val throwScope = sc.createChildScope(pos = start) - errorObject = when (errorObject) { - is ObjString -> ObjException(throwScope, errorObject.value) - is ObjException -> ObjException( + if (errorObject is ObjString) { + errorObject = ObjException(throwScope, errorObject.value).apply { getStackTrace() } + } + if (!errorObject.isInstanceOf(ObjException.Root)) { + throwScope.raiseError("this is not an exception object: $errorObject") + } + if (errorObject is ObjException) { + errorObject = ObjException( errorObject.exceptionClass, throwScope, errorObject.message, errorObject.extraData, errorObject.useStackTrace - ) - - else -> throwScope.raiseError("this is not an exception object: $errorObject") + ).apply { getStackTrace() } + throwScope.raiseError(errorObject) + } else { + val msg = errorObject.invokeInstanceMethod(sc, "message").toString(sc).value + throwScope.raiseError(errorObject, start, msg) } - throwScope.raiseError(errorObject) } } @@ -1773,27 +1873,31 @@ class Compiler( try { // body is a parsed block, it already has separate context result = body.execute(this) + } catch (e: ReturnException) { + throw e + } catch (e: LoopBreakContinueException) { + throw e } catch (e: Exception) { // convert to appropriate exception - val objException = when (e) { + val caughtObj = when (e) { is ExecutionError -> e.errorObject else -> ObjUnknownException(this, e.message ?: e.toString()) } // let's see if we should catch it: var isCaught = false for (cdata in catches) { - var exceptionObject: ObjException? = null + var match: Obj? = null for (exceptionClassName in cdata.classNames) { - val exObj = ObjException.getErrorClass(exceptionClassName) - ?: raiseSymbolNotFound("error clas not exists: $exceptionClassName") - if (objException.isInstanceOf(exObj)) { - exceptionObject = objException + val exObj = this[exceptionClassName]?.value as? ObjClass + ?: raiseSymbolNotFound("error class does not exist or is not a class: $exceptionClassName") + if (caughtObj.isInstanceOf(exObj)) { + match = caughtObj break } } - if (exceptionObject != null) { + if (match != null) { val catchContext = this.createChildScope(pos = cdata.catchVar.pos) - catchContext.addItem(cdata.catchVar.value, false, objException) + catchContext.addItem(cdata.catchVar.value, false, caughtObj) result = cdata.block.execute(catchContext) isCaught = true break @@ -1810,7 +1914,7 @@ class Compiler( } } - private fun parseEnumDeclaration(): Statement { + private fun parseEnumDeclaration(isExtern: Boolean = false): Statement { val nameToken = cc.requireToken(Token.Type.ID) val startPos = pendingDeclStart ?: nameToken.pos val doc = pendingDeclDoc ?: consumePendingDoc() @@ -1818,29 +1922,35 @@ class Compiler( pendingDeclStart = null // so far only simplest enums: val names = mutableListOf() + val positions = mutableListOf() // skip '{' cc.skipTokenOfType(Token.Type.LBRACE) - do { - val t = cc.nextNonWhitespace() - when (t.type) { - Token.Type.ID -> { - names += t.value - val t1 = cc.nextNonWhitespace() - when (t1.type) { - Token.Type.COMMA -> - continue + if (cc.peekNextNonWhitespace().type != Token.Type.RBRACE) { + do { + val t = cc.nextNonWhitespace() + when (t.type) { + Token.Type.ID -> { + names += t.value + positions += t.pos + val t1 = cc.nextNonWhitespace() + when (t1.type) { + Token.Type.COMMA -> + continue - Token.Type.RBRACE -> break - else -> { - t1.raiseSyntax("unexpected token") + Token.Type.RBRACE -> break + else -> { + t1.raiseSyntax("unexpected token") + } } } - } - else -> t.raiseSyntax("expected enum entry name") - } - } while (true) + else -> t.raiseSyntax("expected enum entry name") + } + } while (true) + } else { + cc.nextNonWhitespace() + } miniSink?.onEnumDecl( MiniEnumDecl( @@ -1848,7 +1958,9 @@ class Compiler( name = nameToken.value, entries = names, doc = doc, - nameStart = nameToken.pos + nameStart = nameToken.pos, + isExtern = isExtern, + entryPositions = positions ) ) @@ -1859,9 +1971,13 @@ class Compiler( } } - private suspend fun parseObjectDeclaration(): Statement { - val nameToken = cc.requireToken(Token.Type.ID) - val startPos = pendingDeclStart ?: nameToken.pos + private suspend fun parseObjectDeclaration(isExtern: Boolean = false): Statement { + val next = cc.peekNextNonWhitespace() + val nameToken = if (next.type == Token.Type.ID) cc.requireToken(Token.Type.ID) else null + + val startPos = pendingDeclStart ?: nameToken?.pos ?: cc.current().pos + val className = nameToken?.value ?: generateAnonName(startPos) + val doc = pendingDeclDoc ?: consumePendingDoc() pendingDeclDoc = null pendingDeclStart = null @@ -1887,34 +2003,64 @@ class Compiler( // Robust body detection var classBodyRange: MiniRange? = null - val bodyInit: Statement? = run { + val bodyInit: Statement? = inCodeContext(CodeContext.ClassBody(className, isExtern = isExtern)) { val saved = cc.savePos() - val next = cc.nextNonWhitespace() - if (next.type == Token.Type.LBRACE) { - val bodyStart = next.pos + val nextBody = cc.nextNonWhitespace() + if (nextBody.type == Token.Type.LBRACE) { + // Emit MiniClassDecl before body parsing to track members via enter/exit + run { + val node = MiniClassDecl( + range = MiniRange(startPos, cc.currentPos()), + name = className, + bases = baseSpecs.map { it.name }, + bodyRange = null, + doc = doc, + nameStart = nameToken?.pos ?: startPos, + isObject = true, + isExtern = isExtern + ) + miniSink?.onEnterClass(node) + } + val bodyStart = nextBody.pos val st = withLocalNames(emptySet()) { parseScript() } val rbTok = cc.next() if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in object body") classBodyRange = MiniRange(bodyStart, rbTok.pos) + miniSink?.onExitClass(rbTok.pos) st } else { + // No body, but still emit the class + run { + val node = MiniClassDecl( + range = MiniRange(startPos, cc.currentPos()), + name = className, + bases = baseSpecs.map { it.name }, + bodyRange = null, + doc = doc, + nameStart = nameToken?.pos ?: startPos, + isObject = true, + isExtern = isExtern + ) + miniSink?.onClassDecl(node) + } cc.restorePos(saved) null } } val initScope = popInitScope() - val className = nameToken.value return statement(startPos) { context -> val parentClasses = baseSpecs.map { baseSpec -> - val rec = context[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") - (rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class") + val rec = context[baseSpec.name] ?: throw ScriptError(startPos, "unknown base class: ${baseSpec.name}") + (rec.value as? ObjClass) ?: throw ScriptError(startPos, "${baseSpec.name} is not a class") } val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()) + newClass.isAnonymous = nameToken == null + newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN) for (i in parentClasses.indices) { val argsList = baseSpecs[i].args // In object, we evaluate parent args once at creation time @@ -1924,23 +2070,25 @@ class Compiler( val classScope = context.createChildScope(newThisObj = newClass) classScope.currentClassCtx = newClass newClass.classScope = classScope + classScope.addConst("object", newClass) bodyInit?.execute(classScope) // Create instance (singleton) val instance = newClass.callOn(context.createChildScope(Arguments.EMPTY)) - context.addItem(className, false, instance) + if (nameToken != null) + context.addItem(className, false, instance) instance } } - private suspend fun parseClassDeclaration(isAbstract: Boolean = false): Statement { + private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false): Statement { val nameToken = cc.requireToken(Token.Type.ID) val startPos = pendingDeclStart ?: nameToken.pos val doc = pendingDeclDoc ?: consumePendingDoc() pendingDeclDoc = null pendingDeclStart = null - return inCodeContext(CodeContext.ClassBody(nameToken.value)) { + return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) { val constructorArgsDeclaration = if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) parseArgsDeclaration(isClassDeclaration = true) @@ -1978,7 +2126,36 @@ class Compiler( val bodyInit: Statement? = run { val saved = cc.savePos() val next = cc.nextNonWhitespace() + + val ctorFields = mutableListOf() + constructorArgsDeclaration?.let { ad -> + for (p in ad.params) { + val at = p.accessType + val mutable = at == AccessType.Var + ctorFields += MiniCtorField( + name = p.name, + mutable = mutable, + type = p.miniType, + nameStart = p.pos + ) + } + } + if (next.type == Token.Type.LBRACE) { + // Emit MiniClassDecl before body parsing to track members via enter/exit + run { + val node = MiniClassDecl( + range = MiniRange(startPos, cc.currentPos()), + name = nameToken.value, + bases = baseSpecs.map { it.name }, + bodyRange = null, + ctorFields = ctorFields, + doc = doc, + nameStart = nameToken.pos, + isExtern = isExtern + ) + miniSink?.onEnterClass(node) + } // parse body val bodyStart = next.pos val st = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) { @@ -1987,46 +2164,29 @@ class Compiler( val rbTok = cc.next() if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body") classBodyRange = MiniRange(bodyStart, rbTok.pos) + miniSink?.onExitClass(rbTok.pos) st } else { + // No body, but still emit the class + run { + val node = MiniClassDecl( + range = MiniRange(startPos, cc.currentPos()), + name = nameToken.value, + bases = baseSpecs.map { it.name }, + bodyRange = null, + ctorFields = ctorFields, + doc = doc, + nameStart = nameToken.pos, + isExtern = isExtern + ) + miniSink?.onClassDecl(node) + } // restore if no body starts here cc.restorePos(saved) null } } - // Emit MiniClassDecl with collected base names; bodyRange is omitted for now - run { - val declRange = MiniRange(startPos, cc.currentPos()) - val bases = baseSpecs.map { it.name } - // Collect constructor fields declared as val/var in primary constructor - val ctorFields = mutableListOf() - constructorArgsDeclaration?.let { ad -> - for (p in ad.params) { - val at = p.accessType - if (at != null) { - val mutable = at == AccessType.Var - ctorFields += MiniCtorField( - name = p.name, - mutable = mutable, - type = p.miniType, - nameStart = p.pos - ) - } - } - } - val node = MiniClassDecl( - range = declRange, - name = nameToken.value, - bases = bases, - bodyRange = classBodyRange, - ctorFields = ctorFields, - doc = doc, - nameStart = nameToken.pos - ) - miniSink?.onClassDecl(node) - } - val initScope = popInitScope() // create class @@ -2080,6 +2240,7 @@ class Compiler( // but we should pass Pos.builtIn to skip validation for now if needed, // or p.pos to allow it. pos = Pos.builtIn, + isTransient = p.isTransient, type = ObjRecord.Type.ConstructorField ) } @@ -2098,20 +2259,6 @@ class Compiler( for (s in initScope) s.execute(classScope) } - // Fallback: ensure any functions declared in class scope are also present as instance methods - // (defensive in case some paths skipped cls.addFn during parsing/execution ordering) - for ((k, rec) in classScope.objects) { - val v = rec.value - if (v is Statement) { - if (newClass.members[k] == null) { - newClass.addFn(k, isMutable = true, pos = rec.importedFrom?.pos ?: nameToken.pos) { - (thisObj as? ObjInstance)?.let { i -> - v.execute(ClosureScope(this, i.instanceScope)) - } ?: v.execute(thisObj.autoInstanceScope(this)) - } - } - } - } newClass.checkAbstractSatisfaction(nameToken.pos) // Debug summary: list registered instance methods and class-scope functions for this class newClass @@ -2198,7 +2345,7 @@ class Compiler( } else if (sourceObj.isInstanceOf(ObjIterable)) { loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak) } else { - val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() } + val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() } .getOrElse { throw ScriptError( tOp.pos, @@ -2475,6 +2622,38 @@ class Compiler( } } + private suspend fun parseReturnStatement(start: Pos): Statement { + var t = cc.next() + + val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) { + cc.previous() + null + } else { + t.value + } + + // expression? + t = cc.next() + cc.previous() + val resultExpr = if (t.pos.line == start.line && (!t.isComment && + t.type != Token.Type.SEMICOLON && + t.type != Token.Type.NEWLINE && + t.type != Token.Type.RBRACE && + t.type != Token.Type.RPAREN && + t.type != Token.Type.RBRACKET && + t.type != Token.Type.COMMA && + t.type != Token.Type.EOF) + ) { + // we have something on this line, could be expression + parseExpression() + } else null + + return statement(start) { + val returnValue = resultExpr?.execute(it) ?: ObjVoid + throw ReturnException(returnValue, label) + } + } + private fun ensureRparen(): Pos { val t = cc.next() if (t.type != Token.Type.RPAREN) @@ -2531,7 +2710,10 @@ class Compiler( isOverride: Boolean = false, isExtern: Boolean = false, isStatic: Boolean = false, + isTransient: Boolean = isTransientFlag ): Statement { + isTransientFlag = false + val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true var t = cc.next() val start = t.pos var extTypeName: String? = null @@ -2588,9 +2770,9 @@ class Compiler( // Capture doc locally to reuse even if we need to emit later val declDocLocal = pendingDeclDoc + val outerLabel = lastLabel - // Emit MiniFunDecl before body parsing (body range unknown yet) - run { + val node = run { val params = argsDeclaration.params.map { p -> MiniParam( name = p.name, @@ -2607,20 +2789,26 @@ class Compiler( body = null, doc = declDocLocal, nameStart = nameStartPos, - receiver = receiverMini + receiver = receiverMini, + isExtern = actualExtern, + isStatic = isStatic ) miniSink?.onFunDecl(node) pendingDeclDoc = null + node } + miniSink?.onEnterFunction(node) return inCodeContext(CodeContext.Function(name)) { + cc.labels.add(name) + outerLabel?.let { cc.labels.add(it) } val paramNames: Set = argsDeclaration.params.map { it.name }.toSet() // Parse function body while tracking declared locals to compute precise capacity hints currentLocalDeclCount localDeclCountStack.add(0) - val fnStatements = if (isExtern) + val fnStatements = if (actualExtern) statement { raiseError("extern function not provided: $name") } else if (isAbstract || isDelegated) { null @@ -2629,7 +2817,9 @@ class Compiler( val next = cc.peekNextNonWhitespace() if (next.type == Token.Type.ASSIGN) { cc.nextNonWhitespace() // consume '=' - val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected function body expression") + if (cc.peekNextNonWhitespace().value == "return") + throw ScriptError(cc.currentPos(), "return is not allowed in shorthand function") + val expr = parseExpression() ?: throw ScriptError(cc.currentPos(), "Expected function body expression") // Shorthand function returns the expression value statement(expr.pos) { scope -> expr.execute(scope) @@ -2661,8 +2851,15 @@ class Compiler( if (extTypeName != null) { context.thisObj = callerContext.thisObj } - fnStatements?.execute(context) ?: ObjVoid + try { + fnStatements?.execute(context) ?: ObjVoid + } catch (e: ReturnException) { + if (e.label == null || e.label == name || e.label == outerLabel) e.result + else throw e + } } + cc.labels.remove(name) + outerLabel?.let { cc.labels.remove(it) } // parentContext val fnCreateStatement = statement(start) { context -> if (isDelegated) { @@ -2675,7 +2872,7 @@ class Compiler( } if (extTypeName != null) { - val type = context[extTypeName!!]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found") + val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found") if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance") context.addExtension(type, name, ObjRecord(ObjUnset, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Delegated).apply { delegate = finalDelegate @@ -2685,31 +2882,31 @@ class Compiler( val th = context.thisObj if (isStatic) { - (th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, type = ObjRecord.Type.Delegated).apply { + (th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, isTransient = isTransient, type = ObjRecord.Type.Delegated).apply { delegate = finalDelegate } - context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply { + context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply { delegate = finalDelegate } } else if (th is ObjClass) { val cls: ObjClass = th val storageName = "${cls.className}::$name" - cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, type = ObjRecord.Type.Delegated) + cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient, type = ObjRecord.Type.Delegated) cls.instanceInitializers += statement(start) { scp -> val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable") - val initValue2 = delegateExpression!!.execute(scp) + val initValue2 = delegateExpression.execute(scp) val finalDelegate2 = try { initValue2.invokeInstanceMethod(scp, "bind", Arguments(ObjString(name), accessType2, scp.thisObj)) } catch (e: Exception) { initValue2 } - scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride).apply { + scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient).apply { delegate = finalDelegate2 } ObjVoid } } else { - context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply { + context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply { delegate = finalDelegate } } @@ -2799,8 +2996,12 @@ class Compiler( returnType = returnTypeMini, body = bodyRange?.let { MiniBlock(it) }, doc = declDocLocal, - nameStart = nameStartPos + nameStart = nameStartPos, + receiver = receiverMini, + isExtern = actualExtern, + isStatic = isStatic ) + miniSink?.onExitFunction(cc.currentPos()) miniSink?.onFunDecl(node) } } @@ -2833,8 +3034,12 @@ class Compiler( isAbstract: Boolean = false, isClosed: Boolean = false, isOverride: Boolean = false, - isStatic: Boolean = false + isStatic: Boolean = false, + isExtern: Boolean = false, + isTransient: Boolean = isTransientFlag ): Statement { + isTransientFlag = false + val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true val nextToken = cc.next() val start = nextToken.pos @@ -2856,7 +3061,9 @@ class Compiler( type = null, initRange = null, doc = pendingDeclDoc, - nameStart = namePos + nameStart = namePos, + isExtern = actualExtern, + isStatic = false ) miniSink?.onValDecl(node) } @@ -2875,7 +3082,7 @@ class Compiler( return statement(start) { context -> val value = initialExpression.execute(context) for (name in names) { - context.addItem(name, true, ObjVoid, visibility) + context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient) } pattern.setAt(start, context, value) if (!isMutable) { @@ -2932,31 +3139,88 @@ class Compiler( val mark = cc.savePos() cc.restorePos(markBeforeEq) cc.skipWsTokens() - val next = cc.peekNextNonWhitespace() - if (next.isId("get") || next.isId("set") || next.isId("private") || next.isId("protected")) { + + // Heuristic: if we see 'get(' or 'set(' or 'private set(' or 'protected set(', + // look ahead for a body. + fun hasAccessorWithBody(): Boolean { + val t = cc.peekNextNonWhitespace() + if (t.isId("get") || t.isId("set")) { + val saved = cc.savePos() + cc.next() // consume get/set + val nextToken = cc.peekNextNonWhitespace() + if (nextToken.type == Token.Type.LPAREN) { + cc.next() // consume ( + var depth = 1 + while (cc.hasNext() && depth > 0) { + val tt = cc.next() + if (tt.type == Token.Type.LPAREN) depth++ + else if (tt.type == Token.Type.RPAREN) depth-- + } + val next = cc.peekNextNonWhitespace() + if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + } else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + cc.restorePos(saved) + } else if (t.isId("private") || t.isId("protected")) { + val saved = cc.savePos() + cc.next() // consume modifier + if (cc.skipWsTokens().isId("set")) { + cc.next() // consume set + val nextToken = cc.peekNextNonWhitespace() + if (nextToken.type == Token.Type.LPAREN) { + cc.next() // consume ( + var depth = 1 + while (cc.hasNext() && depth > 0) { + val tt = cc.next() + if (tt.type == Token.Type.LPAREN) depth++ + else if (tt.type == Token.Type.RPAREN) depth-- + } + val next = cc.peekNextNonWhitespace() + if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + } else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + } + cc.restorePos(saved) + } + return false + } + + if (hasAccessorWithBody()) { isProperty = true cc.restorePos(markBeforeEq) - cc.skipWsTokens() + // Do not consume eqToken if it's an accessor keyword } else { cc.restorePos(mark) } } + val effectiveEqToken = if (isProperty) null else eqToken + // Register the local name at compile time so that subsequent identifiers can be emitted as fast locals if (!isStatic) declareLocalName(name) - val isDelegate = if (isAbstract) { - if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.type == Token.Type.BY)) - throw ScriptError(eqToken.pos, "abstract variable $name cannot have an initializer or delegate") - // Abstract variables don't have initializers + val isDelegate = if (isAbstract || actualExtern) { + if (!isProperty && (effectiveEqToken?.type == Token.Type.ASSIGN || effectiveEqToken?.type == Token.Type.BY)) + throw ScriptError(effectiveEqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate") + // Abstract or extern variables don't have initializers cc.restorePos(markBeforeEq) cc.skipWsTokens() setNull = true false - } else if (!isProperty && eqToken.type == Token.Type.BY) { + } else if (!isProperty && effectiveEqToken?.type == Token.Type.BY) { true } else { - if (!isProperty && eqToken.type != Token.Type.ASSIGN) { + if (!isProperty && effectiveEqToken?.type != Token.Type.ASSIGN) { if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null)) throw ScriptError(start, "val must be initialized") else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) { @@ -2976,12 +3240,12 @@ class Compiler( val initialExpression = if (setNull || isProperty) null else parseStatement(true) - ?: throw ScriptError(eqToken.pos, "Expected initializer expression") + ?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression") // Emit MiniValDecl for this declaration (before execution wiring), attach doc if any run { val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos()) - val initR = if (setNull || isProperty) null else MiniRange(eqToken.pos, cc.currentPos()) + val initR = if (setNull || isProperty) null else MiniRange(effectiveEqToken!!.pos, cc.currentPos()) val node = MiniValDecl( range = declRange, name = name, @@ -2990,7 +3254,9 @@ class Compiler( initRange = initR, doc = pendingDeclDoc, nameStart = nameStartPos, - receiver = receiverMini + receiver = receiverMini, + isExtern = actualExtern, + isStatic = isStatic ) miniSink?.onValDecl(node) pendingDeclDoc = null @@ -3024,17 +3290,34 @@ class Compiler( visibility, null, start, + isTransient = isTransient, type = ObjRecord.Type.Delegated ).apply { delegate = finalDelegate } // Also expose in current init scope - scope.addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated).apply { + scope.addItem( + name, + isMutable, + ObjUnset, + visibility, + null, + ObjRecord.Type.Delegated, + isTransient = isTransient + ).apply { delegate = finalDelegate } } else { - (scope.thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start) - scope.addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field) + (scope.thisObj as ObjClass).createClassField( + name, + initValue, + isMutable, + visibility, + null, + start, + isTransient = isTransient + ) + scope.addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient) } return ObjVoid } @@ -3050,52 +3333,73 @@ class Compiler( while (true) { val t = cc.skipWsTokens() if (t.isId("get")) { + val getStart = cc.currentPos() cc.next() // consume 'get' - cc.requireToken(Token.Type.LPAREN) - cc.requireToken(Token.Type.RPAREN) + if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { + cc.next() // consume ( + cc.requireToken(Token.Type.RPAREN) + } + miniSink?.onEnterFunction(null) getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { cc.skipWsTokens() - parseBlock() + inCodeContext(CodeContext.Function("")) { + parseBlock() + } } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { cc.skipWsTokens() cc.next() // consume '=' - val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected getter expression") - expr + inCodeContext(CodeContext.Function("")) { + val expr = parseExpression() + ?: throw ScriptError(cc.current().pos, "Expected getter expression") + expr + } } else { throw ScriptError(cc.current().pos, "Expected { or = after get()") } + miniSink?.onExitFunction(cc.currentPos()) } else if (t.isId("set")) { + val setStart = cc.currentPos() cc.next() // consume 'set' - cc.requireToken(Token.Type.LPAREN) - val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") - cc.requireToken(Token.Type.RPAREN) + var setArgName = "it" + if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { + cc.next() // consume ( + setArgName = cc.requireToken(Token.Type.ID, "Expected setter argument name").value + cc.requireToken(Token.Type.RPAREN) + } + miniSink?.onEnterFunction(null) setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { cc.skipWsTokens() - val body = parseBlock() + val body = inCodeContext(CodeContext.Function("")) { + parseBlock() + } object : Statement() { override val pos: Pos = body.pos override suspend fun execute(scope: Scope): Obj { val value = scope.args.list.firstOrNull() ?: ObjNull - scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) + scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument) return body.execute(scope) } } } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { cc.skipWsTokens() cc.next() // consume '=' - val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected setter expression") + val expr = inCodeContext(CodeContext.Function("")) { + parseExpression() + ?: throw ScriptError(cc.current().pos, "Expected setter expression") + } val st = expr object : Statement() { override val pos: Pos = st.pos override suspend fun execute(scope: Scope): Obj { val value = scope.args.list.firstOrNull() ?: ObjNull - scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) + scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument) return st.execute(scope) } } } else { throw ScriptError(cc.current().pos, "Expected { or = after set(...)") } + miniSink?.onExitFunction(cc.currentPos()) } else if (t.isId("private") || t.isId("protected")) { val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected val mark = cc.savePos() @@ -3107,9 +3411,12 @@ class Compiler( cc.next() // consume '(' val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") cc.requireToken(Token.Type.RPAREN) - setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { + miniSink?.onEnterFunction(null) + val finalSetter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { cc.skipWsTokens() - val body = parseBlock() + val body = inCodeContext(CodeContext.Function("")) { + parseBlock() + } object : Statement() { override val pos: Pos = body.pos override suspend fun execute(scope: Scope): Obj { @@ -3121,11 +3428,12 @@ class Compiler( } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { cc.skipWsTokens() cc.next() // consume '=' - val expr = parseExpression() ?: throw ScriptError( - cc.current().pos, - "Expected setter expression" - ) - val st = expr + val st = inCodeContext(CodeContext.Function("")) { + parseExpression() ?: throw ScriptError( + cc.current().pos, + "Expected setter expression" + ) + } object : Statement() { override val pos: Pos = st.pos override suspend fun execute(scope: Scope): Obj { @@ -3137,6 +3445,10 @@ class Compiler( } else { throw ScriptError(cc.current().pos, "Expected { or = after set(...)") } + setter = finalSetter + miniSink?.onExitFunction(cc.currentPos()) + } else { + // private set without body: setter remains null, visibility is restricted } } else { cc.restorePos(mark) @@ -3160,23 +3472,14 @@ class Compiler( } } - return object : Statement() { - override val pos: Pos = start - override suspend fun execute(context: Scope): Obj { + return statement(start) { context -> if (extTypeName != null) { val prop = if (getter != null || setter != null) { ObjProperty(name, getter, setter) } else { // Simple val extension with initializer val initExpr = initialExpression ?: throw ScriptError(start, "Extension val must be initialized") - ObjProperty( - name, - object : Statement() { - override val pos: Pos = initExpr.pos - override suspend fun execute(scp: Scope): Obj = initExpr.execute(scp) - }, - null - ) + ObjProperty(name, statement(initExpr.pos) { scp -> initExpr.execute(scp) }, null) } val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found") @@ -3184,11 +3487,12 @@ class Compiler( context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, writeVisibility = setterVisibility, declaringClass = null, type = ObjRecord.Type.Property)) - return prop + return@statement prop } // In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions // Do NOT infer declaring class from runtime thisObj here; only the compile-time captured // ClassBody qualifies for class-field storage. Otherwise, this is a plain local. + isProperty = getter != null || setter != null val declaringClassName = declaringClassNameCaptured if (declaringClassName == null) { if (context.containsLocal(name)) @@ -3212,39 +3516,34 @@ class Compiler( visibility, setterVisibility, start, + isTransient = isTransient, type = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride ) - cls.instanceInitializers += object : Statement() { - override val pos: Pos = start - override suspend fun execute(scp: Scope): Obj { - val initValue = initialExpression!!.execute(scp) - val accessTypeStr = if (isMutable) "Var" else "Val" - val accessType = scp.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr") - val finalDelegate = try { - initValue.invokeInstanceMethod( - scp, - "bind", - Arguments(ObjString(name), accessType, scp.thisObj) - ) - } catch (e: Exception) { - initValue - } - scp.addItem( - storageName, isMutable, ObjUnset, visibility, setterVisibility, - recordType = ObjRecord.Type.Delegated, - isAbstract = isAbstract, - isClosed = isClosed, - isOverride = isOverride - ).apply { - delegate = finalDelegate - } - return ObjVoid + cls.instanceInitializers += statement(start) { scp -> + val initValue = initialExpression!!.execute(scp) + val accessTypeStr = if (isMutable) "Var" else "Val" + val accessType = scp.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr") + val finalDelegate = try { + initValue.invokeInstanceMethod(scp, "bind", Arguments(ObjString(name), accessType, scp.thisObj)) + } catch (e: Exception) { + initValue } + scp.addItem( + storageName, isMutable, ObjUnset, visibility, setterVisibility, + recordType = ObjRecord.Type.Delegated, + isAbstract = isAbstract, + isClosed = isClosed, + isOverride = isOverride, + isTransient = isTransient + ).apply { + delegate = finalDelegate + } + ObjVoid } - return ObjVoid + return@statement ObjVoid } else { val initValue = initialExpression!!.execute(context) val accessTypeStr = if (isMutable) "Var" else "Val" @@ -3259,10 +3558,11 @@ class Compiler( recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, - isOverride = isOverride + isOverride = isOverride, + isTransient = isTransient ) rec.delegate = finalDelegate - return finalDelegate + return@statement finalDelegate } } else { val initValue = initialExpression!!.execute(context) @@ -3278,10 +3578,11 @@ class Compiler( recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, - isOverride = isOverride + isOverride = isOverride, + isTransient = isTransient ) rec.delegate = finalDelegate - return finalDelegate + return@statement finalDelegate } } else if (getter != null || setter != null) { val declaringClassName = declaringClassNameCaptured!! @@ -3290,7 +3591,7 @@ class Compiler( // If we are in class scope now (defining instance field), defer initialization to instance time val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance) - return if (isClassScope) { + if (isClassScope) { val cls = context.thisObj as ObjClass // Register in class members for reflection/MRO/satisfaction checks if (isProperty) { @@ -3301,7 +3602,8 @@ class Compiler( isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, - pos = start + pos = start, + prop = prop ) } else { cls.createField( @@ -3313,28 +3615,26 @@ class Compiler( isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, + isTransient = isTransient, type = ObjRecord.Type.Field ) } // Register the property/field initialization thunk if (!isAbstract) { - cls.instanceInitializers += object : Statement() { - override val pos: Pos = start - override suspend fun execute(scp: Scope): Obj { - scp.addItem( - storageName, - isMutable, - prop, - visibility, - setterVisibility, - recordType = ObjRecord.Type.Property, - isAbstract = isAbstract, - isClosed = isClosed, - isOverride = isOverride - ) - return ObjVoid - } + cls.instanceInitializers += statement(start) { scp -> + scp.addItem( + storageName, + isMutable, + prop, + visibility, + setterVisibility, + recordType = ObjRecord.Type.Property, + isAbstract = isAbstract, + isClosed = isClosed, + isOverride = isOverride + ) + ObjVoid } } ObjVoid @@ -3345,37 +3645,37 @@ class Compiler( recordType = ObjRecord.Type.Property, isAbstract = isAbstract, isClosed = isClosed, - isOverride = isOverride + isOverride = isOverride, + isTransient = isTransient ) prop } } else { - val isLateInitVal = !isMutable && initialExpression == null && getter == null && setter == null - return if (declaringClassName != null && !isStatic) { - val storageName = "$declaringClassName::$name" - // If we are in class scope now (defining instance field), defer initialization to instance time - val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance) - if (isClassScope) { - val cls = context.thisObj as ObjClass - // Register in class members for reflection/MRO/satisfaction checks - cls.createField( - name, - ObjNull, - isMutable = isMutable, - visibility = visibility, - writeVisibility = setterVisibility, - isAbstract = isAbstract, - isClosed = isClosed, - isOverride = isOverride, - pos = start, - type = ObjRecord.Type.Field - ) + val isLateInitVal = !isMutable && initialExpression == null + if (declaringClassName != null && !isStatic) { + val storageName = "$declaringClassName::$name" + // If we are in class scope now (defining instance field), defer initialization to instance time + val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance) + if (isClassScope) { + val cls = context.thisObj as ObjClass + // Register in class members for reflection/MRO/satisfaction checks + cls.createField( + name, + ObjNull, + isMutable = isMutable, + visibility = visibility, + writeVisibility = setterVisibility, + isAbstract = isAbstract, + isClosed = isClosed, + isOverride = isOverride, + pos = start, + isTransient = isTransient, + type = ObjRecord.Type.Field + ) - // Defer: at instance construction, evaluate initializer in instance scope and store under mangled name - if (!isAbstract) { - val initStmt = object : Statement() { - override val pos: Pos = start - override suspend fun execute(scp: Scope): Obj { + // Defer: at instance construction, evaluate initializer in instance scope and store under mangled name + if (!isAbstract) { + val initStmt = statement(start) { scp -> val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull @@ -3385,40 +3685,39 @@ class Compiler( recordType = ObjRecord.Type.Field, isAbstract = isAbstract, isClosed = isClosed, - isOverride = isOverride + isOverride = isOverride, + isTransient = isTransient ) - return ObjVoid + ObjVoid } + cls.instanceInitializers += initStmt } - cls.instanceInitializers += initStmt + ObjVoid + } else { + // We are in instance scope already: perform initialization immediately + val initValue = + initialExpression?.execute(context)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull + // Preserve mutability of declaration: create record with correct mutability + context.addItem( + storageName, isMutable, initValue, visibility, setterVisibility, + recordType = ObjRecord.Type.Field, + isAbstract = isAbstract, + isClosed = isClosed, + isOverride = isOverride, + isTransient = isTransient + ) + initValue } - ObjVoid } else { - // We are in instance scope already: perform initialization immediately - val initValue = - initialExpression?.execute(context)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull - // Preserve mutability of declaration: create record with correct mutability - context.addItem( - storageName, isMutable, initValue, visibility, setterVisibility, - recordType = ObjRecord.Type.Field, - isAbstract = isAbstract, - isClosed = isClosed, - isOverride = isOverride - ) + // Not in class body: regular local/var declaration + val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull + context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other, isTransient = isTransient) initValue } - } else { - // Not in class body: regular local/var declaration - val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull - context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field) - initValue - } } } } - } - data class Operator( val tokenType: Token.Type, val priority: Int, val arity: Int = 2, @@ -3571,6 +3870,9 @@ class Compiler( Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b -> AssignOpRef(BinOp.PERCENT, a, b, pos) }, + Operator(Token.Type.IFNULLASSIGN, lastPriority) { pos, a, b -> + AssignIfNullRef(a, b, pos) + }, // logical 1 Operator(Token.Type.OR, ++lastPriority) { _, a, b -> foldBinary(BinOp.OR, a, b)?.let { return@Operator ConstRef(it.asReadonly) } @@ -3725,3 +4027,5 @@ class Compiler( } suspend fun eval(code: String) = compile(code).execute() +suspend fun evalNamed(name: String, code: String, importManager: ImportManager = Script.defaultImportManager) = + compile(Source(name,code), importManager).execute() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index fa08938..4a1b0cc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -70,9 +70,8 @@ open class Scope( internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? { var s: Scope? = this - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break + var hops = 0 + while (s != null && hops++ < 1024) { // Proximity rule: check all extensions in the current scope before going to parent. // Priority within scope: more specific class in MRO wins. for (cls in receiverClass.mro) { @@ -106,28 +105,38 @@ open class Scope( * intertwined closure frames. They traverse the plain parent chain and consult only locals * and bindings of each frame. Instance/class member fallback must be decided by the caller. */ - private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { + internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { + caller?.let { ctx -> + s.objects[ctx.mangledName(name)]?.let { rec -> + if (rec.visibility == Visibility.Private) return rec + } + } s.objects[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec + } + caller?.let { ctx -> + s.localBindings[ctx.mangledName(name)]?.let { rec -> + if (rec.visibility == Visibility.Private) return rec + } } s.localBindings[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec } s.getSlotIndexOf(name)?.let { idx -> val rec = s.getSlotRecord(idx) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec } return null } - internal fun chainLookupIgnoreClosure(name: String): ObjRecord? { + internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? { var s: Scope? = this - // use frameId to detect unexpected structural cycles in the parent chain - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) return null - tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } - s = s.parent + // use hop counter to detect unexpected structural cycles in the parent chain + var hops = 0 + val effectiveCaller = caller ?: currentClassCtx + while (s != null && hops++ < 1024) { + tryGetLocalRecord(s, name, effectiveCaller)?.let { return it } + s = if (followClosure && s is ClosureScope) s.closureScope else s.parent } return null } @@ -144,9 +153,8 @@ open class Scope( tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } // 2) walk parents for plain locals/bindings only var s = parent - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) return null + var hops = 0 + while (s != null && hops++ < 1024) { tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } s = s.parent } @@ -155,7 +163,7 @@ open class Scope( this.extensions[cls]?.get(name)?.let { return it } } return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { + if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) { if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null else rec } else null @@ -169,23 +177,22 @@ open class Scope( * This completely avoids invoking overridden `get` implementations, preventing * ping-pong recursion between `ClosureScope` frames. */ - internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx): ObjRecord? { + internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? { var s: Scope? = this - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) return null + var hops = 0 + while (s != null && hops++ < 1024) { tryGetLocalRecord(s, name, caller)?.let { return it } for (cls in s.thisObj.objClass.mro) { s.extensions[cls]?.get(name)?.let { return it } } s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, caller)) { + if (canAccessMember(rec.visibility, rec.declaringClass, caller, name)) { if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) { // ignore fields, properties and abstracts here, they will be handled by the caller via readField } else return rec } } - s = s.parent + s = if (followClosure && s is ClosureScope) s.closureScope else s.parent } return null } @@ -277,16 +284,22 @@ open class Scope( raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name")) fun raiseError(message: String): Nothing { - throw ExecutionError(ObjException(this, message)) + val ex = ObjException(this, message) + throw ExecutionError(ex, pos, ex.message.value) } fun raiseError(obj: ObjException): Nothing { - throw ExecutionError(obj) + throw ExecutionError(obj, obj.scope.pos, obj.message.value) + } + + fun raiseError(obj: Obj, pos: Pos, message: String): Nothing { + throw ExecutionError(obj, pos, message) } @Suppress("unused") fun raiseNotFound(message: String = "not found"): Nothing { - throw ExecutionError(ObjNotFoundException(this, message)) + val ex = ObjNotFoundException(this, message) + throw ExecutionError(ex, ex.scope.pos, ex.message.value) } inline fun requiredArg(index: Int): T { @@ -314,39 +327,53 @@ open class Scope( inline fun thisAs(): T { var s: Scope? = this - do { - val t = s!!.thisObj + while (s != null) { + val t = s.thisObj if (t is T) return t s = s.parent - } while (s != null) + } raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") } internal val objects = mutableMapOf() - open operator fun get(name: String): ObjRecord? = - if (name == "this") thisObj.asReadonly - else { - // Prefer direct locals/bindings declared in this frame - (objects[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null - } - // Then, check known local bindings in this frame (helps after suspension) - ?: localBindings[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null - } - // Walk up ancestry - ?: parent?.get(name) - // Finally, fallback to class members on thisObj - ?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { - if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null - else rec - } else null - } - ) + open operator fun get(name: String): ObjRecord? { + if (name == "this") return thisObj.asReadonly + + // 1. Prefer direct locals/bindings declared in this frame + tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } + + val p = parent + + // 2. If we share thisObj with parent, delegate to parent to maintain + // "locals shadow members" priority across the this-context. + if (p != null && p.thisObj === thisObj) { + return p.get(name) } + // 3. Otherwise, we are the "primary" scope for this thisObj (or have no parent), + // so check members of thisObj before walking up to a different this-context. + val receiver = thisObj + val effectiveClass = receiver as? ObjClass ?: receiver.objClass + for (cls in effectiveClass.mro) { + val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) + if (rec != null && !rec.isAbstract) { + if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) { + return rec.copy(receiver = receiver) + } + } + } + // Finally, root object fallback + Obj.rootObjectType.members[name]?.let { rec -> + if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) { + return rec.copy(receiver = receiver) + } + } + + // 4. Finally, walk up ancestry to a scope with a different thisObj context + return p?.get(name) + } + // Slot fast-path API fun getSlotRecord(index: Int): ObjRecord = slots[index] fun setSlotValue(index: Int, newValue: Obj) { @@ -365,6 +392,20 @@ open class Scope( nameToSlot[name]?.let { slots[it] = record } } + /** + * Clear all references and maps to prevent memory leaks when pooled. + */ + fun scrub() { + this.parent = null + this.skipScopeCreation = false + this.currentClassCtx = null + objects.clear() + slots.clear() + nameToSlot.clear() + localBindings.clear() + extensions.clear() + } + /** * Reset this scope instance so it can be safely reused as a fresh child frame. * Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj. @@ -374,6 +415,7 @@ open class Scope( // that could interact badly with the new parent and produce a cycle. this.parent = null this.skipScopeCreation = false + this.currentClassCtx = parent?.currentClassCtx // fresh identity for PIC caches this.frameId = nextFrameId() // clear locals and slot maps @@ -474,7 +516,8 @@ open class Scope( declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, isAbstract: Boolean = false, isClosed: Boolean = false, - isOverride: Boolean = false + isOverride: Boolean = false, + isTransient: Boolean = false ): ObjRecord { val rec = ObjRecord( value, isMutable, visibility, writeVisibility, @@ -482,7 +525,8 @@ open class Scope( type = recordType, isAbstract = isAbstract, isClosed = isClosed, - isOverride = isOverride + isOverride = isOverride, + isTransient = isTransient ) objects[name] = rec // Index this binding within the current frame to help resolve locals across suspension @@ -501,11 +545,15 @@ open class Scope( } } // Map to a slot for fast local access (ensure consistency) - val idx = getSlotIndexOf(name) - if (idx == null) { + if (nameToSlot.isEmpty()) { allocateSlotFor(name, rec) } else { - slots[idx] = rec + val idx = nameToSlot[name] + if (idx == null) { + allocateSlotFor(name, rec) + } else { + slots[idx] = rec + } } return rec } @@ -622,31 +670,34 @@ open class Scope( } suspend fun resolve(rec: ObjRecord, name: String): Obj { - if (rec.type == ObjRecord.Type.Delegated) { - val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate") - val th = if (thisObj === ObjVoid) ObjNull else thisObj - if (del.objClass.getInstanceMemberOrNull("getValue") == null) { - return object : Statement() { - override val pos: Pos = Pos.builtIn - override suspend fun execute(scope: Scope): Obj { - val th2 = if (scope.thisObj === ObjVoid) ObjNull else scope.thisObj - val allArgs = (listOf(th2, ObjString(name)) + scope.args.list).toTypedArray() - return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs)) - } - } - } - return del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name))) - } - return rec.value + val receiver = rec.receiver ?: thisObj + return receiver.resolveRecord(this, rec, name, rec.declaringClass).value } suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) { if (rec.type == ObjRecord.Type.Delegated) { - val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate") - val th = if (thisObj === ObjVoid) ObjNull else thisObj + val receiver = rec.receiver ?: thisObj + val del = rec.delegate ?: run { + if (receiver is ObjInstance) { + (receiver as ObjInstance).writeField(this, name, newValue) + return + } + raiseError("Internal error: delegated property $name has no delegate") + } + val th = if (receiver === ObjVoid) ObjNull else receiver del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue)) return } + if (rec.value is ObjProperty) { + (rec.value as ObjProperty).callSetter(this, rec.receiver ?: thisObj, newValue, rec.declaringClass) + return + } + // If it's a member (explicitly tracked by receiver or declaringClass), use writeField. + // Important: locals have receiver == null and declaringClass == null (enforced in addItem). + if (rec.receiver != null || (rec.declaringClass != null && (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property))) { + (rec.receiver ?: thisObj).writeField(this, name, newValue) + return + } if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name") rec.value = newValue } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index da0bd34..27fd9d7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -609,17 +609,16 @@ open class Obj { scope.raiseNotImplemented() } - suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj { - if (PerfFlags.SCOPE_POOL) { - return scope.withChildFrame(args, newThisObj = thisObj) { child -> + suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj = + if (PerfFlags.SCOPE_POOL) + scope.withChildFrame(args, newThisObj = thisObj) { child -> if (declaringClass != null) child.currentClassCtx = declaringClass callOn(child) } - } - val child = scope.createChildScope(scope.pos, args = args, newThisObj = thisObj) - if (declaringClass != null) child.currentClassCtx = declaringClass - return callOn(child) - } + else + callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also { + if (declaringClass != null) it.currentClassCtx = declaringClass + }) suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj = callOn(