some improvements to IR peephole optimizer

This commit is contained in:
Irmen de Jong 2023-05-01 23:00:51 +02:00
parent ce00e49a89
commit f465b2e2a0
14 changed files with 156 additions and 126 deletions

View File

@ -54,23 +54,8 @@ class IRCodeGen(
ensureFirstChunkLabels(irProg)
irProg.linkChunks()
if(options.optimize) {
val optimizer = IRPeepholeOptimizer(irProg)
optimizer.optimize()
val remover = IRUnusedCodeRemover(irProg, irSymbolTable, errors)
do {
val numRemoved = remover.optimize()
} while(numRemoved>0 && errors.noErrors())
errors.report()
irProg.linkChunks() // re-link
} else {
val optimizer = IRPeepholeOptimizer(irProg)
optimizer.optimizeOnlyJoinChunks()
}
val optimizer = IRPeepholeOptimizer(irProg)
optimizer.optimize(options.optimize, errors)
irProg.validate()
return irProg
}
@ -1470,7 +1455,7 @@ class IRCodeGen(
}
private fun translate(block: PtBlock): IRBlock {
val irBlock = IRBlock(block.name, block.address, translate(block.alignment), block.position) // no use for other attributes yet?
val irBlock = IRBlock(block.name, block.address, block.library, block.forceOutput, translate(block.alignment), block.position) // no use for other attributes yet?
for (child in block.children) {
when(child) {
is PtNop -> { /* nothing */ }

View File

@ -1,9 +1,28 @@
package prog8.codegen.intermediate
import prog8.code.core.IErrorReporter
import prog8.intermediate.*
internal class IRPeepholeOptimizer(private val irprog: IRProgram) {
fun optimizeOnlyJoinChunks() {
class IRPeepholeOptimizer(private val irprog: IRProgram) {
fun optimize(optimizationsEnabled: Boolean, errors: IErrorReporter) {
if(!optimizationsEnabled)
return optimizeOnlyJoinChunks()
peepholeOptimize()
val remover = IRUnusedCodeRemover(irprog, errors)
var totalRemovals = 0
do {
val numRemoved = remover.optimize()
totalRemovals += numRemoved
} while(numRemoved>0 && errors.noErrors())
errors.report()
if(totalRemovals>0) {
irprog.linkChunks() // re-link again.
}
}
private fun optimizeOnlyJoinChunks() {
irprog.blocks.asSequence().flatMap { it.children.filterIsInstance<IRSubroutine>() }.forEach { sub ->
removeEmptyChunks(sub)
joinChunks(sub)
@ -11,7 +30,7 @@ internal class IRPeepholeOptimizer(private val irprog: IRProgram) {
irprog.linkChunks() // re-link
}
fun optimize() {
private fun peepholeOptimize() {
irprog.blocks.asSequence().flatMap { it.children.filterIsInstance<IRSubroutine>() }.forEach { sub ->
removeEmptyChunks(sub)
joinChunks(sub)
@ -36,7 +55,7 @@ internal class IRPeepholeOptimizer(private val irprog: IRProgram) {
removeEmptyChunks(sub)
}
irprog.linkChunks() // re-link
irprog.linkChunks() // re-link
}
private fun removeEmptyChunks(sub: IRSubroutine) {

View File

@ -5,14 +5,27 @@ import prog8.code.core.SourceCode.Companion.libraryFilePrefix
import prog8.intermediate.*
internal class IRUnusedCodeRemover(
class IRUnusedCodeRemover(
private val irprog: IRProgram,
private val symbolTable: IRSymbolTable,
private val errors: IErrorReporter
) {
fun optimize(): Int {
val allLabeledChunks = mutableMapOf<String, IRCodeChunkBase>()
var numRemoved = removeUnusedSubroutines() + removeUnusedAsmSubroutines()
// remove empty blocks
irprog.blocks.reversed().forEach { block ->
if(block.isEmpty()) {
irprog.blocks.remove(block)
irprog.st.removeTree(block.label)
numRemoved++
}
}
return numRemoved
}
private fun removeUnusedSubroutines(): Int {
val allLabeledChunks = mutableMapOf<String, IRCodeChunkBase>()
irprog.blocks.asSequence().flatMap { it.children.filterIsInstance<IRSubroutine>() }.forEach { sub ->
sub.chunks.forEach { chunk ->
chunk.label?.let { allLabeledChunks[it] = chunk }
@ -20,8 +33,6 @@ internal class IRUnusedCodeRemover(
}
var numRemoved = removeSimpleUnlinked(allLabeledChunks) + removeUnreachable(allLabeledChunks)
// remove empty subs
irprog.blocks.forEach { block ->
block.children.filterIsInstance<IRSubroutine>().reversed().forEach { sub ->
if(sub.isEmpty()) {
@ -29,24 +40,82 @@ internal class IRUnusedCodeRemover(
errors.warn("unused subroutine ${sub.label}", sub.position)
}
block.children.remove(sub)
symbolTable.removeTree(sub.label)
irprog.st.removeTree(sub.label)
numRemoved++
}
}
}
// remove empty blocks
irprog.blocks.reversed().forEach { block ->
if(block.isEmpty()) {
irprog.blocks.remove(block)
symbolTable.removeTree(block.label)
numRemoved++
return numRemoved
}
private fun removeUnusedAsmSubroutines(): Int {
val allLabeledAsmsubs = irprog.blocks.asSequence().flatMap { it.children.filterIsInstance<IRAsmSubroutine>() }
.associateBy { it.label }
var numRemoved = removeSimpleUnlinkedAsmsubs(allLabeledAsmsubs)
irprog.blocks.forEach { block ->
block.children.filterIsInstance<IRAsmSubroutine>().reversed().forEach { sub ->
if(sub.isEmpty()) {
if(!sub.position.file.startsWith(libraryFilePrefix)) {
errors.warn("unused asmsubroutine ${sub.label}", sub.position)
}
block.children.remove(sub)
irprog.st.removeTree(sub.label)
numRemoved++
}
}
}
return numRemoved
}
private fun removeSimpleUnlinkedAsmsubs(allSubs: Map<String, IRAsmSubroutine>): Int {
val linkedAsmSubs = mutableSetOf<IRAsmSubroutine>()
// TODO: asmsubs in library modules are never removed, we can't really tell here if they're actually being called or not...
// check if asmsub is called from another asmsub
irprog.blocks.asSequence().forEach { block ->
block.children.filterIsInstance<IRAsmSubroutine>().forEach { sub ->
require(sub.asmChunk.next == null) { "asmsubs won't be pointing to their successor, otherwise we should do more work here" }
if (block.forceOutput || block.library)
linkedAsmSubs += sub
if (sub.asmChunk.isNotEmpty()) {
allSubs.forEach { (label, asmsub) ->
if (sub.asmChunk.assembly.contains(label))
linkedAsmSubs += asmsub
}
}
}
}
// check if asmsub is linked or called from another regular subroutine
irprog.blocks.asSequence().flatMap { it.children.filterIsInstance<IRSubroutine>() }.forEach { sub ->
sub.chunks.forEach { chunk ->
chunk.instructions.forEach {
it.labelSymbol?.let { label -> allSubs[label]?.let { cc -> linkedAsmSubs += cc } }
// note: branchTarget can't yet point to another IRAsmSubroutine, so do nothing when it's set
}
}
}
return removeUnlinkedAsmsubs(linkedAsmSubs)
}
private fun removeUnlinkedAsmsubs(linkedAsmSubs: Set<IRAsmSubroutine>): Int {
var numRemoved = 0
irprog.blocks.asSequence().forEach { block ->
block.children.withIndex().reversed().forEach { (index, child) ->
if(child is IRAsmSubroutine && child !in linkedAsmSubs) {
block.children.removeAt(index)
numRemoved++
}
}
}
return numRemoved
}
private fun removeUnreachable(allLabeledChunks: MutableMap<String, IRCodeChunkBase>): Int {
val entrypointSub = irprog.blocks.single { it.label=="main" }.children.single { it is IRSubroutine && it.label=="main.start" }
val reachable = mutableSetOf((entrypointSub as IRSubroutine).chunks.first())
@ -98,7 +167,7 @@ internal class IRUnusedCodeRemover(
}
private fun removeUnlinkedChunks(
linkedChunks: MutableSet<IRCodeChunkBase>
linkedChunks: Set<IRCodeChunkBase>
): Int {
var numRemoved = 0
irprog.blocks.asSequence().flatMap { it.children.filterIsInstance<IRSubroutine>() }.forEach { sub ->

View File

@ -8,7 +8,7 @@ import prog8.intermediate.*
class TestIRPeepholeOpt: FunSpec({
fun makeIRProgram(chunks: List<IRCodeChunkBase>): IRProgram {
require(chunks.first().label=="main.start")
val block = IRBlock("main", null, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val block = IRBlock("main", null, false, false, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val sub = IRSubroutine("main.start", emptyList(), null, Position.DUMMY)
chunks.forEach { sub += it }
block += sub
@ -46,7 +46,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 3
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
irProg.chunks().single().instructions.size shouldBe 1
}
@ -65,7 +65,7 @@ class TestIRPeepholeOpt: FunSpec({
irProg.chunks().size shouldBe 4
irProg.chunks().flatMap { it.instructions }.size shouldBe 5
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
irProg.chunks().size shouldBe 4
irProg.chunks()[0].label shouldBe "main.start"
irProg.chunks()[1].label shouldBe "label"
@ -92,7 +92,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 6
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 1
instr[0].opcode shouldBe Opcode.CLC
@ -107,7 +107,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 4
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 1
instr[0].opcode shouldBe Opcode.LOADR
@ -130,7 +130,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 10
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
irProg.chunks().single().instructions.size shouldBe 4
}
@ -141,7 +141,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 2
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 2
instr[0].opcode shouldBe Opcode.INC
@ -161,7 +161,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 8
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
irProg.chunks().single().instructions.size shouldBe 4
}
@ -174,7 +174,7 @@ class TestIRPeepholeOpt: FunSpec({
))
irProg.chunks().single().instructions.size shouldBe 4
val opt = IRPeepholeOptimizer(irProg)
opt.optimize()
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 4
instr[0].opcode shouldBe Opcode.LOAD

View File

@ -1 +1 @@
8.12
8.13-dev

View File

@ -33,7 +33,7 @@ class TestLaunchEmu: FunSpec({
<INITGLOBALS>
</INITGLOBALS>
<BLOCK NAME="main" ADDRESS="" ALIGN="NONE" POS="[unittest: line 42 col 1-9]">
<BLOCK NAME="main" ADDRESS="" LIBRARY="false" FORCEOUTPUT="false" ALIGN="NONE" POS="[unittest: line 42 col 1-9]">
</BLOCK>
</PROGRAM>
""")

View File

@ -8,36 +8,9 @@ For next minor release
...
For 9.0 major changes
^^^^^^^^^^^^^^^^^^^^^
- rename builtin function sqrt16 to just sqrt
- copy (not move) the CBM kernal romsubs to a new 'cbm' block so programs on c128 and cx16 can also
simply refer to cbm.CHROUT rather than c64.CHROUT which looks a bit silly on the non-c64 cbm systems.
we keep the old definitions intact because of backwards compatibility reasons.
- try to reintroduce builtin functions max/maxw/min/minw that take 2 args and return the largest/smallest of them.
This is a major change because it will likely break existing code that is now using min and max as variable names.
Also add optimization that changes the word variant to byte variant if the operands are bytes.
- 6502 codegen: see if we can let for loops skip the loop if startvar>endvar, without adding a lot of code size/duplicating the loop condition.
It is documented behavior to now loop 'around' $00 but it's too easy to forget about!
Lot of work because of so many special cases in ForLoopsAsmgen.....
(vm codegen already behaves like this!)
- ?? get rid of the disknumber parameter everywhere in diskio, make it a configurable variable that defaults to 8.
the large majority of users will only deal with a single disk drive so why not make it easier for them.
But see complaint on github https://github.com/irmen/prog8/issues/106
- duplicate diskio for cx16 (get rid of cx16diskio, just copy diskio and tweak everything) + documentation
- get f_seek_w working like in the BASIC program - this needs the changes to diskio.f_open to use suffixes ,p,m
- add special (u)word array type (or modifier such as @fast? ) that puts the array into memory as 2 separate byte-arrays 1 for LSB 1 for MSB -> allows for word arrays of length 256 and faster indexing
this is an enormous amout of work, if this type is to be treated equally as existing (u)word , because all expression / lookup / assignment routines need to know about the distinction....
So maybe only allow the bare essentials? (store, get, bitwise operations?)
- Some more support for (64tass) SEGMENTS ?
- (What, how, isn't current BSS support enough?)
- Add a mechanism to allocate variables into golden ram (or segments really) (see GoldenRam class)
- maybe treat block "golden" in a special way: can only contain vars, every var will be allocated in the Golden ram area?
- maybe or may not needed: the variables can NOT have initialization values, they will all be set to zero on startup (simple memset)
just initialize them yourself in start() if you need a non-zero value .
- OR.... do all this automatically if 'golden' is enabled as a compiler option? So compiler allocates in ZP first, then Golden Ram, then regular ram
- OR.... make all this more generic and use some %segment option to create real segments for 64tass?
- (need separate step in codegen and IR to write the "golden" variables)
For 9.0 major changes are being made in the "version_9" branch. Look there.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Need help with

View File

@ -4,25 +4,16 @@
main {
sub start() {
byte v1s = 22
byte v2s = -99
word ww
txt.print_w(minsb()) ; TODO WRONG RESULT!
txt.spc()
ww = minsb()
txt.print_w(ww)
txt.spc()
txt.print_b(minsb())
txt.spc()
v2s = minsb()
txt.print_w(v2s)
sub minsb() -> byte {
cx16.r0++
return v2s
}
txt.print("hello")
; foobar()
}
asmsub foobar() {
%asm {{
nop
rts
}}
}
}

View File

@ -336,6 +336,8 @@ class IRFileReader {
val block = IRBlock(
attrs.getValue("NAME"),
if(attrs.getValue("ADDRESS")=="") null else parseIRValue(attrs.getValue("ADDRESS")).toUInt(),
if(attrs.getValue("LIBRARY")=="") false else attrs.getValue("LIBRARY").toBoolean(),
if(attrs.getValue("FORCEOUTPUT")=="") false else attrs.getValue("FORCEOUTPUT").toBoolean(),
IRBlock.BlockAlignment.valueOf(attrs.getValue("ALIGN")),
parsePosition(attrs.getValue("POS")))
skipText(reader)

View File

@ -57,6 +57,8 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
xml.writeStartElement("BLOCK")
xml.writeAttribute("NAME", block.label)
xml.writeAttribute("ADDRESS", block.address?.toHex() ?: "")
xml.writeAttribute("LIBRARY", block.library.toString())
xml.writeAttribute("FORCEOUTPUT", block.forceOutput.toString())
xml.writeAttribute("ALIGN", block.alignment.toString())
xml.writeAttribute("POS", block.position.toString())
xml.writeCharacters("\n")

View File

@ -23,7 +23,6 @@ Currently ther is NO support for 24 or 32 bits integers.
There is no distinction between signed and unsigned integers.
Instead, a different instruction is used if a distinction should be made (for example div and divs).
Floating point operations are just 'f' typed regular instructions, however there are a few unique fp conversion instructions.
Instructions taking more than 1 register cannot take the same register multiple times! (to avoid confusing different datatypes)
LOAD/STORE
@ -65,29 +64,29 @@ BRANCHING and CONDITIONALS
--------------------------
All have type b or w except the branches that only check status bits.
bstcc address - branch to location if Status bit Carry is Clear
bstcs address - branch to location if Status bit Carry is Set
bstcc address - branch to location if Status bit Carry is clear
bstcs address - branch to location if Status bit Carry is set
bstne address - branch to location if Status bit Zero is clear
bsteq address - branch to location if Status bit Zero is set
bstne address - branch to location if Status bit Zero is not set
bstneg address - branch to location if Status bit Negative is not set
bstpos address - branch to location if Status bit Negative is not set
bstvc address - branch to location if Status bit Overflow is not set
bstvs address - branch to location if Status bit Overflow is not set
bstpos address - branch to location if Status bit Negative is clear
bstneg address - branch to location if Status bit Negative is set
bstvc address - branch to location if Status bit Overflow is clear
bstvs address - branch to location if Status bit Overflow is set
beqr reg1, reg2, address - jump to location in program given by location, if reg1 == reg2
beq reg1, value, address - jump to location in program given by location, if reg1 == immediate value
bner reg1, reg2, address - jump to location in program given by location, if reg1 != reg2
bne reg1, value, address - jump to location in program given by location, if reg1 != immediate value
bgtr reg1, reg2, address - jump to location in program given by location, if reg1 > reg2 (unsigned)
bgt reg1, value, address - jump to location in program given by location, if reg1 > immediate value (unsigned)
blt reg1, value, address - jump to location in program given by location, if reg1 < immediate value (unsigned)
bgtsr reg1, reg2, address - jump to location in program given by location, if reg1 > reg2 (signed)
bgts reg1, value, address - jump to location in program given by location, if reg1 > immediate value (signed)
bgtr reg1, reg2, address - jump to location in program given by location, if reg1 > reg2 (unsigned)
bgtsr reg1, reg2, address - jump to location in program given by location, if reg1 > reg2 (signed)
blt reg1, value, address - jump to location in program given by location, if reg1 < immediate value (unsigned)
blts reg1, value, address - jump to location in program given by location, if reg1 < immediate value (signed)
bger reg1, reg2, address - jump to location in program given by location, if reg1 >= reg2 (unsigned)
bge reg1, value, address - jump to location in program given by location, if reg1 >= immediate value (unsigned)
ble reg1, value, address - jump to location in program given by location, if reg1 <= immediate value (unsigned)
bgesr reg1, reg2, address - jump to location in program given by location, if reg1 >= reg2 (signed)
bges reg1, value, address - jump to location in program given by location, if reg1 >= immediate value (signed)
bger reg1, reg2, address - jump to location in program given by location, if reg1 >= reg2 (unsigned)
bgesr reg1, reg2, address - jump to location in program given by location, if reg1 >= reg2 (signed)
ble reg1, value, address - jump to location in program given by location, if reg1 <= immediate value (unsigned)
bles reg1, value, address - jump to location in program given by location, if reg1 <= immediate value (signed)
( NOTE: there are no bltr/bler instructions because these are equivalent to bgtr/bger with the register operands swapped around.)
sz reg1, reg2 - set reg1=1 if reg2==0, otherwise set reg1=0
@ -727,18 +726,6 @@ data class IRInstruction(
fpReg1direction = format.fpReg1
fpReg2direction = format.fpReg2
if(opcode in setOf(Opcode.BEQR, Opcode.BNER,
Opcode.BGTR, Opcode.BGTSR,
Opcode.BGER, Opcode.BGESR,
Opcode.SEQ, Opcode.SNE, Opcode.SLT, Opcode.SLTS,
Opcode.SGT, Opcode.SGTS, Opcode.SLE, Opcode.SLES,
Opcode.SGE, Opcode.SGES)) {
if(type==IRDataType.FLOAT)
require(fpReg1!=fpReg2) {"$opcode: fpReg1 and fpReg2 should be different"}
else
require(reg1!=reg2) {"$opcode: reg1 and reg2 should be different"}
}
if(opcode==Opcode.SYSCALL) {
require(immediate!=null) {
"syscall needs immediate integer for the syscall number"

View File

@ -240,6 +240,8 @@ class IRProgram(val name: String,
class IRBlock(
val label: String,
val address: UInt?,
val library: Boolean,
val forceOutput: Boolean,
val alignment: BlockAlignment,
val position: Position
) {

View File

@ -75,7 +75,7 @@ load.b r1,42
</CODE>
</INITGLOBALS>
<BLOCK NAME="main" ADDRESS="" ALIGN="NONE" POS="[examples/test.p8: line 2 col 2-5]">
<BLOCK NAME="main" ADDRESS="" LIBRARY="false" FORCEOUTPUT="false" ALIGN="NONE" POS="[examples/test.p8: line 2 col 2-5]">
<SUB NAME="main.start" RETURNTYPE="" POS="[examples/test.p8: line 4 col 6-8]">
<PARAMS>
</PARAMS>
@ -85,7 +85,7 @@ return
</SUB>
</BLOCK>
<BLOCK NAME="sys" ADDRESS="" ALIGN="NONE" POS="[library:/prog8lib/virtual/syslib.p8: line 3 col 2-4]">
<BLOCK NAME="sys" ADDRESS="" LIBRARY="false" FORCEOUTPUT="false" ALIGN="NONE" POS="[library:/prog8lib/virtual/syslib.p8: line 3 col 2-4]">
<SUB NAME="sys.wait" RETURNTYPE="" POS="[library:/prog8lib/virtual/syslib.p8: line 15 col 6-8]">
<PARAMS>
uword sys.wait.jiffies

View File

@ -43,7 +43,7 @@ class TestVm: FunSpec( {
test("vm execution: modify memory") {
val program = IRProgram("test", IRSymbolTable(null), getTestOptions(), VMTarget())
val block = IRBlock("testmain", null, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val block = IRBlock("testmain", null, false, false, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val startSub = IRSubroutine("testmain.testsub", emptyList(), null, Position.DUMMY)
val code = IRCodeChunk(startSub.label, null)
code += IRInstruction(Opcode.NOP)
@ -72,7 +72,7 @@ class TestVm: FunSpec( {
test("vm asmbinary not supported") {
val program = IRProgram("test", IRSymbolTable(null), getTestOptions(), VMTarget())
val block = IRBlock("testmain", null, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val block = IRBlock("testmain", null, false, false, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val startSub = IRSubroutine("testmain.testsub", emptyList(), null, Position.DUMMY)
val code = IRCodeChunk(startSub.label, null)
code += IRInstruction(Opcode.BINARYDATA, binaryData = listOf(1u,2u,3u))
@ -88,7 +88,7 @@ class TestVm: FunSpec( {
test("asmsub not supported in vm even with IR") {
val program = IRProgram("test", IRSymbolTable(null), getTestOptions(), VMTarget())
val block = IRBlock("main", null, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val block = IRBlock("main", null, false, false, IRBlock.BlockAlignment.NONE, Position.DUMMY)
val startSub = IRAsmSubroutine(
"main.asmstart",
0x2000u,
@ -129,7 +129,7 @@ class TestVm: FunSpec( {
<INITGLOBALS>
</INITGLOBALS>
<BLOCK NAME="main" ADDRESS="" ALIGN="NONE" POS="[unittest: line 42 col 1-9]">
<BLOCK NAME="main" ADDRESS="" LIBRARY="false" FORCEOUTPUT="false" ALIGN="NONE" POS="[unittest: line 42 col 1-9]">
</BLOCK>
</PROGRAM>
"""