fix compiler crash on 'not' combined with or/and in one expression

cherry-picked from v12 maintenance b71787a929
This commit is contained in:
Irmen de Jong committed 2026-10-17 01:18:09 +02:00
1 parent fba4bb5272
commit 99ff5768c5
2 files changed
+66 -2

No files matched your search

+31
View File
@@ -1,5 +1,6 @@
package prog8tests.ast
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.nulls.shouldNotBeNull
@@ -974,4 +975,34 @@ main {
errors.errors.size shouldBe 0
errors.infos.none { "when statement" in it } shouldBe true
}
test("boolean expressions mixing not with or/and compile") {
// the 'not' simplification and the De Morgan rewrite of the same subtree used to both queue an
// ast modification, which corrupted the parent links of the nodes they shared (compiler crash)
val conditions = listOf(
"a == b or not (a >= b)",
"not (a >= b) or not (c >= b)",
"not (a >= b) or c == b",
"not (a == 0) and b == 0",
"a == b and not (a >= b)",
"(a == b) == 0 or not (c >= d)"
)
conditions.forEach { condition ->
val src = """
main {
sub start() {
uword a,b,c,d
if $condition {
a++
}
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(Cx16Target(), true, src, outputDir, errors, false) shouldNotBe null
withClue(condition) {
errors.errors.size shouldBe 0
}
}
}
})
+35 -2
View File
@@ -3,7 +3,9 @@ package prog8.ast.walk
import prog8.ast.*
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.statements.Enumeration
import prog8.code.core.CommutativeOperators
import java.util.*
/**
@@ -26,6 +28,15 @@ open class AstModification {
/** Nodes that will be affected when [perform] is called. */
open val affectedNodes: List<Node> = emptyList()
/**
* The node in which a child is modified by [perform], if there is one.
* Modifications that target a node which is no longer part of the tree are skipped by
* [AstWalker.applyModifications]: they would only change a subtree that has been detached
* by another modification, and re-linking the parents of nodes that are still in use
* elsewhere corrupts the tree.
*/
open val targetParent: Node? = null
/** Apply the modification to the AST. */
open fun perform() {}
}
@@ -60,6 +71,7 @@ open class AstSetExpression(
private val parent: Node
) : AstModification() {
override val affectedNodes = listOf(newExpr)
override val targetParent: Node get() = parent
override fun perform() {
setter(newExpr)
@@ -113,6 +125,7 @@ open class AstReplaceNode(
private val parent: Node
) : AstModification() {
override val affectedNodes = listOf(node, replacement)
override val targetParent: Node get() = parent
override fun perform() {
parent.replaceChildNode(node, replacement)
@@ -273,6 +286,12 @@ abstract class AstWalker {
protected val modifications = mutableListOf<Triple<AstModification, Node, Node>>()
protected val affectedModules = mutableSetOf<Module>()
/**
* The nodes that have been replaced or removed by a modification that was applied already,
* and thus are no longer part of the tree.
*/
private val detachedNodes = Collections.newSetFromMap(IdentityHashMap<Node, Boolean>())
private fun track(mods: Iterable<AstModification>, node: Node, parent: Node) {
if (mods.any()) {
try {
@@ -315,10 +334,24 @@ abstract class AstWalker {
}
}
detachedNodes.clear()
var amount = 0
modifications.forEach {
it.first.perform()
val mod = it.first
if (mod.targetParent in detachedNodes) {
// another modification of this batch already detached the node that this modification
// would change, so applying it has no effect on the tree; skip it to avoid corrupting
// the parent links of nodes that are still in use by the replacement.
return@forEach
}
mod.perform()
when (mod) {
is AstReplaceNode -> detachedNodes += mod.node
is AstRemove -> detachedNodes += mod.node
else -> { }
}
amount++
}
val amount = modifications.size
modifications.clear()
return amount
}