Files
prog8/compiler/test/TestFrameAddressEscape.kt
T

257 lines
8.3 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 prog8.code.target.C64Target
import prog8.code.target.Qemu68kTarget
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
class TestFrameAddressEscape : FunSpec({
val outputDir = tempdir().toPath()
// m68k stack frames make subroutine locals per-activation,
// so a pointer to a local dangles after the subroutine returns. The compiler warns, best-effort,
// on the m68k targets when a frame address is passed to a call or mentioned in inline assembly -
// but a returned address, or one stored where it outlives the activation, is always wrong
// and is a compile error instead.
test("storing the address of a local into a shared variable is an error on qemu68k") {
val src = """
main {
^^ubyte @shared shared_ptr
sub start() {
ubyte buf = 42
shared_ptr = &buf
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.any { "the address of local 'buf' in subroutine 'start' is stored in 'shared_ptr'" in it } shouldBe true
errors.errors.any { "a stack frame address is only valid while the subroutine is running" in it } shouldBe true
}
test("returning the address of a local is an error on qemu68k") {
val src = """
main {
sub start() {
^^ubyte p = getbuf()
}
sub getbuf() -> ^^ubyte {
ubyte buf = 42
return &buf
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.any { "the address of local 'buf' in subroutine 'getbuf' escapes the subroutine (returned)" in it } shouldBe true
}
test("passing the address of a local to a subroutine call warns on qemu68k") {
val src = """
main {
ubyte @shared result
sub start() {
ubyte buf = 42
consume(&buf)
}
sub consume(^^ubyte p) {
result = p^^
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.any { "the address of local 'buf' in subroutine 'start' escapes the subroutine (passed to a call)" in it } shouldBe true
}
test("error fires only once per escaping variable even with multiple uses") {
val src = """
main {
^^ubyte @shared shared_ptr
sub start() {
ubyte buf = 42
shared_ptr = &buf
shared_ptr = &buf
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.count { "the address of local 'buf' in subroutine 'start'" in it } shouldBe 1
}
test("no warning when the pointer stays inside the same subroutine") {
val src = """
main {
sub start() {
ubyte buf = 42
^^ubyte p = &buf
p^^ = 99
buf = p^^
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.none { "address of local" in it } shouldBe true
}
test("no escape warnings at all on a 6502 target") {
val src = """
main {
^^ubyte @shared shared_ptr
sub start() {
ubyte buf = 42
shared_ptr = &buf
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(C64Target(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.none { "address of local" in it } shouldBe true
}
test("inline assembly using p8_regfile warns when the program is recursive on qemu68k") {
val src = """
main {
ubyte @shared g
sub start() {
g = fib(5)
}
sub fib(ubyte n) -> ubyte {
%asm {{
move.l p8_regfile, d0
}}
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.any { "inline assembly uses p8_regfile" in it } shouldBe true
errors.warnings.any { "registers now live in stack frames" in it } shouldBe true
}
test("inline assembly using p8_fregfile also warns when the program is recursive on qemu68k") {
val src = """
main {
ubyte @shared g
sub start() {
g = fib(5)
}
sub fib(ubyte n) -> ubyte {
%asm {{
fmove.s p8_fregfile, fp0
}}
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.any { "inline assembly uses p8_fregfile" in it } shouldBe true
}
test("inline assembly using p8_regfile does not warn when the program is not recursive") {
val src = """
main {
sub start() {
%asm {{
move.l p8_regfile, d0
}}
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.none { "p8_regfile" in it } shouldBe true
}
test("parameter referenced from inline assembly suggests @shared on qemu68k") {
val src = """
main {
sub start() {
test(42)
}
sub test(ubyte param) {
%asm {{
tst.b p8b_main.p8s_test.p8v_param
}}
param++
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.infos.any { "parameter 'param' of subroutine 'test' is referenced from inline assembly" in it } shouldBe true
errors.infos.any { "consider declaring it @shared" in it } shouldBe true
}
test("parameter referenced from inline assembly stays silent when it is @shared") {
val src = """
main {
sub start() {
test(42)
}
sub test(ubyte @shared param) {
%asm {{
tst.b p8b_main.p8s_test.p8v_param
}}
param++
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.infos.none { "consider declaring it @shared" in it } shouldBe true
}
test("local referenced from inline assembly suggests @shared on qemu68k") {
val src = """
main {
sub start() {
ubyte xx = 1
%asm {{
tst.b p8b_main.p8s_start.p8v_xx
}}
xx++
}
}
"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = false, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.infos.any { "local 'xx' in subroutine 'start' is referenced from inline assembly" in it } shouldBe true
errors.infos.any { "consider declaring it @shared" in it } shouldBe true
}
})