From 9704f18284dc760af16ea9c6789b6fdad581643a Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 12 Aug 2025 04:51:26 +0300 Subject: [PATCH] function annotation and some docs for it --- README.md | 1 + docs/advanced_topics.md | 26 +++++++++++- lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 42 +++++++++++++++++-- .../net/sergeych/lyng/CompilerContext.kt | 37 ++++++++++++---- .../kotlin/net/sergeych/lyng/statements.kt | 2 + lynglib/src/commonTest/kotlin/ScriptTest.kt | 24 +++++++++++ 7 files changed, 119 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 02e476c..6769f03 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Ready features: - [x] typesafe bit-effective serialization - [x] compression/decompression (integrated in serialization) - [x] dynamic fields +- [x] function annotations ### Under way: diff --git a/docs/advanced_topics.md b/docs/advanced_topics.md index 13f24dc..47b8978 100644 --- a/docs/advanced_topics.md +++ b/docs/advanced_topics.md @@ -112,6 +112,30 @@ arguments list in almost arbitrary ways. For example: ) >>> void -, +# Annotations + +Annotation in Lyng resembles these proposed for Javascript. Annotation is just regular functions that, if used as annotation, are called when defining a function, var, val or class. + +## Function annotation + +When used without params, annotation calls a function with two arguments: actual function name and callable function body. Function annotation __must return callable for the function__, either what it received as a second argument (most often), or something else. Annotation name convention is upper scaled: + + var annotated = false + + // this is annotation function: + fun Special(name, body) { + assertEquals("foo", name) + annotated = true + { body(it) + 100 } + } + + @Special + fun foo(value) { value + 1 } + + assert(annotated) + assertEquals(111, foo( 10 )) + >>> void + +Function annotation can have more args specified at call time. [parallelism]: parallelism.md diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 3df5c24..a527aa8 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.8.6-SNAPSHOT" +version = "0.8.7-SNAPSHOT" buildscript { repositories { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index a2da1d9..cdce001 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -96,7 +96,10 @@ class Compiler( return result.toString() } + private var lastAnnotation: (suspend (Scope, ObjString,Statement) -> Statement)? = null + private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { + lastAnnotation = null while (true) { val t = cc.next() return when (t.type) { @@ -113,6 +116,10 @@ class Compiler( parseExpression() } + Token.Type.ATLABEL -> { + lastAnnotation = parseAnnotation(t) ?: throw ScriptError(t.pos, "can't parse annotation") + continue + } Token.Type.LABEL -> continue Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue @@ -818,6 +825,26 @@ class Compiler( return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number") } + suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString,Statement)->Statement) { + val extraArgs = parseArgsOrNull() + println("annotation ${t.value}: args: $extraArgs") + return { scope, name, body -> + val extras = extraArgs?.first?.toArguments(scope, extraArgs.second)?.list + val required = listOf(name, body) + val args = extras?.let { required + it } ?: required + val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}") + if( fn !is Statement ) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}") + (fn.execute(scope.copy(Arguments(args))) as? Statement) + ?: scope.raiseClassCastError("function annotation must return callable") + } + } + + suspend fun parseArgsOrNull(): Pair, Boolean>? = + if( cc.skipNextIf(Token.Type.LPAREN)) + parseArgs() + else + null + /** * Parse keyword-starting statement. * @return parsed statement or null if, for example. [id] is not among keywords @@ -1572,6 +1599,9 @@ class Compiler( throw ScriptError(t.pos, "Expected identifier after 'fun'") else t.value + val annotation = lastAnnotation + + t = cc.next() // Is extension? if (t.type == Token.Type.DOT) { @@ -1622,6 +1652,10 @@ class Compiler( // we added fn in the context. now we must save closure // for the function closure = context + + val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody) + ?: fnBody + extTypeName?.let { typeName -> // class extension method val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") @@ -1629,17 +1663,17 @@ class Compiler( type.addFn(name, isOpen = true) { // ObjInstance has a fixed instance scope, so we need to build a closure (thisObj as? ObjInstance)?.let { i -> - fnBody.execute(ClosureScope(this, i.instanceScope)) + annotatedFnBody.execute(ClosureScope(this, i.instanceScope)) } // other classes can create one-time scope for this rare case: - ?: fnBody.execute(thisObj.autoInstanceScope(this)) + ?: annotatedFnBody.execute(thisObj.autoInstanceScope(this)) } } // regular function/method - ?: context.addItem(name, false, fnBody, visibility) + ?: context.addItem(name, false, annotatedFnBody, visibility) // as the function can be called from anywhere, we have // saved the proper context in the closure - fnBody + annotatedFnBody } return if (isStatic) { currentInitScope += fnCreateStatement diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index ebca79a..ada269b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -20,7 +20,7 @@ class CompilerContext(val tokens: List) { fun hasNext() = currentIndex < tokens.size fun hasPrevious() = currentIndex > 0 fun next() = - if( currentIndex < tokens.size ) tokens[currentIndex++] + if (currentIndex < tokens.size) tokens[currentIndex++] else Token("", tokens.last().pos, Token.Type.EOF) // throw IllegalStateException("No more tokens") @@ -61,7 +61,7 @@ class CompilerContext(val tokens: List) { */ fun skipId(name: String): Boolean { current().let { t -> - if( t.type == Token.Type.ID && t.value == name ) { + if (t.type == Token.Type.ID && t.value == name) { next() return true } @@ -93,6 +93,20 @@ class CompilerContext(val tokens: List) { } else true } + /** + * If next token is one of these types, skip it. + * @return true if token was found and skipped + */ + fun skipNextIf(vararg types: Token.Type): Boolean { + val t = next() + return if (t.type in types) + true + else { + previous() + false + } + } + @Suppress("unused") fun skipTokens(vararg tokenTypes: Token.Type) { while (next().type in tokenTypes) { /**/ @@ -143,19 +157,24 @@ class CompilerContext(val tokens: List) { fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean { val pos = savePos() var count = 0 - while( count < qualifiers.size) { + while (count < qualifiers.size) { val t = next() - when(t.type) { + when (t.type) { Token.Type.ID -> { - if( t.value in qualifiers ) count++ - else { restorePos(pos); return false } + if (t.value in qualifiers) count++ + else { + restorePos(pos); return false + } } + Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {} - else -> { restorePos(pos); return false } + else -> { + restorePos(pos); return false + } } } val t = next() - if( t.type == Token.Type.ID && t.value == keyword ) { + if (t.type == Token.Type.ID && t.value == keyword) { return true } else { restorePos(pos) @@ -168,7 +187,7 @@ class CompilerContext(val tokens: List) { * Note that [Token.Type.EOF] is not considered a whitespace token. */ fun skipWsTokens(): Token { - while( current().type in wstokens ) { + while (current().type in wstokens) { next() } return next() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index 80540f9..f4f08e6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -2,6 +2,7 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.ObjVoid fun String.toSource(name: String = "eval"): Source = Source(name, this) @@ -28,6 +29,7 @@ abstract class Statement( abstract suspend fun execute(scope: Scope): Obj override suspend fun compareTo(scope: Scope, other: Obj): Int { + if( other == ObjNull || other == ObjVoid ) return 1 throw UnsupportedOperationException("not comparable") } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 2e0233c..19a5717 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -2831,4 +2831,28 @@ class ScriptTest { ) } + @Test + fun tesFunAnnotation() = runTest { + eval(""" + + val exportedSymbols = Map() + + fun Exported(name, f, overrideName = null) { + assertEquals(name, "getBalance") + assertEquals(null, overrideName) + exportedSymbols[ overrideName ?: name ] = f + f + } + + @Exported + fun getBalance(x = 0) { + 121 + x + } + + assert( exportedSymbols["getBalance"] != null ) + assertEquals(122, getBalance(1)) + + """.trimIndent()) + } + } \ No newline at end of file