v1.5.5-SNAPSHOT started. compile if support
This commit is contained in:
parent
214f1aec9e
commit
671583638b
@ -132,6 +132,9 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
|
||||
## 6. Control Flow
|
||||
- `if` is expression-like.
|
||||
- `compile if (cond) { ... } else { ... }` is a compile-time-only conditional.
|
||||
- current condition grammar is restricted to `defined(NameOr.Package)`, `!`, `&&`, `||`, and parentheses.
|
||||
- the untaken branch is skipped by the compiler and is not name-resolved or type-checked.
|
||||
- `when(value) { ... }` supported.
|
||||
- branch conditions support equality, `in`, `!in`, `is`, `!is`, and `nullable` predicate.
|
||||
- `when { ... }` (subject-less) is currently not implemented.
|
||||
|
||||
@ -6,6 +6,7 @@ This page documents the **current** rules: static name resolution, closure captu
|
||||
|
||||
## Current rules (bytecode compiler)
|
||||
- **All names resolve at compile time**: locals, parameters, captures, members, imports, and module globals must be known when compiling. Missing symbols are compile-time errors.
|
||||
- **Exception: `compile if` can skip dead branches**: inside an untaken `compile if (...)` branch, names are not resolved or type-checked at all. This is the supported way to guard optional classes or packages such as `defined(Udp)` or `defined(lyng.io.net)`.
|
||||
- **No runtime fallbacks**: there is no dynamic name lookup, no fallback opcodes, and no “search parent scopes” at runtime for missing names.
|
||||
- **Object members on unknown types only**: `toString`, `toInspectString`, `let`, `also`, `apply`, `run` are allowed on unknown types; all other members require a statically known receiver type or an explicit cast.
|
||||
- **Closures capture slots**: lambdas and nested functions capture **frame slots** directly. Captures are resolved at compile time and compiled to slot references.
|
||||
|
||||
@ -1094,6 +1094,37 @@ Or, more neat:
|
||||
>>> just 3
|
||||
>>> void
|
||||
|
||||
## compile if
|
||||
|
||||
`compile if` is a compile-time conditional. Unlike normal `if`, the compiler evaluates its condition while compiling
|
||||
the file and completely skips the untaken branch. This is useful when some class or package may or may not be
|
||||
available:
|
||||
|
||||
compile if (defined(Udp)) {
|
||||
val socket = Udp()
|
||||
println("udp is available")
|
||||
} else {
|
||||
println("udp is not available")
|
||||
}
|
||||
|
||||
`compile if` also supports single-statement branches:
|
||||
|
||||
compile if (defined(lyng.io.net) && !defined(Udp))
|
||||
println("network module exists, but Udp is not visible here")
|
||||
else
|
||||
println("either Udp exists or the module is unavailable")
|
||||
|
||||
Current condition syntax is intentionally limited to compile-time symbol checks:
|
||||
|
||||
- `defined(Name)`
|
||||
- `defined(package.name)`
|
||||
- `!`, `&&`, `||`
|
||||
- parentheses
|
||||
|
||||
Examples:
|
||||
compile if (defined(Udp) && defined(Tcp))
|
||||
println("both transports are available")
|
||||
|
||||
## When
|
||||
|
||||
See also: [Comprehensive guide to `when`](when.md)
|
||||
|
||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "1.5.4"
|
||||
version = "1.5.5-SNAPSHOT"
|
||||
|
||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||
|
||||
|
||||
@ -6763,6 +6763,16 @@ class Compiler(
|
||||
"break" -> parseBreakStatement(id.pos)
|
||||
"continue" -> parseContinueStatement(id.pos)
|
||||
"if" -> parseIfStatement()
|
||||
"compile" -> {
|
||||
val saved = cc.savePos()
|
||||
val next = cc.nextNonWhitespace()
|
||||
if (next.type == Token.Type.ID && next.value == "if") {
|
||||
parseCompileIfStatement(id.pos)
|
||||
} else {
|
||||
cc.restorePos(saved)
|
||||
null
|
||||
}
|
||||
}
|
||||
"class" -> {
|
||||
pendingDeclStart = id.pos
|
||||
pendingDeclDoc = consumePendingDoc()
|
||||
@ -8336,6 +8346,307 @@ class Compiler(
|
||||
return wrapBytecode(stmt)
|
||||
}
|
||||
|
||||
private suspend fun parseCompileIfStatement(startPos: Pos): Statement {
|
||||
val start = ensureLparen()
|
||||
val condition = parseCompileCondition()
|
||||
val pos = ensureRparen()
|
||||
|
||||
val ifBody = if (condition) {
|
||||
parseCompileIfBranch(pos, "compile if")
|
||||
} else {
|
||||
skipCompileIfBranch(pos, "compile if")
|
||||
NopStatement
|
||||
}
|
||||
|
||||
val saved = cc.savePos()
|
||||
val maybeElse = cc.nextNonWhitespace()
|
||||
return if (maybeElse.type == Token.Type.ID && maybeElse.value == "else") {
|
||||
if (condition) {
|
||||
skipCompileIfBranch(pos, "compile else")
|
||||
ifBody
|
||||
} else {
|
||||
parseCompileIfBranch(pos, "compile else")
|
||||
}
|
||||
} else {
|
||||
cc.restorePos(saved)
|
||||
if (condition) ifBody else NopStatement
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseCompileCondition(): Boolean = parseCompileConditionOr()
|
||||
|
||||
private suspend fun parseCompileConditionOr(): Boolean {
|
||||
var value = parseCompileConditionAnd()
|
||||
while (true) {
|
||||
val saved = cc.savePos()
|
||||
val token = cc.nextNonWhitespace()
|
||||
if (token.type != Token.Type.OR) {
|
||||
cc.restorePos(saved)
|
||||
return value
|
||||
}
|
||||
value = value || parseCompileConditionAnd()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseCompileConditionAnd(): Boolean {
|
||||
var value = parseCompileConditionUnary()
|
||||
while (true) {
|
||||
val saved = cc.savePos()
|
||||
val token = cc.nextNonWhitespace()
|
||||
if (token.type != Token.Type.AND) {
|
||||
cc.restorePos(saved)
|
||||
return value
|
||||
}
|
||||
value = value && parseCompileConditionUnary()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseCompileConditionUnary(): Boolean {
|
||||
val saved = cc.savePos()
|
||||
val token = cc.nextNonWhitespace()
|
||||
return when {
|
||||
token.type == Token.Type.NOT -> !parseCompileConditionUnary()
|
||||
token.type == Token.Type.LPAREN -> {
|
||||
val nested = parseCompileConditionOr()
|
||||
val close = cc.nextNonWhitespace()
|
||||
if (close.type != Token.Type.RPAREN) {
|
||||
throw ScriptError(close.pos, "expected ')' in compile-time condition")
|
||||
}
|
||||
nested
|
||||
}
|
||||
token.type == Token.Type.ID && token.value == "defined" -> parseDefinedCompileCondition(token.pos)
|
||||
else -> {
|
||||
cc.restorePos(saved)
|
||||
throw ScriptError(token.pos, "compile if condition supports only defined(...), !, &&, ||, and parentheses")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseDefinedCompileCondition(atPos: Pos): Boolean {
|
||||
val open = cc.nextNonWhitespace()
|
||||
if (open.type != Token.Type.LPAREN) {
|
||||
throw ScriptError(open.pos, "expected '(' after defined")
|
||||
}
|
||||
val target = parseDefinedCompileTarget()
|
||||
val close = cc.nextNonWhitespace()
|
||||
if (close.type != Token.Type.RPAREN) {
|
||||
throw ScriptError(close.pos, "expected ')' after defined($target")
|
||||
}
|
||||
return isCompileDefined(target, atPos)
|
||||
}
|
||||
|
||||
private fun parseDefinedCompileTarget(): String {
|
||||
val first = cc.nextNonWhitespace()
|
||||
if (first.type != Token.Type.ID) {
|
||||
throw ScriptError(first.pos, "defined(...) expects a class or package name")
|
||||
}
|
||||
val parts = mutableListOf(first.value)
|
||||
while (true) {
|
||||
val saved = cc.savePos()
|
||||
val dot = cc.nextNonWhitespace()
|
||||
if (dot.type != Token.Type.DOT) {
|
||||
cc.restorePos(saved)
|
||||
break
|
||||
}
|
||||
val part = cc.nextNonWhitespace()
|
||||
if (part.type != Token.Type.ID) {
|
||||
throw ScriptError(part.pos, "expected identifier after '.' in defined(...)")
|
||||
}
|
||||
parts += part.value
|
||||
}
|
||||
return parts.joinToString(".")
|
||||
}
|
||||
|
||||
private suspend fun isCompileDefined(name: String, pos: Pos): Boolean {
|
||||
if (name.contains('.')) {
|
||||
if (resolveClassByName(name) != null) return true
|
||||
return try {
|
||||
importManager.prepareImport(pos, name, null)
|
||||
true
|
||||
} catch (_: ImportException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
if (name == "__PACKAGE__" || name == "this") return true
|
||||
if (lookupSlotLocation(name, includeModule = false) != null) return true
|
||||
val classCtx = codeContexts.lastOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
|
||||
val implicitTypeFromFunc = implicitReceiverTypeForMember(name)
|
||||
val hasImplicitClassMember = classCtx != null && hasImplicitThisMember(name, classCtx.name)
|
||||
if (classCtx != null && classCtx.declaredMembers.contains(name)) return true
|
||||
if (classCtx != null && classCtx.classScopeMembers.contains(name)) return true
|
||||
if (classCtx != null && hasImplicitThisMember(name, classCtx.name)) return true
|
||||
if (implicitTypeFromFunc != null) return true
|
||||
if (codeContexts.any { it is CodeContext.ClassBody } && extensionNames.contains(name)) return true
|
||||
|
||||
val moduleLoc = if (slotPlanStack.size == 1) lookupSlotLocation(name, includeModule = true) else null
|
||||
if (moduleLoc != null) {
|
||||
val moduleDeclaredNames = localNamesStack.firstOrNull()
|
||||
if (moduleDeclaredNames == null || !moduleDeclaredNames.contains(name)) {
|
||||
if (resolveImportBinding(name, pos) != null) return true
|
||||
if (predeclaredTopLevelValueNames.contains(name)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val modulePlan = moduleSlotPlan()
|
||||
val moduleEntry = modulePlan?.slots?.get(name)
|
||||
if (moduleEntry != null) {
|
||||
val moduleDeclaredNames = localNamesStack.firstOrNull()
|
||||
if (moduleDeclaredNames == null || !moduleDeclaredNames.contains(name)) {
|
||||
if (resolveImportBinding(name, pos) != null) return true
|
||||
if (predeclaredTopLevelValueNames.contains(name)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (resolveImportBinding(name, pos) != null) return true
|
||||
return resolveClassByName(name) != null
|
||||
}
|
||||
|
||||
private suspend fun parseCompileIfBranch(pos: Pos, branchLabel: String): Statement {
|
||||
return parseStatement() ?: throw ScriptError(pos, "$branchLabel expected statement")
|
||||
}
|
||||
|
||||
private fun skipCompileIfBranch(pos: Pos, branchLabel: String) {
|
||||
skipStandaloneStatement(pos, branchLabel)
|
||||
}
|
||||
|
||||
private fun skipStandaloneStatement(pos: Pos, branchLabel: String) {
|
||||
while (true) {
|
||||
val token = cc.next()
|
||||
when (token.type) {
|
||||
Token.Type.NEWLINE,
|
||||
Token.Type.SEMICOLON,
|
||||
Token.Type.SINGLE_LINE_COMMENT,
|
||||
Token.Type.MULTILINE_COMMENT -> continue
|
||||
Token.Type.EOF -> throw ScriptError(pos, "$branchLabel expected statement")
|
||||
else -> {
|
||||
skipStatementFromToken(token, branchLabel)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipStatementFromToken(first: Token, branchLabel: String) {
|
||||
if (first.type == Token.Type.LBRACE) {
|
||||
skipBalancedGroup(Token.Type.LBRACE, Token.Type.RBRACE, branchLabel)
|
||||
return
|
||||
}
|
||||
if (first.type == Token.Type.ID) {
|
||||
when (first.value) {
|
||||
"if" -> {
|
||||
skipParenthesizedAfterKeyword(branchLabel)
|
||||
skipStandaloneStatement(first.pos, branchLabel)
|
||||
skipOptionalElseBranch(branchLabel)
|
||||
return
|
||||
}
|
||||
"compile" -> {
|
||||
val saved = cc.savePos()
|
||||
val next = cc.nextNonWhitespace()
|
||||
if (next.type == Token.Type.ID && next.value == "if") {
|
||||
skipParenthesizedAfterKeyword(branchLabel)
|
||||
skipStandaloneStatement(first.pos, branchLabel)
|
||||
skipOptionalElseBranch(branchLabel)
|
||||
return
|
||||
}
|
||||
cc.restorePos(saved)
|
||||
}
|
||||
"while", "for" -> {
|
||||
skipParenthesizedAfterKeyword(branchLabel)
|
||||
skipStandaloneStatement(first.pos, branchLabel)
|
||||
skipOptionalElseBranch(branchLabel)
|
||||
return
|
||||
}
|
||||
"do" -> {
|
||||
skipStandaloneStatement(first.pos, branchLabel)
|
||||
val whileTok = cc.nextNonWhitespace()
|
||||
if (whileTok.type != Token.Type.ID || whileTok.value != "while") {
|
||||
throw ScriptError(whileTok.pos, "expected 'while' after do body in skipped $branchLabel")
|
||||
}
|
||||
skipParenthesizedAfterKeyword(branchLabel)
|
||||
skipOptionalElseBranch(branchLabel)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
skipFlatStatementRemainder(branchLabel)
|
||||
}
|
||||
|
||||
private fun skipOptionalElseBranch(branchLabel: String) {
|
||||
val saved = cc.savePos()
|
||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||
val token = cc.nextNonWhitespace()
|
||||
if (token.type == Token.Type.ID && token.value == "else") {
|
||||
skipStandaloneStatement(token.pos, branchLabel)
|
||||
} else {
|
||||
cc.restorePos(saved)
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipParenthesizedAfterKeyword(branchLabel: String) {
|
||||
val open = cc.nextNonWhitespace()
|
||||
if (open.type != Token.Type.LPAREN) {
|
||||
throw ScriptError(open.pos, "expected '(' in skipped $branchLabel")
|
||||
}
|
||||
skipBalancedGroup(Token.Type.LPAREN, Token.Type.RPAREN, branchLabel)
|
||||
}
|
||||
|
||||
private fun skipBalancedGroup(openType: Token.Type, closeType: Token.Type, branchLabel: String) {
|
||||
var depth = 1
|
||||
while (depth > 0) {
|
||||
val token = cc.next()
|
||||
when (token.type) {
|
||||
openType -> depth += 1
|
||||
closeType -> depth -= 1
|
||||
Token.Type.EOF -> throw ScriptError(token.pos, "unbalanced tokens in skipped $branchLabel")
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipFlatStatementRemainder(branchLabel: String) {
|
||||
var parenDepth = 0
|
||||
var bracketDepth = 0
|
||||
var braceDepth = 0
|
||||
while (true) {
|
||||
val saved = cc.savePos()
|
||||
val token = cc.next()
|
||||
when (token.type) {
|
||||
Token.Type.LPAREN -> parenDepth += 1
|
||||
Token.Type.RPAREN -> {
|
||||
if (parenDepth == 0 && bracketDepth == 0 && braceDepth == 0) {
|
||||
cc.restorePos(saved)
|
||||
return
|
||||
}
|
||||
parenDepth -= 1
|
||||
}
|
||||
Token.Type.LBRACKET -> bracketDepth += 1
|
||||
Token.Type.RBRACKET -> {
|
||||
if (bracketDepth == 0 && parenDepth == 0 && braceDepth == 0) {
|
||||
cc.restorePos(saved)
|
||||
return
|
||||
}
|
||||
bracketDepth -= 1
|
||||
}
|
||||
Token.Type.LBRACE -> braceDepth += 1
|
||||
Token.Type.RBRACE -> {
|
||||
if (braceDepth == 0 && parenDepth == 0 && bracketDepth == 0) {
|
||||
cc.restorePos(saved)
|
||||
return
|
||||
}
|
||||
braceDepth -= 1
|
||||
}
|
||||
Token.Type.NEWLINE, Token.Type.SEMICOLON -> {
|
||||
if (parenDepth == 0 && bracketDepth == 0 && braceDepth == 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
Token.Type.EOF -> return
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseFunctionDeclaration(
|
||||
visibility: Visibility = Visibility.Public,
|
||||
isAbstract: Boolean = false,
|
||||
|
||||
106
lynglib/src/commonTest/kotlin/CompileIfTest.kt
Normal file
106
lynglib/src/commonTest/kotlin/CompileIfTest.kt
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.eval
|
||||
import net.sergeych.lyng.obj.toInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CompileIfTest {
|
||||
|
||||
@Test
|
||||
fun falseBranchSkipsMissingSymbols() = runTest {
|
||||
val result = eval(
|
||||
"""
|
||||
var x = 0
|
||||
compile if (defined(NoSuchClass)) {
|
||||
x = NoSuchClass("foo")
|
||||
} else {
|
||||
x = 42
|
||||
}
|
||||
x
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(42, result.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun logicExpressionsWorkForDefinedChecks() = runTest {
|
||||
val result = eval(
|
||||
"""
|
||||
var x = 0
|
||||
compile if (defined(String) && !defined(NoSuchClass)) {
|
||||
x = 7
|
||||
} else {
|
||||
x = NoSuchClass("foo")
|
||||
}
|
||||
x
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(7, result.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun packageChecksWorkInCompileIf() = runTest {
|
||||
val result = eval(
|
||||
"""
|
||||
var x = 0
|
||||
compile if (defined(lyng.stdlib) && !defined(no.such.pkg)) {
|
||||
x = 1
|
||||
} else {
|
||||
x = 2
|
||||
}
|
||||
x
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(1, result.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleStatementBranchesWork() = runTest {
|
||||
val result = eval(
|
||||
"""
|
||||
var x = 0
|
||||
compile if (defined(String))
|
||||
x = 9
|
||||
else
|
||||
x = NoSuchClass("foo")
|
||||
x
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(9, result.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedElseBranchIsSkippedAsSingleStatement() = runTest {
|
||||
val result = eval(
|
||||
"""
|
||||
var x = 0
|
||||
compile if (defined(NoSuchClass))
|
||||
if (true)
|
||||
x = NoSuchClass("foo")
|
||||
else
|
||||
x = 1
|
||||
else
|
||||
x = 5
|
||||
x
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(5, result.toInt())
|
||||
}
|
||||
}
|
||||
@ -20,9 +20,7 @@ import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.resolution.SymbolOrigin
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CompileTimeResolutionSpecTest {
|
||||
@ -83,6 +81,20 @@ class CompileTimeResolutionSpecTest {
|
||||
assertTrue(report.errors.any { it.message.contains("missingName") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compileIfSkipsResolutionForUnselectedBranch() = runTest {
|
||||
val report = dryRun(
|
||||
"""
|
||||
compile if (defined(NoSuchClass)) {
|
||||
NoSuchClass("foo")
|
||||
} else {
|
||||
1
|
||||
}
|
||||
"""
|
||||
)
|
||||
assertTrue(report.errors.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miAmbiguityIsCompileError() = runTest {
|
||||
val report = dryRun(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user