fix #11 and private visibility for constructor params, fields and methods.

This commit is contained in:
Sergey Chernov 2025-06-10 12:05:51 +04:00
parent fffa3d31bb
commit 20c81dbf2e
9 changed files with 212 additions and 63 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ xcuserdata
*.gpg *.gpg
.gigaide .gigaide
/kotlin-js-store/yarn.lock /kotlin-js-store/yarn.lock
/test.lyng

View File

@ -1,12 +1,19 @@
# OO implementation in Lyng # 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) { class Point(x,y) {
fun length() { sqrt(x*x + y*y) } fun length() { sqrt(x*x + y*y) }
} }
assert( Point is Class )
val p = Point(3,4) val p = Point(3,4)
assert(p is Point) assert(p is Point)
assertEquals(5, p.length()) 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. example above.
Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to 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: ## Basic principles:

View File

@ -23,10 +23,12 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
suspend fun assignToContext( suspend fun assignToContext(
context: Context, context: Context,
fromArgs: Arguments = context.args, 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) { 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 { suspend fun processHead(index: Int): Int {

View File

@ -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 -> { Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous() cc.previous()
parseExpression(cc) parseExpression(cc)
@ -408,7 +415,7 @@ class Compiler(
} }
enum class Visibility { enum class Visibility {
Public, Private, Protected, Internal Public, Private, Protected//, Internal
} }
/** /**
@ -429,43 +436,16 @@ class Compiler(
} }
Token.Type.NEWLINE -> {} Token.Type.NEWLINE -> {}
Token.Type.PROTECTED, Token.Type.PRIVATE -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
}
Token.Type.ID -> { Token.Type.ID -> {
// visibility // visibility
val visibility = when (t.value) { val visibility = if( isClassDeclaration )
"private" -> { cc.getVisibility(Visibility.Public)
if (!isClassDeclaration) { else Visibility.Public
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/var? // val/var?
val access = when (t.value) { val access = when (t.value) {
"val" -> { "val" -> {
@ -703,7 +683,7 @@ class Compiler(
val nameToken = cc.requireToken(Token.Type.ID) val nameToken = cc.requireToken(Token.Type.ID)
val constructorArgsDeclaration = val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(cc) parseArgsDeclaration(cc, isClassDeclaration = true)
else null else null
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN) if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
@ -752,7 +732,9 @@ class Compiler(
} }
Visibility.Protected -> Visibility.Protected ->
thisObj.protectedFields += name thisObj.protectedFields += name
else -> {
Visibility.Private -> {
//println("private field: $name")
} }
} }
} }
@ -762,20 +744,6 @@ class Compiler(
// inheritance must alter this code: // inheritance must alter this code:
val newClass = ObjClass(className).apply { val newClass = ObjClass(className).apply {
instanceConstructor = constructorCode 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 { return statement {
@ -1100,6 +1068,8 @@ class Compiler(
} }
private fun parseFunctionDeclaration(tokens: CompilerContext): Statement { private fun parseFunctionDeclaration(tokens: CompilerContext): Statement {
val visibility = tokens.getVisibility()
var t = tokens.next() var t = tokens.next()
val start = t.pos val start = t.pos
val name = if (t.type != Token.Type.ID) 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 // we added fn in the context. now we must save closure
// for the function // for the function
closure = context closure = context
context.addItem(name, false, fnBody) context.addItem(name, false, fnBody, visibility)
// as the function can be called from anywhere, we have // as the function can be called from anywhere, we have
// saved the proper context in the closure // saved the proper context in the closure
fnBody fnBody
@ -1162,6 +1132,14 @@ class Compiler(
} }
private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: CompilerContext): Statement { 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 nameToken = tokens.next()
val start = nameToken.pos val start = nameToken.pos
if (nameToken.type != Token.Type.ID) if (nameToken.type != Token.Type.ID)
@ -1190,7 +1168,7 @@ class Compiler(
// create a separate copy: // create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, mutable, initValue) context.addItem(name, mutable, initValue, visibility)
ObjVoid ObjVoid
} }
} }

View File

@ -9,7 +9,7 @@ internal class CompilerContext(val tokens: List<Token>) {
var loopLevel = 0 var loopLevel = 0
private set private set
inline fun <T> parseLoop(f: () -> T): Pair<Boolean,T> { inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
if (++loopLevel == 0) breakFound = false if (++loopLevel == 0) breakFound = false
val result = f() val result = f()
return Pair(breakFound, result).also { return Pair(breakFound, result).also {
@ -98,4 +98,40 @@ internal class CompilerContext(val tokens: List<Token>) {
breakFound = true 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
}
} }

View File

@ -28,7 +28,8 @@ class Context(
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg)) fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg))
@Suppress("unused") @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 { fun raiseError(message: String): Nothing {
throw ExecutionError(ObjError(this, message)) throw ExecutionError(ObjError(this, message))
@ -73,8 +74,13 @@ class Context(
fun copy() = Context(this, args, pos, thisObj) fun copy() = Context(this, args, pos, thisObj)
fun addItem(name: String, isMutable: Boolean, value: Obj): ObjRecord { fun addItem(
return ObjRecord(value, isMutable).also { objects.put(name, it) } 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 { fun getOrCreateNamespace(name: String): ObjClass {

View File

@ -280,6 +280,8 @@ private class Parser(fromPos: Pos) {
when (text) { when (text) {
"in" -> Token("in", from, Token.Type.IN) "in" -> Token("in", from, Token.Type.IN)
"is" -> Token("is", from, Token.Type.IS) "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 -> Token(text, from, Token.Type.ID)
} }
} else } else

View File

@ -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, AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT, SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label 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, ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE, NEWLINE,
EOF, EOF,

View File

@ -1453,4 +1453,15 @@ class ScriptTest {
assertEquals(sqrt(109), p.length()) assertEquals(sqrt(109), p.length())
""".trimIndent()) """.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 }
""")
}
} }