Files
prog8/compiler/test/TestDeferRecursionCheck.kt
2026-10-07 19:06:16 +02:00

294 lines
8.0 KiB
Kotlin

package prog8tests.compiler
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import prog8.code.target.C64Target
import prog8.code.target.Qemu68kTarget
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
class TestDeferRecursionCheck : FunSpec({
val outputDir = tempdir().toPath()
// m68k stack frames make defer-referenced locals static,
// so a deferring subroutine that can have two live activations must be rejected.
// The check only runs on m68k targets and only when a defer body touches subroutine-local state.
test("directly recursive subroutine with defer on a local is rejected on qemu68k") {
val src = """
main {
sub start() {
counter(0)
}
sub counter(ubyte n) {
ubyte x = 1
defer x = x + 1
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("same recursive program is unaffected on a 6502 target") {
val src = """
main {
sub start() {
counter(0)
}
sub counter(ubyte n) {
ubyte x = 1
defer x = x + 1
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(C64Target(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("recursive subroutine whose defer only touches a global shared variable is allowed on qemu68k") {
val src = """
main {
ubyte @shared result
sub start() {
counter(0)
}
sub counter(ubyte n) {
defer result = result + 1
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("non-recursive subroutine with defer on a local is allowed on qemu68k") {
val src = """
main {
sub start() {
helper()
}
sub helper() {
ubyte x = 1
defer x = x + 1
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("mutually recursive subroutine with defer on a local is rejected on qemu68k") {
val src = """
main {
sub start() {
a(0)
}
sub a(ubyte n) {
ubyte x = 1
defer x = x + 1
if n < 10 {
b(n + 1)
}
}
sub b(ubyte n) {
a(n)
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("deferring subroutine whose address is taken is conservatively rejected on qemu68k") {
val src = """
main {
long @shared sub_address
sub start() {
sub_address = &helper
}
sub helper() {
ubyte x = 1
defer x = x + 1
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("defer nested inside a loop in a recursive subroutine is also detected on qemu68k") {
val src = """
main {
sub start() {
counter(0)
}
sub counter(ubyte n) {
ubyte x = 1
repeat 2 {
defer x = x + 1
}
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("pointer dereference through a local pointer in a defer is detected on qemu68k") {
val src = """
main {
sub start() {
counter(0)
}
sub counter(ubyte n) {
ubyte @shared storage
^^ubyte p = &storage
defer p^^ = 42
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("defer referencing a parameter of a recursive subroutine is detected on qemu68k") {
val src = """
main {
sub start() {
counter(0)
}
sub counter(ubyte n) {
defer n = n + 1
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("indexed write through a local array in a defer is detected on qemu68k") {
val src = """
main {
sub start() {
counter(0)
}
sub counter(ubyte n) {
ubyte[4] arr
defer arr[0] = 1
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("recursive subroutine referenced via on-call dispatch with defer on a local is rejected on qemu68k") {
val src = """
main {
sub start() {
ubyte i = 0
on i call (helper, other)
}
sub helper() {
ubyte x = 1
defer x = x + 1
}
sub other() {
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "defer"
errors.errors[0] shouldContain "recursive"
}
test("defer touching only constants and globals in a recursive subroutine is allowed on qemu68k") {
val src = """
main {
ubyte @shared out
sub start() {
counter(0)
}
sub counter(ubyte n) {
const X = 5
defer out = X
if n < 10 {
counter(n + 1)
}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
})