replace unwritten vars by consts. Improved const eval.

Fixed some slight bugs in library code
This commit is contained in:
Irmen de Jong 2023-12-28 05:17:15 +01:00
parent b428343c2a
commit 09c6cb4d6b
19 changed files with 131 additions and 70 deletions

View File

@ -3,10 +3,8 @@ package prog8.code.core
val AssociativeOperators = setOf("+", "*", "&", "|", "^", "==", "!=")
val ComparisonOperators = setOf("==", "!=", "<", ">", "<=", ">=")
val LogicalOperators = setOf("and", "or", "xor", "not")
val AugmentAssignmentOperators = setOf("+", "-", "/", "*", "&", "|", "^", "<<", ">>", "%", "and", "or", "xor")
val BitwiseOperators = setOf("&", "|", "^", "~")
val PrefixOperators = setOf("+", "-", "~", "not")
// val InvalidOperatorsForBoolean = setOf("+", "-", "*", "/", "%", "<<", ">>") + BitwiseOperators
fun invertedComparisonOperator(operator: String) =
when (operator) {

View File

@ -19,9 +19,9 @@ class ConstExprEvaluator {
"*" -> multiply(left, right)
"/" -> divide(left, right)
"%" -> remainder(left, right)
"&" -> bitwiseand(left, right)
"|" -> bitwiseor(left, right)
"^" -> bitwisexor(left, right)
"&" -> bitwiseAnd(left, right)
"|" -> bitwiseOr(left, right)
"^" -> bitwiseXor(left, right)
"<" -> NumericLiteral.fromBoolean(left < right, left.position)
">" -> NumericLiteral.fromBoolean(left > right, left.position)
"<=" -> NumericLiteral.fromBoolean(left <= right, left.position)
@ -30,6 +30,9 @@ class ConstExprEvaluator {
"!=" -> NumericLiteral.fromBoolean(left != right, left.position)
"<<" -> shiftedleft(left, right)
">>" -> shiftedright(left, right)
"and" -> logicalAnd(left, right)
"or" -> logicalOr(left, right)
"xor" -> logicalXor(left, right)
else -> throw FatalAstException("const evaluation for invalid operator $operator")
}
} catch (ax: FatalAstException) {
@ -55,7 +58,7 @@ class ConstExprEvaluator {
return NumericLiteral(left.type, result.toDouble(), left.position)
}
private fun bitwisexor(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
private fun bitwiseXor(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(left.type== DataType.UBYTE) {
if(right.type in IntegerDatatypes) {
return NumericLiteral(DataType.UBYTE, (left.number.toInt() xor (right.number.toInt() and 255)).toDouble(), left.position)
@ -68,7 +71,7 @@ class ConstExprEvaluator {
throw ExpressionError("cannot calculate $left ^ $right", left.position)
}
private fun bitwiseor(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
private fun bitwiseOr(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(left.type== DataType.UBYTE) {
if(right.type in IntegerDatatypes) {
return NumericLiteral(DataType.UBYTE, (left.number.toInt() or (right.number.toInt() and 255)).toDouble(), left.position)
@ -81,7 +84,7 @@ class ConstExprEvaluator {
throw ExpressionError("cannot calculate $left | $right", left.position)
}
private fun bitwiseand(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
private fun bitwiseAnd(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(left.type== DataType.UBYTE) {
if(right.type in IntegerDatatypes) {
return NumericLiteral(DataType.UBYTE, (left.number.toInt() and (right.number.toInt() and 255)).toDouble(), left.position)
@ -94,6 +97,15 @@ class ConstExprEvaluator {
throw ExpressionError("cannot calculate $left & $right", left.position)
}
private fun logicalAnd(left: NumericLiteral, right: NumericLiteral): NumericLiteral =
NumericLiteral.fromBoolean(left.asBooleanValue and right.asBooleanValue, left.position)
private fun logicalOr(left: NumericLiteral, right: NumericLiteral): NumericLiteral =
NumericLiteral.fromBoolean(left.asBooleanValue or right.asBooleanValue, left.position)
private fun logicalXor(left: NumericLiteral, right: NumericLiteral): NumericLiteral =
NumericLiteral.fromBoolean(left.asBooleanValue xor right.asBooleanValue, left.position)
private fun plus(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
val error = "cannot add $left and $right"
return when (left.type) {

View File

@ -18,6 +18,8 @@ import kotlin.math.floor
class ConstantFoldingOptimizer(private val program: Program, private val errors: IErrorReporter) : AstWalker() {
private val evaluator = ConstExprEvaluator()
override fun before(memread: DirectMemoryRead, parent: Node): Iterable<IAstModification> {
// @( &thing ) --> thing (but only if thing is a byte type!)
val addrOf = memread.addressExpression as? AddressOf
@ -137,12 +139,13 @@ class ConstantFoldingOptimizer(private val program: Program, private val errors:
}
}
val evaluator = ConstExprEvaluator()
// const fold when both operands are a const
// const fold when both operands are a const.
// if in a chained comparison, that one has to be desugared first though.
if(leftconst != null && rightconst != null) {
val result = evaluator.evaluate(leftconst, expr.operator, rightconst)
modifications += IAstModification.ReplaceNode(expr, result, parent)
if((expr.parent as? BinaryExpression)?.isChainedComparison()!=true) {
val result = evaluator.evaluate(leftconst, expr.operator, rightconst)
modifications += IAstModification.ReplaceNode(expr, result, parent)
}
}
if(leftconst==null && rightconst!=null && rightconst.number<0.0) {

View File

@ -1,17 +1,11 @@
package prog8.optimizer
import prog8.ast.*
import prog8.ast.expressions.BinaryExpression
import prog8.ast.expressions.NumericLiteral
import prog8.ast.expressions.PrefixExpression
import prog8.ast.expressions.TypecastExpression
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
import prog8.code.core.DataType
import prog8.code.core.ICompilationTarget
import prog8.code.core.IErrorReporter
import prog8.code.core.internedStringsModuleName
import prog8.code.core.*
import prog8.compiler.CallGraph
@ -124,6 +118,36 @@ class UnusedCodeRemover(private val program: Program,
return listOf(IAstModification.Remove(decl, parent as IStatementContainer))
}
else {
val (writes, reads) = usages
.partition{
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|| it.parent is AssignTarget
|| it.parent is ForLoop
|| it.parent is AddressOf
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
}
val singleAssignment = writes.singleOrNull()?.parent?.parent as? Assignment ?: writes.singleOrNull()?.parent as? Assignment
if (singleAssignment!=null && reads.isNotEmpty()) {
if (singleAssignment.origin == AssignmentOrigin.VARINIT && singleAssignment.value.constValue(program) != null) {
// variable only has a single write and it is the initialization value, so it can be replaced with a constant, IF the value is a constant
errors.warn("variable is never written to and was replaced by a constant", decl.position)
val const = VarDecl(VarDeclType.CONST, decl.origin, decl.datatype, decl.zeropage, decl.arraysize, decl.name, decl.names, singleAssignment.value, decl.sharedWithAsm, decl.splitArray, decl.position)
return listOf(
IAstModification.ReplaceNode(decl, const, parent),
IAstModification.Remove(singleAssignment, singleAssignment.parent as IStatementContainer)
)
}
}
/*
TODO: need to check if there are no variable usages between the declaration and the assignment (because these rely on the original initialization value)
if(writes.size==2) {
val firstAssignment = writes[0].parent as? Assignment
val secondAssignment = writes[1].parent as? Assignment
if(firstAssignment?.origin==AssignmentOrigin.VARINIT && secondAssignment?.value?.constValue(program)!=null) {
errors.warn("variable is only assigned once here, consider using this as the initialization value in the declaration instead", secondAssignment.position)
}
}
*/
if(usages.size==1) {
val singleUse = usages[0].parent
if(singleUse is AssignTarget) {

View File

@ -3,6 +3,7 @@
; BMX Specification: https://cx16forum.com/forum/viewtopic.php?t=6945
%import diskio
%option no_symbol_prefixing, ignore_unused
bmx {
@ -17,7 +18,7 @@ bmx {
uword palette_entries ; 1-256
ubyte palette_start
ubyte compression
uword palette_buffer_ptr = 0 ; should you want to load or save the palette into main memory instead of directly into vram
uword @shared palette_buffer_ptr = 0 ; should you want to load or save the palette into main memory instead of directly into vram
uword error_message ; pointer to error message, or 0 if all ok
ubyte old_drivenumber

View File

@ -22,7 +22,7 @@ diskio {
const ubyte READ_IO_CHANNEL=12
const ubyte WRITE_IO_CHANNEL=13
ubyte drivenumber = 8 ; user programs can set this to the drive number they want to load/save to!
ubyte @shared drivenumber = 8 ; user programs can set this to the drive number they want to load/save to!
sub reset_read_channel() {
void cbm.CHKIN(READ_IO_CHANNEL)

View File

@ -11,7 +11,7 @@ diskio {
const ubyte READ_IO_CHANNEL=12
const ubyte WRITE_IO_CHANNEL=13
ubyte drivenumber = 8 ; user programs can set this to the drive number they want to load/save to!
ubyte @shared drivenumber = 8 ; user programs can set this to the drive number they want to load/save to!
sub reset_read_channel() {
void cbm.CHKIN(READ_IO_CHANNEL)

View File

@ -81,8 +81,7 @@ internal class BeforeAsmTypecastCleaner(val program: Program,
// also convert calls to builtin functions to BuiltinFunctionCall nodes to make things easier for codegen
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<IAstModification> {
if(functionCallStatement.target.nameInSource.size==1
&& functionCallStatement.target.nameInSource[0] in program.builtinFunctions.names) {
if(functionCallStatement.target.nameInSource.singleOrNull() in program.builtinFunctions.names) {
return listOf(IAstModification.ReplaceNode(
functionCallStatement,
BuiltinFunctionCallStatement(functionCallStatement.target, functionCallStatement.args, functionCallStatement.position),
@ -94,8 +93,7 @@ internal class BeforeAsmTypecastCleaner(val program: Program,
}
override fun before(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<IAstModification> {
if(functionCallExpr.target.nameInSource.size==1
&& functionCallExpr.target.nameInSource[0] in program.builtinFunctions.names) {
if(functionCallExpr.target.nameInSource.singleOrNull() in program.builtinFunctions.names) {
return listOf(IAstModification.ReplaceNode(
functionCallExpr,
BuiltinFunctionCall(functionCallExpr.target, functionCallExpr.args, functionCallExpr.position),

View File

@ -262,10 +262,9 @@ _after:
// desugar chained comparisons: i < x < j ---> i<x and x<j
// only if i<x or x<j was not written in parentheses! (i<x) < y, i < (x<y) -> leave untouched
if(expr.operator in ComparisonOperators) {
if(expr.isChainedComparison()) {
val leftBinExpr = expr.left as? BinaryExpression
val rightBinExpr = expr.right as? BinaryExpression
if(leftBinExpr!=null && !leftBinExpr.insideParentheses && leftBinExpr.operator in ComparisonOperators) {
if(leftBinExpr!=null) {
if(!leftBinExpr.right.isSimple) {
errors.warn("possible multiple evaluation of subexpression in chained comparison, consider using a temporary variable", leftBinExpr.right.position)
}
@ -273,7 +272,8 @@ _after:
val desugar = BinaryExpression(leftBinExpr, "and", right, expr.position)
return listOf(IAstModification.ReplaceNode(expr, desugar, parent))
}
else if(rightBinExpr!=null && !rightBinExpr.insideParentheses && rightBinExpr.operator in ComparisonOperators) {
val rightBinExpr = expr.right as? BinaryExpression
if(rightBinExpr!=null) {
if(!rightBinExpr.left.isSimple) {
errors.warn("possible multiple evaluation of subexpression in chained comparison, consider using a temporary variable", rightBinExpr.left.position)
}

View File

@ -106,8 +106,8 @@ class TestOptimization: FunSpec({
const ubyte boardOffsetC = 3
sub start() {
uword load_location = 12345
word llw = 12345
uword @shared load_location = 12345
word @shared llw = 12345
cx16.r0 = load_location + 8000 + 1000 + 1000
cx16.r2 = 8000 + 1000 + 1000 + load_location
cx16.r4 = load_location + boardOffsetC + boardHeightC - 1
@ -156,8 +156,8 @@ class TestOptimization: FunSpec({
%option enable_floats
main {
sub start() {
float llw = 300.0
float result
float @shared llw = 300.0
float @shared result
result = 9 * 2 * 10 * llw
result++
result = llw * 9 * 2 * 10
@ -214,7 +214,7 @@ class TestOptimization: FunSpec({
val source = """
main {
sub start() {
word llw = 300
word @shared llw = 300
cx16.r0s = 9 * 2 * 10 * llw
cx16.r1s = llw * 9 * 2 * 10
cx16.r2s = llw / 30 / 3
@ -483,8 +483,8 @@ class TestOptimization: FunSpec({
val src="""
main {
sub start() {
uword aa
ubyte zz
uword @shared aa
ubyte @shared zz
@(aa) = zz + 32 ; do not optimize this away!
}
}
@ -633,8 +633,8 @@ class TestOptimization: FunSpec({
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
ubyte @shared source=99
ubyte @shared thingy=42
if source==3 or source==4 or source==99 or source==1
thingy++
@ -673,8 +673,8 @@ class TestOptimization: FunSpec({
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
ubyte @shared source=99
ubyte @shared thingy=42
if source==3 or source==4 or source!=99 or source==1
thingy++
@ -691,8 +691,8 @@ class TestOptimization: FunSpec({
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
ubyte @shared source=99
ubyte @shared thingy=42
if source==3 or source==4 or thingy==99 or source==1
thingy++
@ -709,8 +709,8 @@ class TestOptimization: FunSpec({
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
ubyte @shared source=99
ubyte @shared thingy=42
if source==3 or source==4 and source==99 or source==1
thingy++
@ -727,7 +727,7 @@ class TestOptimization: FunSpec({
val src="""
main{
sub start () {
uword eRef
uword @shared eRef
if eRef[3] and 10 {
return
}

View File

@ -44,7 +44,7 @@ class TestTypecasts: FunSpec({
main {
sub start() {
bool bb2=true
bool @shared bb2=true
bool @shared bb = bb2 and true
}
}"""
@ -73,10 +73,9 @@ main {
}
sub start() {
bool ub1 = true
bool ub2 = true
bool ub3 = true
bool ub4 = 0
bool @shared ub1 = true
bool @shared ub2 = true
bool @shared ub3 = true
bool @shared bvalue
bvalue = ub1 xor ub2 xor ub3 xor true

View File

@ -153,7 +153,7 @@ main {
val text="""
main {
sub start() {
ubyte c = 1
ubyte @shared c = 1
@(15000 + c<<${'$'}0003) = 42
@(15000 + (c<<${'$'}0003)) = 42
@(15000 + c*${'$'}0008) = 42 ; *8 becomes a shift after opt
@ -247,7 +247,7 @@ main {
sub start() {
mylabel:
ubyte variable
ubyte @shared variable
uword @shared pointer1 = &main.start
uword @shared pointer2 = &start
uword @shared pointer3 = &main.start.mylabel

View File

@ -152,9 +152,9 @@ mylabel_inside:
val src = """
main {
sub start() {
ubyte bytevar = 11 ; var at 0
ubyte byteVAR = 22 ; var at 1
ubyte ByteVar = 33 ; var at 2
ubyte @shared bytevar = 11 ; var at 0
ubyte @shared byteVAR = 22 ; var at 1
ubyte @shared ByteVar = 33 ; var at 2
ubyte @shared total = bytevar+byteVAR+ByteVar ; var at 3
goto skipLABEL
SkipLabel:

View File

@ -153,7 +153,7 @@ class BinaryExpression(
var operator: String,
var right: Expression,
override val position: Position,
val insideParentheses: Boolean = false // used in very few places to check priorities
private val insideParentheses: Boolean = false
) : Expression() {
override lateinit var parent: Node
@ -178,6 +178,18 @@ class BinaryExpression(
override val isSimple = false
fun isChainedComparison(): Boolean {
if(operator in ComparisonOperators) {
val leftBinExpr = left as? BinaryExpression
if (leftBinExpr != null && !leftBinExpr.insideParentheses && leftBinExpr.operator in ComparisonOperators)
return true
val rightBinExpr = right as? BinaryExpression
if (rightBinExpr != null && !rightBinExpr.insideParentheses && rightBinExpr.operator in ComparisonOperators)
return true
}
return false
}
// binary expression should actually have been optimized away into a single value, before const value was requested...
override fun constValue(program: Program): NumericLiteral? = null
@ -980,7 +992,7 @@ data class IdentifierReference(val nameInSource: List<String>, override val posi
override val isSimple = true
fun targetStatement(program: Program) =
if(nameInSource.size==1 && nameInSource[0] in program.builtinFunctions.names)
if(nameInSource.singleOrNull() in program.builtinFunctions.names)
BuiltinFunctionPlaceholder(nameInSource[0], position, parent)
else
definingScope.lookup(nameInSource)

View File

@ -141,30 +141,39 @@ class CallGraph(private val program: Program) : IAstVisitor {
fun unused(module: Module) = module !in usedModules
fun unused(sub: Subroutine): Boolean {
return sub !in usedSubroutines && !nameInAssemblyCode(sub.name)
return sub !in usedSubroutines && !nameInAssemblyCode(sub.name, listOf("p8s_", ""))
}
fun unused(block: Block): Boolean {
return block !in usedBlocks && !nameInAssemblyCode(block.name)
return block !in usedBlocks && !nameInAssemblyCode(block.name, listOf("p8b_", ""))
}
fun unused(decl: VarDecl): Boolean {
// Don't check assembly just for occurrences of variables, if they're not used in prog8 itself, just kill them
// Don't check assembly just for occurrences of variables, if they're not used in prog8 itself, just kill them.
// User should use @shared if they want to keep them.
return usages(decl).isEmpty()
}
fun usages(decl: VarDecl): List<IdentifierReference> {
fun usages(decl: VarDecl): List<Node> {
if(decl.type!=VarDeclType.VAR)
return emptyList()
if(decl.definingBlock !in usedBlocks)
return emptyList()
return allIdentifiersAndTargets.filter { decl===it.value }.map{ it.key }
val assemblyBlocks = allAssemblyNodes.filter {
decl.name in it.names || "p8v_" + decl.name in it.names
}
return allIdentifiersAndTargets.filter { decl===it.value }.map{ it.key } + assemblyBlocks
}
private val prefixes = listOf("p8b_", "p8v_", "p8s_", "p8l_", "p8_", "")
private fun nameInAssemblyCode(name: String): Boolean {
private fun nameInAssemblyCode(name: String, knownAsmPrefixes: List<String> = emptyList()): Boolean {
if(knownAsmPrefixes.isNotEmpty())
return allAssemblyNodes.any {
knownAsmPrefixes.any { prefix -> prefix+name in it.names }
}
return allAssemblyNodes.any {
prefixes.any { prefix -> prefix+name in it.names }
}

View File

@ -2,6 +2,10 @@
TODO
====
- make constants have p8c_ prefix instead of p8v_
- add INFO error level and move some warnings to info
- add switch to enable INFO error messages (default is WARN and up)
- [on branch: shortcircuit] investigate McCarthy evaluation again? this may also reduce code size perhaps for things like if a>4 or a<2 ....
...
@ -56,8 +60,6 @@ Libraries:
Optimizations:
- give a warning for variables that could be a const - or even make them a const (if not @shared)?
- treat every scalar variable decl with initialization value, as const by default, unless the variable gets assigned to somewhere (or has its address taken, or is @shared)
- VariableAllocator: can we think of a smarter strategy for allocating variables into zeropage, rather than first-come-first-served?
for instance, vars used inside loops first, then loopvars, then uwords used as pointers, then the rest
- various optimizers skip stuff if compTarget.name==VMTarget.NAME. Once 6502-codegen is done from IR code,

View File

@ -17,6 +17,7 @@ mcf {
ubyte file_channel
sub open(str filename, ubyte drive, ubyte channel) -> bool {
file_channel = channel
cbm.SETNAM(string.length(filename), filename)
cbm.SETLFS(channel, drive, 2)
void cbm.OPEN()

View File

@ -24,8 +24,8 @@ main {
for i in 0 to len(s2)-1
txt.setchr(i, 1, s2[i])
ubyte c1 = 'z'
ubyte c2 = sc:'z'
const ubyte c1 = 'z'
const ubyte c2 = sc:'z'
txt.print("\npetscii z=")
txt.print_ub(c1)

View File

@ -13,5 +13,7 @@ main {
txt.nl()
txt.print_ub(5<(x-y)<=9<y)
txt.nl()
txt.print_ub(5<(x-y)<=9<(y+40))
txt.nl()
}
}