mirror of
https://github.com/irmen/prog8.git
synced 2024-11-25 19:31:36 +00:00
more rewrite variable allocation
This commit is contained in:
parent
9acc2f92d1
commit
7d67005709
@ -218,8 +218,6 @@ internal class ProgramGen(
|
||||
}
|
||||
|
||||
asmgen.out("")
|
||||
asmgen.outputSourceLine(sub)
|
||||
|
||||
|
||||
if(sub.isAsmSubroutine) {
|
||||
if(sub.asmAddress!=null)
|
||||
|
@ -7,6 +7,7 @@ import prog8.ast.base.DataType
|
||||
import prog8.ast.base.VarDeclType
|
||||
import prog8.ast.expressions.StringLiteralValue
|
||||
import prog8.ast.statements.VarDecl
|
||||
import prog8.ast.statements.VarDeclOrigin
|
||||
import prog8.ast.statements.ZeropageWish
|
||||
import prog8.compilerinterface.*
|
||||
|
||||
@ -19,10 +20,12 @@ internal class VariableAllocation(val vars: IVariablesAndConsts, val errors: IEr
|
||||
return
|
||||
|
||||
val zeropage = options.compTarget.machine.zeropage
|
||||
// val allVariables = vars.blockVars.asSequence().flatMap { it.value }.map {it.origVar to it.origVar.scopedName} +
|
||||
// vars.subroutineVars.asSequence().flatMap { it.value }.map {it.origVar to it.origVar.scopedName}
|
||||
val allVariables = callGraph.allIdentifiers.asSequence()
|
||||
.map { it.value }
|
||||
.filterIsInstance<VarDecl>()
|
||||
.filter { it.type== VarDeclType.VAR }
|
||||
.filter { it.type== VarDeclType.VAR && it.origin!=VarDeclOrigin.SUBROUTINEPARAM }
|
||||
.toSet()
|
||||
.map { it to it.scopedName }
|
||||
val varsRequiringZp = allVariables.filter { it.first.zeropage== ZeropageWish.REQUIRE_ZEROPAGE }
|
||||
@ -30,6 +33,33 @@ internal class VariableAllocation(val vars: IVariablesAndConsts, val errors: IEr
|
||||
.filter { it.first.zeropage== ZeropageWish.PREFER_ZEROPAGE }
|
||||
.sortedBy { options.compTarget.memorySize(it.first.datatype) } // allocate the smallest DT first
|
||||
|
||||
|
||||
val allVarsFromCallgraph = allVariables.map { it.first.name to it.first.position }.toSet()
|
||||
val altAllVars = (vars.blockVars.flatMap { it.value }.map {it.name to it.position} + vars.subroutineVars.flatMap { it.value }.map {it.name to it.position}).toSet()
|
||||
val extraVarsInCallgraph = allVarsFromCallgraph - altAllVars
|
||||
val extraVarsInNew = altAllVars - allVarsFromCallgraph
|
||||
|
||||
if(extraVarsInCallgraph.any() || extraVarsInNew.any()) {
|
||||
println("EXTRA VARS IN CALLGRAPH: ${extraVarsInCallgraph.size}")
|
||||
extraVarsInCallgraph.forEach {
|
||||
println(" $it")
|
||||
}
|
||||
println("EXTRA VARS IN VARIABLESOBJ: ${extraVarsInNew.size}")
|
||||
extraVarsInNew.forEach {
|
||||
println(" $it")
|
||||
}
|
||||
// TODO("fix differences")
|
||||
}
|
||||
|
||||
val altTotalNumVars = vars.blockVars.flatMap { it.value }.size + vars.subroutineVars.flatMap { it.value }.size
|
||||
val altVarsRequiringZpBlocks = vars.blockVars.flatMap { it.value }.filter { it.zp==ZeropageWish.REQUIRE_ZEROPAGE }
|
||||
val altVarsRequiringZpSubs = vars.subroutineVars.flatMap { it.value }.filter { it.zp==ZeropageWish.REQUIRE_ZEROPAGE }
|
||||
val altTotalZpVars = altVarsRequiringZpBlocks.size + altVarsRequiringZpSubs.size
|
||||
val altVarsPreferringZpBlocks = vars.blockVars.flatMap { it.value }.filter { it.zp==ZeropageWish.PREFER_ZEROPAGE }
|
||||
val altVarsPreferringZpSubs = vars.subroutineVars.flatMap { it.value }.filter { it.zp==ZeropageWish.PREFER_ZEROPAGE }
|
||||
val altTotalPrefZpVars = altVarsPreferringZpBlocks.size + altVarsPreferringZpSubs.size
|
||||
|
||||
|
||||
for ((vardecl, scopedname) in varsRequiringZp) {
|
||||
val numElements: Int? = when(vardecl.datatype) {
|
||||
DataType.STR -> {
|
||||
|
@ -89,7 +89,11 @@ class StatementOptimizer(private val program: Program,
|
||||
mutableListOf(NumericLiteralValue(DataType.UBYTE, firstCharEncoded.toDouble(), pos)),
|
||||
functionCallStatement.void, pos
|
||||
)
|
||||
return listOf(IAstModification.ReplaceNode(functionCallStatement, chrout, parent))
|
||||
val stringDecl = string.parent as VarDecl
|
||||
return listOf(
|
||||
IAstModification.ReplaceNode(functionCallStatement, chrout, parent),
|
||||
IAstModification.Remove(stringDecl, stringDecl.parent as IStatementContainer)
|
||||
)
|
||||
} else if (string.value.length == 2) {
|
||||
val firstTwoCharsEncoded = compTarget.encodeString(string.value.take(2), string.encoding)
|
||||
val chrout1 = FunctionCallStatement(
|
||||
@ -102,9 +106,11 @@ class StatementOptimizer(private val program: Program,
|
||||
mutableListOf(NumericLiteralValue(DataType.UBYTE, firstTwoCharsEncoded[1].toDouble(), pos)),
|
||||
functionCallStatement.void, pos
|
||||
)
|
||||
val stringDecl = string.parent as VarDecl
|
||||
return listOf(
|
||||
IAstModification.InsertBefore(functionCallStatement, chrout1, parent as IStatementContainer),
|
||||
IAstModification.ReplaceNode(functionCallStatement, chrout2, parent)
|
||||
IAstModification.ReplaceNode(functionCallStatement, chrout2, parent),
|
||||
IAstModification.Remove(stringDecl, stringDecl.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ class UnusedCodeRemover(private val program: Program,
|
||||
if(callgraph.unused(block)) {
|
||||
if(block.statements.any{ it !is VarDecl || it.type==VarDeclType.VAR})
|
||||
errors.warn("removing unused block '${block.name}'", block.position)
|
||||
program.removeInternedStringsFromRemovedBlock(block)
|
||||
return listOf(IAstModification.Remove(block, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
@ -92,6 +93,7 @@ class UnusedCodeRemover(private val program: Program,
|
||||
}
|
||||
if(!subroutine.definingModule.isLibrary)
|
||||
errors.warn("removing unused subroutine '${subroutine.name}'", subroutine.position)
|
||||
program.removeInternedStringsFromRemovedSubroutine(subroutine)
|
||||
return listOf(IAstModification.Remove(subroutine, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
@ -101,11 +103,13 @@ class UnusedCodeRemover(private val program: Program,
|
||||
|
||||
override fun after(decl: VarDecl, parent: Node): Iterable<IAstModification> {
|
||||
if(decl.type==VarDeclType.VAR) {
|
||||
val forceOutput = "force_output" in decl.definingBlock.options()
|
||||
if (!forceOutput && decl.origin==VarDeclOrigin.USERCODE && !decl.sharedWithAsm && !decl.definingBlock.isInLibrary) {
|
||||
val block = decl.definingBlock
|
||||
val forceOutput = "force_output" in block.options()
|
||||
if (!forceOutput && decl.origin==VarDeclOrigin.USERCODE && !decl.sharedWithAsm && !block.isInLibrary) { // TODO remove check on block.isInLibrary, but this now breaks some programs
|
||||
val usages = callgraph.usages(decl)
|
||||
if (usages.isEmpty()) {
|
||||
errors.warn("removing unused variable '${decl.name}'", decl.position)
|
||||
if(!decl.definingModule.isLibrary)
|
||||
errors.warn("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(IAstModification.Remove(decl, parent as IStatementContainer))
|
||||
}
|
||||
else {
|
||||
@ -113,8 +117,9 @@ class UnusedCodeRemover(private val program: Program,
|
||||
val singleUse = usages[0].parent
|
||||
if(singleUse is AssignTarget) {
|
||||
val assignment = singleUse.parent as Assignment
|
||||
if(assignment.value !is IFunctionCall) {
|
||||
errors.warn("removing unused variable '${decl.name}'", decl.position)
|
||||
if(assignment.origin==AssignmentOrigin.VARINIT) {
|
||||
if(!decl.definingModule.isLibrary)
|
||||
errors.warn("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(
|
||||
IAstModification.Remove(decl, parent as IStatementContainer),
|
||||
IAstModification.Remove(assignment, assignment.parent as IStatementContainer)
|
||||
|
@ -7,13 +7,13 @@ prog8_lib {
|
||||
%asminclude "library:prog8_funcs.asm"
|
||||
|
||||
; to store intermediary expression results for return values:
|
||||
; NOTE: these variables are used in the StatementReorderer and StatementOptimizer
|
||||
uword @zp retval_interm_uw
|
||||
word @zp retval_interm_w
|
||||
ubyte @zp retval_interm_ub
|
||||
byte @zp retval_interm_b
|
||||
word retval_interm_w2
|
||||
byte retval_interm_b2
|
||||
; NOTE: these variables can be used in the StatementReorderer and StatementOptimizer
|
||||
uword @zp @shared retval_interm_uw
|
||||
word @zp @shared retval_interm_w
|
||||
ubyte @zp @shared retval_interm_ub
|
||||
byte @zp @shared retval_interm_b
|
||||
word @shared retval_interm_w2
|
||||
byte @shared retval_interm_b2
|
||||
|
||||
; prog8 "hooks" to be able to access the temporary scratch variables
|
||||
; YOU SHOULD NOT USE THESE IN USER CODE - THESE ARE MEANT FOR INTERNAL COMPILER USE
|
||||
|
@ -71,19 +71,21 @@ internal class BeforeAsmAstChanger(val program: Program, private val options: Co
|
||||
val scope=decl.definingScope
|
||||
when (decl.type) {
|
||||
VarDeclType.VAR -> {
|
||||
when(scope) {
|
||||
is Block -> {
|
||||
val decls = allBlockVars[scope] ?: mutableSetOf()
|
||||
decls.add(decl)
|
||||
allBlockVars[scope] = decls
|
||||
}
|
||||
is Subroutine -> {
|
||||
val decls = allSubroutineVars[scope] ?: mutableSetOf()
|
||||
decls.add(decl)
|
||||
allSubroutineVars[scope] = decls
|
||||
}
|
||||
else -> {
|
||||
throw FatalAstException("var can only occur in subroutine or block scope")
|
||||
if(decl.origin!=VarDeclOrigin.SUBROUTINEPARAM) {
|
||||
when (scope) {
|
||||
is Block -> {
|
||||
val decls = allBlockVars[scope] ?: mutableSetOf()
|
||||
decls.add(decl)
|
||||
allBlockVars[scope] = decls
|
||||
}
|
||||
is Subroutine -> {
|
||||
val decls = allSubroutineVars[scope] ?: mutableSetOf()
|
||||
decls.add(decl)
|
||||
allSubroutineVars[scope] = decls
|
||||
}
|
||||
else -> {
|
||||
throw FatalAstException("var can only occur in subroutine or block scope")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -469,7 +471,7 @@ internal class VariablesAndConsts (
|
||||
astBlockVars.forEach { (block, decls) ->
|
||||
val vars = bv.getValue(block)
|
||||
vars.addAll(decls.map {
|
||||
IVariablesAndConsts.StaticBlockVariable(it.datatype, it.name, it.value, it.position, it)
|
||||
IVariablesAndConsts.StaticBlockVariable(it.datatype, it.name, it.value, it.zeropage, it.position, it)
|
||||
})
|
||||
}
|
||||
astBlockConsts.forEach { (block, decls) ->
|
||||
@ -497,7 +499,7 @@ internal class VariablesAndConsts (
|
||||
astSubroutineVars.forEach { (sub, decls) ->
|
||||
val vars = sv.getValue(sub)
|
||||
vars.addAll(decls.map {
|
||||
IVariablesAndConsts.StaticSubroutineVariable(it.datatype, it.name, it.position, it)
|
||||
IVariablesAndConsts.StaticSubroutineVariable(it.datatype, it.name, it.zeropage, it.position, it)
|
||||
})
|
||||
}
|
||||
astSubroutineConsts.forEach { (sub, decls) ->
|
||||
|
@ -36,7 +36,7 @@ main {
|
||||
}
|
||||
withClue("module order in parse tree") {
|
||||
moduleNames.drop(1) shouldBe listOf(
|
||||
"prog8_interned_strings",
|
||||
internedStringsModuleName,
|
||||
"textio",
|
||||
"syslib",
|
||||
"conv",
|
||||
|
@ -79,13 +79,13 @@ class TestAsmGenSymbols: StringSpec({
|
||||
override val subroutineMemvars: Map<Subroutine, Set<IVariablesAndConsts.MemoryMappedVariable>>
|
||||
init {
|
||||
blockVars = mutableMapOf()
|
||||
blockVars[block] = mutableSetOf(IVariablesAndConsts.StaticBlockVariable(varInBlock.datatype, varInBlock.name, varInBlock.value, varInBlock.position, varInBlock))
|
||||
blockVars[block] = mutableSetOf(IVariablesAndConsts.StaticBlockVariable(varInBlock.datatype, varInBlock.name, varInBlock.value, varInBlock.zeropage, varInBlock.position, varInBlock))
|
||||
blockConsts = mutableMapOf()
|
||||
blockMemvars = mutableMapOf()
|
||||
subroutineVars = mutableMapOf()
|
||||
subroutineVars[subroutine] = mutableSetOf(
|
||||
IVariablesAndConsts.StaticSubroutineVariable(varInSub.datatype, varInSub.name, varInSub.position, varInSub),
|
||||
IVariablesAndConsts.StaticSubroutineVariable(var2InSub.datatype, var2InSub.name, var2InSub.position, var2InSub)
|
||||
IVariablesAndConsts.StaticSubroutineVariable(varInSub.datatype, varInSub.name, var2InSub.zeropage, varInSub.position, varInSub),
|
||||
IVariablesAndConsts.StaticSubroutineVariable(var2InSub.datatype, var2InSub.name, var2InSub.zeropage, var2InSub.position, var2InSub)
|
||||
)
|
||||
subroutineConsts = mutableMapOf()
|
||||
subroutineMemvars = mutableMapOf()
|
||||
|
@ -4,9 +4,10 @@ import prog8.ast.base.DataType
|
||||
import prog8.ast.base.FatalAstException
|
||||
import prog8.ast.base.Position
|
||||
import prog8.ast.base.VarDeclType
|
||||
import prog8.ast.expressions.IdentifierReference
|
||||
import prog8.ast.expressions.StringLiteralValue
|
||||
import prog8.ast.statements.*
|
||||
import prog8.compilerinterface.Encoding
|
||||
import prog8.ast.walk.IAstVisitor
|
||||
import prog8.compilerinterface.IMemSizer
|
||||
import prog8.compilerinterface.IStringEncoding
|
||||
import prog8.parser.SourceCode
|
||||
@ -74,7 +75,8 @@ class Program(val name: String,
|
||||
get() = toplevelModule.loadAddress
|
||||
|
||||
var actualLoadAddress = 0u
|
||||
private val internedStringsUnique = mutableMapOf<Pair<String, Encoding>, List<String>>()
|
||||
|
||||
private val internedStringsReferenceCounts = mutableMapOf<VarDecl, Int>()
|
||||
|
||||
fun internString(string: StringLiteralValue): List<String> {
|
||||
// Move a string literal into the internal, deduplicated, string pool
|
||||
@ -85,10 +87,11 @@ class Program(val name: String,
|
||||
throw FatalAstException("cannot intern a string literal that's part of a vardecl")
|
||||
}
|
||||
|
||||
fun getScopedName(string: StringLiteralValue): List<String> {
|
||||
val internedStringsBlock = modules
|
||||
.first { it.name == internedStringsModuleName }.statements
|
||||
.first { it is Block && it.name == internedStringsModuleName } as Block
|
||||
val internedStringsBlock = modules
|
||||
.first { it.name == internedStringsModuleName }.statements
|
||||
.first { it is Block && it.name == internedStringsModuleName } as Block
|
||||
|
||||
fun addNewInternedStringvar(string: StringLiteralValue): Pair<List<String>, VarDecl> {
|
||||
val varName = "string_${internedStringsBlock.statements.size}"
|
||||
val decl = VarDecl(
|
||||
VarDeclType.VAR, VarDeclOrigin.STRINGLITERAL, DataType.STR, ZeropageWish.NOT_IN_ZEROPAGE, null, varName, string,
|
||||
@ -96,16 +99,58 @@ class Program(val name: String,
|
||||
)
|
||||
internedStringsBlock.statements.add(decl)
|
||||
decl.linkParents(internedStringsBlock)
|
||||
return listOf(internedStringsModuleName, decl.name)
|
||||
return Pair(listOf(internedStringsModuleName, decl.name), decl)
|
||||
}
|
||||
|
||||
val key = Pair(string.value, string.encoding)
|
||||
val existing = internedStringsUnique[key]
|
||||
if (existing != null)
|
||||
return existing
|
||||
|
||||
val scopedName = getScopedName(string)
|
||||
internedStringsUnique[key] = scopedName
|
||||
return scopedName
|
||||
val existingDecl = internedStringsBlock.statements.singleOrNull {
|
||||
val declString = (it as VarDecl).value as StringLiteralValue
|
||||
declString.encoding == string.encoding && declString.value == string.value
|
||||
}
|
||||
return if (existingDecl != null) {
|
||||
existingDecl as VarDecl
|
||||
internedStringsReferenceCounts[existingDecl] = internedStringsReferenceCounts.getValue(existingDecl)+1
|
||||
existingDecl.scopedName
|
||||
}
|
||||
else {
|
||||
val (newName, newDecl) = addNewInternedStringvar(string)
|
||||
internedStringsReferenceCounts[newDecl] = 1
|
||||
newName
|
||||
}
|
||||
}
|
||||
|
||||
fun removeInternedStringsFromRemovedSubroutine(sub: Subroutine) {
|
||||
val s = StringSearch(this)
|
||||
sub.accept(s)
|
||||
s.removeStrings(modules)
|
||||
}
|
||||
|
||||
fun removeInternedStringsFromRemovedBlock(block: Block) {
|
||||
val s = StringSearch(this)
|
||||
block.accept(s)
|
||||
s.removeStrings(modules)
|
||||
}
|
||||
|
||||
private class StringSearch(val program: Program): IAstVisitor {
|
||||
val removals = mutableListOf<List<String>>()
|
||||
override fun visit(identifier: IdentifierReference) {
|
||||
if(identifier.wasStringLiteral(program))
|
||||
removals.add(identifier.nameInSource)
|
||||
}
|
||||
|
||||
fun removeStrings(modules: List<Module>) {
|
||||
if(removals.isNotEmpty()) {
|
||||
val internedStringsBlock = modules
|
||||
.first { it.name == internedStringsModuleName }.statements
|
||||
.first { it is Block && it.name == internedStringsModuleName } as Block
|
||||
removals.forEach { scopedname ->
|
||||
val decl = internedStringsBlock.statements.single { decl -> (decl as VarDecl).scopedName == scopedname } as VarDecl
|
||||
val numRefs = program.internedStringsReferenceCounts.getValue(decl) - 1
|
||||
program.internedStringsReferenceCounts[decl] = numRefs
|
||||
if(numRefs==0)
|
||||
internedStringsBlock.statements.remove(decl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import prog8.ast.expressions.Expression
|
||||
import prog8.ast.statements.Block
|
||||
import prog8.ast.statements.Subroutine
|
||||
import prog8.ast.statements.VarDecl
|
||||
import prog8.ast.statements.ZeropageWish
|
||||
|
||||
/**
|
||||
* Experimental attempt for:
|
||||
@ -16,8 +17,8 @@ interface IVariablesAndConsts {
|
||||
data class ConstantNumberSymbol(val type: DataType, val name: String, val value: Double, val position: Position)
|
||||
data class MemoryMappedVariable(val type: DataType, val name: String, val address: UInt, val position: Position)
|
||||
// TODO should get rid of origVar altogether in the following two:
|
||||
data class StaticBlockVariable(val type: DataType, val name: String, val initialValue: Expression?, val position: Position, val origVar: VarDecl)
|
||||
data class StaticSubroutineVariable(val type: DataType, val name: String, val position: Position, val origVar: VarDecl)
|
||||
data class StaticBlockVariable(val type: DataType, val name: String, val initialValue: Expression?, val zp: ZeropageWish, val position: Position, val origVar: VarDecl)
|
||||
data class StaticSubroutineVariable(val type: DataType, val name: String, val zp: ZeropageWish, val position: Position, val origVar: VarDecl)
|
||||
|
||||
fun dump(memsizer: IMemSizer)
|
||||
|
||||
|
@ -3,7 +3,8 @@ TODO
|
||||
|
||||
For next release
|
||||
^^^^^^^^^^^^^^^^
|
||||
...
|
||||
- (newvaralloc) UnusedCodeRemover after(decl: VarDecl): fix that vars defined in a library can also safely be removed if unused. Currently this breaks programs such as textelite (due to diskio.save().end_address ?)
|
||||
|
||||
|
||||
Need help with
|
||||
^^^^^^^^^^^^^^
|
||||
|
@ -2,42 +2,43 @@
|
||||
%zeropage basicsafe
|
||||
|
||||
main {
|
||||
ubyte mainglobal1 = 10
|
||||
ubyte mainglobal2 = 20
|
||||
ubyte mainglobal3 = 30
|
||||
ubyte mainglobal4 = 40
|
||||
ubyte @zp mainglobal1=10
|
||||
|
||||
sub start() {
|
||||
ubyte startval1 = 100
|
||||
ubyte startval2 = 110
|
||||
ubyte startval3 = 120
|
||||
ubyte startval4 = 130
|
||||
|
||||
txt.print_ub(mainglobal1)
|
||||
txt.nl()
|
||||
txt.print_ub(startval1)
|
||||
txt.nl()
|
||||
derp()
|
||||
derp()
|
||||
foobar()
|
||||
startval1++
|
||||
mainglobal1++
|
||||
start2()
|
||||
|
||||
sub start2() {
|
||||
uword @shared startval1 = 2002
|
||||
ubyte[2] @shared barr
|
||||
uword[2] @shared warr
|
||||
uword[] @shared warr2 = [1,2]
|
||||
}
|
||||
c64.SETNAM(1, "$")
|
||||
txt.print("1")
|
||||
txt.print("12")
|
||||
txt.print("test.")
|
||||
txt.print("test.")
|
||||
; foobar(2,1,1,1)
|
||||
; foobar2(2,1,1,1)
|
||||
}
|
||||
|
||||
asmsub derp() {
|
||||
sub unusedsubroutine() {
|
||||
c64.SETNAM(1, "$") ; TODO fix don't remove this interned string because referenced in start()
|
||||
txt.print("this string should be removed from the pool")
|
||||
txt.print("this string should be removed from the pool")
|
||||
txt.print("this string should be removed from the pool")
|
||||
}
|
||||
|
||||
sub foobar() {
|
||||
uword @shared startval1 = 2002
|
||||
uword @shared mainglobal1 = 2002
|
||||
txt.print("foobar\n")
|
||||
; asmsub foobar2(ubyte argument @A, uword a2 @R0, uword a3 @R1, uword a4 @R2) -> ubyte @A {
|
||||
; %asm {{
|
||||
; lda #0
|
||||
; rts
|
||||
; }}
|
||||
; }
|
||||
; sub foobar(ubyte argument, uword a2, uword a3, uword a4) -> ubyte {
|
||||
; argument++
|
||||
; return lsb(a2)
|
||||
; }
|
||||
}
|
||||
|
||||
foobar {
|
||||
str name = "don't remove this one"
|
||||
|
||||
sub unusedinfoobar() {
|
||||
name = "should be removed" ; TODO remove from interned strings because subroutine got removed
|
||||
txt.print(name)
|
||||
txt.print("should also be removed2") ; TODO remove from interned strings because subroutine got removed
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user