From 20c81dbf2e67434d5b5ce9be525004baee2d5c28 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 10 Jun 2025 12:05:51 +0400 Subject: [PATCH] fix #11 and private visibility for constructor params, fields and methods. --- .gitignore | 1 + docs/OOP.md | 117 +++++++++++++++++- .../net/sergeych/lyng/ArgsDeclaration.kt | 6 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 86 +++++-------- .../net/sergeych/lyng/CompilerContext.kt | 38 +++++- .../kotlin/net/sergeych/lyng/Context.kt | 12 +- .../kotlin/net/sergeych/lyng/Parser.kt | 2 + .../kotlin/net/sergeych/lyng/Token.kt | 2 + library/src/commonTest/kotlin/ScriptTest.kt | 11 ++ 9 files changed, 212 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index ef53dfe..9359b89 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ xcuserdata *.gpg .gigaide /kotlin-js-store/yarn.lock +/test.lyng diff --git a/docs/OOP.md b/docs/OOP.md index 805f266..fb5ff26 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -1,12 +1,19 @@ # OO implementation in Lyng -Short introduction +## Declaration + +The class clause looks like + + class Point(x,y) + assert( Point is Class ) + >>> void + +It creates new `Class` with two fields. Here is the more practical sample: class Point(x,y) { fun length() { sqrt(x*x + y*y) } } - assert( Point is Class ) val p = Point(3,4) assert(p is Point) assertEquals(5, p.length()) @@ -32,7 +39,111 @@ Form now on `Point` is a class, it's type is `Class`, and we can create instance example above. Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to -calculate the magnitude. +calculate the magnitude. Length is called + +### default values in constructor + +Constructor arguments are the same as function arguments except visibility +statements discussed later, there could be default values, ellipsis, etc. + + class Point(x=0,y=0) + val p = Point() + assert( p.x == 0 && p.y == 0 ) + >>> void + +## Methods + +Functions defined inside a class body are methods, and unless declared +`private` are available to be called from outside the class: + + class Point(x,y) { + // public method declaration: + fun length() { sqrt(d2()) } + + // private method: + private fun d2() {x*x + y*y} + } + val p = Point(3,4) + // private called from inside public: OK + assertEquals( 5, p.length() ) + // but us not available directly + assertThrows() { p.d2() } + void + >>> void + +## fields and visibility + +It is possible to add non-constructor fields: + + class Point(x,y) { + fun length() { sqrt(x*x + y*y) } + + // set at construction time: + val initialLength = length() + } + val p = Point(3,4) + p.x = 3 + p.y = 0 + assertEquals( 3, p.length() ) + // but initial length could not be changed after as declard val: + assert( p.initialLength == 5 ) + >>> void + +### Mutable fields + +Are declared with var + + class Point(x,y) { + var isSpecial = false + } + val p = Point(0,0) + assert( p.isSpecial == false ) + + p.isSpecial = true + assert( p.isSpecial == true ) + >>> void + +### Private fields + +Private fields are visible only _inside the class instance_: + + class SecretCounter { + private var count = 0 + + fun increment() { + count++ + void // hide counter + } + + fun isEnough() { + count > 10 + } + } + val c = SecretCounter() + assert( c.isEnough() == false ) + assert( c.increment() == void ) + for( i in 0..10 ) c.increment() + assert( c.isEnough() ) + + // but the count is not available outside: + assertThrows() { c.count } + void + >>> void + +It is possible to provide private constructor parameters so they can be +set at construction but not available outside the class: + + class SecretCounter(private var count = 0) { + // ... + } + val c = SecretCounter(10) + assertThrows() { c.count } + void + >>> void + + + +# Theory ## Basic principles: diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index 0e585af..1bdea48 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -23,10 +23,12 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) suspend fun assignToContext( context: Context, fromArgs: Arguments = context.args, - defaultAccessType: Compiler.AccessType = Compiler.AccessType.Var + defaultAccessType: Compiler.AccessType = Compiler.AccessType.Var, + defaultVisibility: Compiler.Visibility = Compiler.Visibility.Public ) { fun assign(a: Item, value: Obj) { - context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value) + context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value, + a.visibility ?: defaultVisibility) } suspend fun processHead(index: Int): Int { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 941ad37..aa317bc 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -37,6 +37,13 @@ class Compiler( } } + Token.Type.PRIVATE, Token.Type.PROTECTED -> { + if(cc.nextIdValue() in setOf("var", "val", "class", "fun", "fn")) { + continue + } else + throw ScriptError(t.pos, "unexpected keyword ${t.value}") + } + Token.Type.PLUS2, Token.Type.MINUS2 -> { cc.previous() parseExpression(cc) @@ -408,7 +415,7 @@ class Compiler( } enum class Visibility { - Public, Private, Protected, Internal + Public, Private, Protected//, Internal } /** @@ -429,43 +436,16 @@ class Compiler( } Token.Type.NEWLINE -> {} + Token.Type.PROTECTED, Token.Type.PRIVATE -> { + if (!isClassDeclaration) { + cc.restorePos(startPos); return null + } + } Token.Type.ID -> { // visibility - val visibility = when (t.value) { - "private" -> { - if (!isClassDeclaration) { - cc.restorePos(startPos); return null - } - t = cc.next() - Visibility.Private - } - - "protected" -> { - if (!isClassDeclaration) { - cc.restorePos(startPos); return null - } - t = cc.next() - Visibility.Protected - } - - "internal" -> { - if (!isClassDeclaration) { - cc.restorePos(startPos); return null - } - t = cc.next() - Visibility.Internal - } - - "public" -> { - if (!isClassDeclaration) { - cc.restorePos(startPos); return null - } - t = cc.next() - Visibility.Public - } - - else -> null - } + val visibility = if( isClassDeclaration ) + cc.getVisibility(Visibility.Public) + else Visibility.Public // val/var? val access = when (t.value) { "val" -> { @@ -703,7 +683,7 @@ class Compiler( val nameToken = cc.requireToken(Token.Type.ID) val constructorArgsDeclaration = if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) - parseArgsDeclaration(cc) + parseArgsDeclaration(cc, isClassDeclaration = true) else null if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN) @@ -752,7 +732,9 @@ class Compiler( } Visibility.Protected -> thisObj.protectedFields += name - else -> { + + Visibility.Private -> { + //println("private field: $name") } } } @@ -762,20 +744,6 @@ class Compiler( // inheritance must alter this code: val newClass = ObjClass(className).apply { instanceConstructor = constructorCode - constructorArgsDeclaration?.let { cad -> - // we need accessors for all fields: - for (f in cad.params) { - createField( - f.name, - statement { - val context = (thisObj as ObjInstance).instanceContext - context[f.name]?.value ?: raiseError("field is not initialized: ${f.name}") - }, - true, -// (f.accessType ?: defaultAccess).isMutable, - ) - } - } } return statement { @@ -1100,6 +1068,8 @@ class Compiler( } private fun parseFunctionDeclaration(tokens: CompilerContext): Statement { + val visibility = tokens.getVisibility() + var t = tokens.next() val start = t.pos val name = if (t.type != Token.Type.ID) @@ -1136,7 +1106,7 @@ class Compiler( // we added fn in the context. now we must save closure // for the function closure = context - context.addItem(name, false, fnBody) + context.addItem(name, false, fnBody, visibility) // as the function can be called from anywhere, we have // saved the proper context in the closure fnBody @@ -1162,6 +1132,14 @@ class Compiler( } private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement { + // we are just after var/val, visibility if exists is 2 steps behind + val visibility = when( tokens.atOffset(-2)?.type ) { + Token.Type.PRIVATE -> + Visibility.Private + Token.Type.PROTECTED -> Visibility.Protected + else -> Visibility.Public + } + val nameToken = tokens.next() val start = nameToken.pos if (nameToken.type != Token.Type.ID) @@ -1190,7 +1168,7 @@ class Compiler( // create a separate copy: val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull - context.addItem(name, mutable, initValue) + context.addItem(name, mutable, initValue, visibility) ObjVoid } } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index 32ac545..1ac8b38 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -9,7 +9,7 @@ internal class CompilerContext(val tokens: List) { var loopLevel = 0 private set - inline fun parseLoop(f: () -> T): Pair { + inline fun parseLoop(f: () -> T): Pair { if (++loopLevel == 0) breakFound = false val result = f() return Pair(breakFound, result).also { @@ -98,4 +98,40 @@ internal class CompilerContext(val tokens: List) { breakFound = true } + /** + * Return value of the next token if it is an identifier, null otherwise. + * Does not change position. + */ + fun nextIdValue(): String? { + return if (hasNext()) { + val nt = tokens[currentIndex] + if (nt.type == Token.Type.ID) + nt.value + else null + } else null + } + + @Suppress("unused") + fun current(): Token = tokens[currentIndex] + + /** + * If the token at current position plus offset (could be negative) exists, returns it, otherwise returns null. + */ + fun atOffset(offset: Int): Token? = + if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null + + /** + * Scan backwards as deep as specified looking for visibility token. Does not change position. + */ + fun getVisibility(default: Compiler.Visibility = Compiler.Visibility.Public, depths: Int = 2): Compiler.Visibility { + for( i in -depths .. -1) { + when( atOffset(i)?.type) { + Token.Type.PROTECTED -> return Compiler.Visibility.Protected + Token.Type.PRIVATE -> return Compiler.Visibility.Private + else -> {} + } + } + return default + } + } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index 392096c..1880e26 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -28,7 +28,8 @@ class Context( fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg)) @Suppress("unused") - fun raiseSymbolNotFound(name: String): Nothing = raiseError(ObjSymbolNotDefinedError(this, "symbol is not defined: $name")) + fun raiseSymbolNotFound(name: String): Nothing = + raiseError(ObjSymbolNotDefinedError(this, "symbol is not defined: $name")) fun raiseError(message: String): Nothing { throw ExecutionError(ObjError(this, message)) @@ -73,8 +74,13 @@ class Context( fun copy() = Context(this, args, pos, thisObj) - fun addItem(name: String, isMutable: Boolean, value: Obj): ObjRecord { - return ObjRecord(value, isMutable).also { objects.put(name, it) } + fun addItem( + name: String, + isMutable: Boolean, + value: Obj, + visibility: Compiler.Visibility = Compiler.Visibility.Public + ): ObjRecord { + return ObjRecord(value, isMutable, visibility).also { objects.put(name, it) } } fun getOrCreateNamespace(name: String): ObjClass { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index 3335a28..05dc2f1 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -280,6 +280,8 @@ private class Parser(fromPos: Pos) { when (text) { "in" -> Token("in", from, Token.Type.IN) "is" -> Token("is", from, Token.Type.IS) + "protected" -> Token("protected", from, Token.Type.PROTECTED) + "private" -> Token("private", from, Token.Type.PRIVATE) else -> Token(text, from, Token.Type.ID) } } else diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Token.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Token.kt index 75fa942..e6e8831 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Token.kt @@ -17,6 +17,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) { AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON, SINLGE_LINE_COMMENT, MULTILINE_COMMENT, LABEL, ATLABEL, // label@ at@label + PRIVATE, PROTECTED, + //PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS, ELLIPSIS, DOTDOT, DOTDOTLT, NEWLINE, EOF, diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 689a3ac..20fdc15 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -1453,4 +1453,15 @@ class ScriptTest { assertEquals(sqrt(109), p.length()) """.trimIndent()) } + + @Test + fun testPrivateConstructorParams() = runTest { + val c = Context() + c.eval(""" + class Point(private var x,y) + val p = Point(1,2) + p.y = 101 + assertThrows() { p.x = 10 } + """) + } } \ No newline at end of file