function annotation and some docs for it

This commit is contained in:
Sergey Chernov 2025-08-12 04:51:26 +03:00
parent 804087f16d
commit 9704f18284
7 changed files with 119 additions and 15 deletions

View File

@ -157,6 +157,7 @@ Ready features:
- [x] typesafe bit-effective serialization - [x] typesafe bit-effective serialization
- [x] compression/decompression (integrated in serialization) - [x] compression/decompression (integrated in serialization)
- [x] dynamic fields - [x] dynamic fields
- [x] function annotations
### Under way: ### Under way:

View File

@ -112,6 +112,30 @@ arguments list in almost arbitrary ways. For example:
) )
>>> void >>> 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 [parallelism]: parallelism.md

View File

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

View File

@ -96,7 +96,10 @@ class Compiler(
return result.toString() return result.toString()
} }
private var lastAnnotation: (suspend (Scope, ObjString,Statement) -> Statement)? = null
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
lastAnnotation = null
while (true) { while (true) {
val t = cc.next() val t = cc.next()
return when (t.type) { return when (t.type) {
@ -113,6 +116,10 @@ class Compiler(
parseExpression() parseExpression()
} }
Token.Type.ATLABEL -> {
lastAnnotation = parseAnnotation(t) ?: throw ScriptError(t.pos, "can't parse annotation")
continue
}
Token.Type.LABEL -> continue Token.Type.LABEL -> continue
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> 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") 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<List<ParsedArgument>, Boolean>? =
if( cc.skipNextIf(Token.Type.LPAREN))
parseArgs()
else
null
/** /**
* Parse keyword-starting statement. * Parse keyword-starting statement.
* @return parsed statement or null if, for example. [id] is not among keywords * @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'") throw ScriptError(t.pos, "Expected identifier after 'fun'")
else t.value else t.value
val annotation = lastAnnotation
t = cc.next() t = cc.next()
// Is extension? // Is extension?
if (t.type == Token.Type.DOT) { if (t.type == Token.Type.DOT) {
@ -1622,6 +1652,10 @@ 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
val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody)
?: fnBody
extTypeName?.let { typeName -> extTypeName?.let { typeName ->
// class extension method // class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
@ -1629,17 +1663,17 @@ class Compiler(
type.addFn(name, isOpen = true) { type.addFn(name, isOpen = true) {
// ObjInstance has a fixed instance scope, so we need to build a closure // ObjInstance has a fixed instance scope, so we need to build a closure
(thisObj as? ObjInstance)?.let { i -> (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: // other classes can create one-time scope for this rare case:
?: fnBody.execute(thisObj.autoInstanceScope(this)) ?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
} }
} }
// regular function/method // 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 // 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 annotatedFnBody
} }
return if (isStatic) { return if (isStatic) {
currentInitScope += fnCreateStatement currentInitScope += fnCreateStatement

View File

@ -20,7 +20,7 @@ class CompilerContext(val tokens: List<Token>) {
fun hasNext() = currentIndex < tokens.size fun hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0 fun hasPrevious() = currentIndex > 0
fun next() = fun next() =
if( currentIndex < tokens.size ) tokens[currentIndex++] if (currentIndex < tokens.size) tokens[currentIndex++]
else Token("", tokens.last().pos, Token.Type.EOF) else Token("", tokens.last().pos, Token.Type.EOF)
// throw IllegalStateException("No more tokens") // throw IllegalStateException("No more tokens")
@ -61,7 +61,7 @@ class CompilerContext(val tokens: List<Token>) {
*/ */
fun skipId(name: String): Boolean { fun skipId(name: String): Boolean {
current().let { t -> current().let { t ->
if( t.type == Token.Type.ID && t.value == name ) { if (t.type == Token.Type.ID && t.value == name) {
next() next()
return true return true
} }
@ -93,6 +93,20 @@ class CompilerContext(val tokens: List<Token>) {
} else true } 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") @Suppress("unused")
fun skipTokens(vararg tokenTypes: Token.Type) { fun skipTokens(vararg tokenTypes: Token.Type) {
while (next().type in tokenTypes) { /**/ while (next().type in tokenTypes) { /**/
@ -143,19 +157,24 @@ class CompilerContext(val tokens: List<Token>) {
fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean { fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean {
val pos = savePos() val pos = savePos()
var count = 0 var count = 0
while( count < qualifiers.size) { while (count < qualifiers.size) {
val t = next() val t = next()
when(t.type) { when (t.type) {
Token.Type.ID -> { Token.Type.ID -> {
if( t.value in qualifiers ) count++ if (t.value in qualifiers) count++
else { restorePos(pos); return false } else {
restorePos(pos); return false
}
} }
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {} 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() val t = next()
if( t.type == Token.Type.ID && t.value == keyword ) { if (t.type == Token.Type.ID && t.value == keyword) {
return true return true
} else { } else {
restorePos(pos) restorePos(pos)
@ -168,7 +187,7 @@ class CompilerContext(val tokens: List<Token>) {
* Note that [Token.Type.EOF] is not considered a whitespace token. * Note that [Token.Type.EOF] is not considered a whitespace token.
*/ */
fun skipWsTokens(): Token { fun skipWsTokens(): Token {
while( current().type in wstokens ) { while (current().type in wstokens) {
next() next()
} }
return next() return next()

View File

@ -2,6 +2,7 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
fun String.toSource(name: String = "eval"): Source = Source(name, this) fun String.toSource(name: String = "eval"): Source = Source(name, this)
@ -28,6 +29,7 @@ abstract class Statement(
abstract suspend fun execute(scope: Scope): Obj abstract suspend fun execute(scope: Scope): Obj
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other == ObjNull || other == ObjVoid ) return 1
throw UnsupportedOperationException("not comparable") throw UnsupportedOperationException("not comparable")
} }

View File

@ -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())
}
} }