lambda syntax added

This commit is contained in:
Sergey Chernov 2025-06-02 14:02:01 +04:00
parent 2344e19857
commit 19a2a1d909
10 changed files with 342 additions and 59 deletions

View File

@ -2,24 +2,24 @@
## Closures/scopes isolation
Each block has own scope, in which it can safely uses closures and override
outer vars:
> blocks are no-yet-ready lambda declaration so this sample will soon be altered
Each block has own scope, in which it can safely use closures and override
outer vars. Lets use some lambdas to create isolated scopes:
var param = "global"
val prefix = "param in "
val scope1 = {
var param = prefix + "scope1"
param
}
val scope2 = {
var param = prefix + "scope2"
param
}
// note that block returns its last value
println(scope1)
println(scope2)
println(scope1())
println(scope2())
println(param)
>>> param in scope1
>>> param in scope2
@ -38,7 +38,9 @@ One interesting way of using closure isolation is to keep state of the functions
counter = counter + 1
was
}
}
}()
// notice using of () above: it calls the lambda block that returns
// a function (callable!) that we will use:
println(getAndIncrement())
println(getAndIncrement())
println(getAndIncrement())
@ -50,3 +52,26 @@ One interesting way of using closure isolation is to keep state of the functions
Inner `counter` is not accessible from outside, no way; still it is kept
between calls in the closure, as inner function `doit`, returned from the
block, keeps reference to it and keeps it alive.
The example above could be rewritten using inner lambda, too:
val getAndIncrement = {
// will be updated by doIt()
var counter = 0
// we return callable fn from the block:
{
val was = counter
counter = counter + 1
was
}
}()
// notice using of () above: it calls the lambda block that returns
// a function (callable!) that we will use:
println(getAndIncrement())
println(getAndIncrement())
println(getAndIncrement())
>>> 0
>>> 1
>>> 2
>>> void

View File

@ -219,7 +219,17 @@ likely will know some English, the rest is the pure uncertainty.
Notice how function definition return a value, instance of `Callable`.
You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_.
You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_,
but Lyng syntax requires using the __lambda syntax__ to create such.
val check = {
it > 0 && it < 100
}
assert( check(1) )
assert( !check(101) )
>>> void
See lambdas section below.
There are default parameters in Lyng:
@ -239,13 +249,16 @@ Each __block has an isolated context that can be accessed from closures__. For e
var counter = 1
// this is ok: coumter is incremented
// this is ok: counter is incremented
fun increment(amount=1) {
// use counter from a closure:
counter = counter + amount
}
val taskAlias = fun someTask() {
increment(10)
assert( counter == 11 )
val callable = {
// this obscures global outer var with a local one
var counter = 0
// ...
@ -253,24 +266,57 @@ Each __block has an isolated context that can be accessed from closures__. For e
// ...
counter
}
assert(callable() == 1)
// but the global counter is not changed:
assert(counter == 11)
>>> void
As was told, `fun` statement return callable for the function, it could be used as a parameter, or elsewhere
to call it:
## Lambda functions
val taskAlias = fun someTask() {
println("Hello")
Lambda expression is a block with optional argument list ending with `->`. If argument list is omitted,
the call arguments will be assigned to `it`:
lambda = {
it + "!"
}
// call the callable stored in the var
taskAlias()
// or directly:
someTask()
>>> Hello
>>> Hello
assert( lambda is Callable)
assert( lambda("hello") == "hello!" )
void
### `it` assignment rules
When lambda is called with:
- no arguments: `it == void`
- exactly one argument: `it` will be assigned to it
- more than 1 argument: `it` will be a `List` with these arguments:
Here is an example:
val lambda = { it }
assert( lambda() == void )
assert( lambda("one") == "one")
assert( lambda("one", "two") == ["one", "two"])
>>> void
If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?)
### Declaring parameters
Parameter is a list of comma-separated names, with optional default value; last
one could be with ellipsis that means "the rest pf arguments as List":
assert( { a -> a }(10) == 10 )
assert( { a, b -> [a,b] }(1,2) == [1,2])
assert( { a, b=-1 -> [a,b] }(1) == [1,-1])
assert( { a, b...-> [a,...b] }(100) == [100])
// notice that splat syntax in array literal unrills
// ellipsis-caught arguments back:
assert( { a, b...-> [a,...b] }(100, 1, 2, 3) == [100, 1, 2, 3])
void
# Lists (aka arrays)
Lyng has built-in mutable array class `List` with simple literals:

View File

@ -24,6 +24,8 @@ data class Arguments(val list: List<Info>) : Iterable<Obj> {
operator fun get(index: Int): Obj = list[index].value
val values: List<Obj> by lazy { list.map { it.value } }
fun firstAndOnly(): Obj {
if (list.size != 1) throw IllegalArgumentException("Expected one argument, got ${list.size}")
return list.first().value

View File

@ -1,5 +1,7 @@
package net.sergeych.lyng
import net.sergeych.ling.TypeDecl
/**
* The LYNG compiler.
*/
@ -28,22 +30,6 @@ class Compiler(
val t = cc.next()
return when (t.type) {
Token.Type.ID -> {
// could be keyword, assignment or just the expression
// val next = tokens.next()
// if (next.type == Token.Type.ASSIGN) {
// this _is_ assignment statement
// return AssignStatement(
// t.pos, t.value,
// parseStatement(tokens) ?: throw ScriptError(
// t.pos,
// "Expecting expression for assignment operator"
// )
// )
// }
// not assignment, maybe keyword statement:
// get back the token which is not '=':
// tokens.previous()
// try keyword statement
parseKeywordStatement(t, cc)
?: run {
cc.previous()
@ -315,11 +301,11 @@ class Compiler(
}
}
// Token.Type.LBRACE -> {
// if( operand != null ) {
// throw ScriptError(t.pos, "syntax error: lambda expression not allowed here")
// }
// }
Token.Type.LBRACE -> {
if (operand != null) {
throw ScriptError(t.pos, "syntax error: lambda expression not allowed here")
} else operand = parseLambdaExpression(cc)
}
else -> {
@ -331,6 +317,56 @@ class Compiler(
}
}
/**
* Parse lambda expression, leading '{' is already consumed
*/
private fun parseLambdaExpression(cc: CompilerContext): Accessor {
// lambda args are different:
val startPos = cc.currentPos()
val argsDeclaration = parseArgsDeclaration(cc)
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
throw ScriptError(startPos, "lambda must have either valid arguments declaration with '->' or no arguments")
val pos = cc.currentPos()
val body = parseBlock(cc, skipLeadingBrace = true)
return Accessor { _ ->
statement {
val context = this.copy(pos)
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.values
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
// one args: it is this arg
1 -> l[0]
// more args: it is a list of args
else -> ObjList(l.toMutableList())
}
context.addItem("it", false, itValue)
} else {
// assign vars as declared
if( args.size != argsDeclaration.args.size && !argsDeclaration.args.last().isEllipsis)
raiseArgumentError("Too many arguments : called with ${args.size}, lambda accepts only ${argsDeclaration.args.size}")
for ((n, a) in argsDeclaration.args.withIndex()) {
if (n >= args.size) {
if (a.initialValue != null)
context.addItem(a.name, false, a.initialValue.execute(context))
else throw ScriptError(a.pos, "argument $n is out of scope")
} else {
val value = if( a.isEllipsis) {
ObjList(args.values.subList(n, args.values.size).toMutableList())
}
else
args[n]
context.addItem(a.name, false, value)
}
}
}
body.execute(context)
}.asReadonly
}
}
private fun parseArrayLiteral(cc: CompilerContext): List<ListEntry> {
// it should be called after LBRACKET is consumed
val entries = mutableListOf<ListEntry>()
@ -369,6 +405,89 @@ class Compiler(
}
}
data class ArgVar(
val name: String,
val type: TypeDecl = TypeDecl.Obj,
val pos: Pos,
val isEllipsis: Boolean,
val initialValue: Statement? = null
)
data class ArgsDeclaration(val args: List<ArgVar>, val endTokenType: Token.Type) {
init {
val i = args.indexOfFirst { it.isEllipsis }
if (i >= 0 && i != args.lastIndex) throw ScriptError(args[i].pos, "ellipsis argument must be last")
}
}
/**
* Parse argument declaration, used in lambda (and later in fn too)
* @return declaration or null if there is no valid list of arguments
*/
private fun parseArgsDeclaration(cc: CompilerContext): ArgsDeclaration? {
val result = mutableListOf<ArgVar>()
var endTokenType: Token.Type? = null
val startPos = cc.savePos()
while (endTokenType == null) {
val t = cc.next()
when (t.type) {
Token.Type.NEWLINE -> {}
Token.Type.ID -> {
var defaultValue: Statement? = null
cc.ifNextIs(Token.Type.ASSIGN) {
defaultValue = parseExpression(cc)
}
// type information
val typeInfo = parseTypeDeclaration(cc)
val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true)
result += ArgVar(t.value, typeInfo, t.pos, isEllipsis, defaultValue)
// important: valid argument list continues with ',' and ends with '->' or ')'
// otherwise it is not an argument list:
when (val tt = cc.next().type) {
Token.Type.RPAREN -> {
// end of arguments
endTokenType = tt
}
Token.Type.ARROW -> {
// end of arguments too
endTokenType = tt
}
Token.Type.COMMA -> {
// next argument, OK
}
else -> {
// this is not a valid list of arguments:
cc.restorePos(startPos) // for the current
return null
}
}
}
else -> {
// if we get here. there os also no valid list of arguments:
cc.restorePos(startPos)
return null
}
}
}
// arg list is valid:
checkNotNull(endTokenType)
return ArgsDeclaration(result, endTokenType)
}
private fun parseTypeDeclaration(cc: CompilerContext): TypeDecl {
val result = TypeDecl.Obj
cc.ifNextIs(Token.Type.COLON) {
TODO("parse type declaration here")
}
return result
}
private fun parseArgs(cc: CompilerContext): List<ParsedArgument> {
val args = mutableListOf<ParsedArgument>()
do {
@ -837,16 +956,19 @@ class Compiler(
}
}
private fun parseBlock(tokens: CompilerContext): Statement {
val t = tokens.next()
private fun parseBlock(cc: CompilerContext, skipLeadingBrace: Boolean = false): Statement {
val startPos = cc.currentPos()
if( !skipLeadingBrace ) {
val t = cc.next()
if (t.type != Token.Type.LBRACE)
throw ScriptError(t.pos, "Expected block body start: {")
val block = parseScript(t.pos, tokens)
return statement(t.pos) {
}
val block = parseScript(startPos, cc)
return statement(startPos) {
// block run on inner context:
block.execute(it.copy(t.pos))
block.execute(it.copy(startPos))
}.also {
val t1 = tokens.next()
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }")
}
@ -870,7 +992,7 @@ class Compiler(
}
}
val initialExpression = if (setNull) null else parseStatement(tokens)
val initialExpression = if (setNull) null else parseExpression(tokens)
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
return statement(nameToken.pos) { context ->

View File

@ -1,8 +1,18 @@
package net.sergeych.lyng
internal class CompilerContext(val tokens: List<Token>) : ListIterator<Token> by tokens.listIterator() {
internal class CompilerContext(val tokens: List<Token>) {
val labels = mutableSetOf<String>()
var currentIndex = 0
fun hasNext() = currentIndex < tokens.size
fun hasPrevious() = currentIndex > 0
fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ }
fun previous() = if( !hasPrevious() ) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = currentIndex
fun restorePos(pos: Int) { currentIndex = pos }
fun ensureLabelIsValid(pos: Pos, label: String) {
if (label !in labels)
throw ScriptError(pos, "Undefined label '$label'")

View File

@ -83,6 +83,11 @@ private class Parser(fromPos: Pos) {
Token("-", from, Token.Type.MINUSASSIGN)
}
'>' -> {
pos.advance()
Token("->", from, Token.Type.ARROW)
}
else -> Token("-", from, Token.Type.MINUS)
}
}

View File

@ -60,7 +60,8 @@ class Script(
addConst("Char", ObjChar.type)
addConst("List", ObjList.type)
addConst("Range", ObjRange.type)
@Suppress("RemoveRedundantQualifierName")
addConst("Callable", Statement.type)
// interfaces
addConst("Iterable", ObjIterable)
addConst("Array", ObjArray)

View File

@ -0,0 +1,5 @@
package net.sergeych.ling
sealed class TypeDecl {
object Obj : TypeDecl()
}

View File

@ -15,6 +15,8 @@ abstract class Statement(
val returnType: ObjType = ObjType.Any
) : Obj() {
override val objClass: ObjClass = type
abstract val pos: Pos
abstract suspend fun execute(context: Context): Obj
@ -28,6 +30,10 @@ abstract class Statement(
override fun toString(): String = "Callable@${this.hashCode()}"
companion object {
val type = ObjClass("Callable")
}
}
fun Statement.raise(text: String): Nothing {

View File

@ -1040,14 +1040,75 @@ class ScriptTest {
}
@Test
fun testLambda1() = runTest {
val l = eval("""
fun testLambdaWithIt1() = runTest {
eval("""
val x = {
122
it + "!"
}
x
val y = if( 4 < 3 ) "NG" else "OK"
assert( x::class == Callable)
assert( x is Callable)
assert(y == "OK")
assert( x("hello") == "hello!")
""".trimIndent())
println(l)
}
@Test
fun testLambdaWithIt2() = runTest {
eval("""
val x = {
assert(it == void)
}
assert( x() == void)
""".trimIndent())
}
@Test
fun testLambdaWithIt3() = runTest {
eval("""
val x = {
assert( it == [1,2,"end"])
}
println("0----")
assert( x(1, 2, "end") == void)
""".trimIndent())
}
@Test
fun testLambdaWithArgs() = runTest {
eval("""
val x = { x, y, z ->
assert( [x, y, z] == [1,2,"end"])
}
assert( x(1, 2, "end") == void)
""".trimIndent())
}
@Test
fun testLambdaWithArgsEllipsis() = runTest {
eval("""
val x = { x, y... ->
println("-- y=",y)
println(":: "+y::class)
assert( [x, ...y] == [1,2,"end"])
}
assert( x(1, 2, "end") == void)
assert( x(1, ...[2, "end"]) == void)
""".trimIndent())
}
@Test
fun testLambdaWithBadArgs() = runTest {
assertFails {
eval(
"""
val x = { x, y ->
void
}
assert( x(1, 2) == void)
assert( x(1, ...[2, "end"]) == void)
""".trimIndent()
)
}
}
}