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] compression/decompression (integrated in serialization)
- [x] dynamic fields
- [x] function annotations
### Under way:

View File

@ -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

View File

@ -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 {

View File

@ -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<List<ParsedArgument>, 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

View File

@ -20,7 +20,7 @@ class CompilerContext(val tokens: List<Token>) {
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<Token>) {
*/
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<Token>) {
} 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<Token>) {
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<Token>) {
* 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()

View File

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

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