From fd25e85d59df62576a0e797a380fb7cc0719baa6 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Tue, 14 Mar 2023 23:37:49 +0100 Subject: [PATCH] added `unroll` loop construct --- .../src/prog8/optimizer/StatementOptimizer.kt | 7 +++ .../compiler/astprocessing/AstChecker.kt | 13 ++++++ .../astprocessing/IntermediateAstMaker.kt | 11 +++++ compiler/test/ast/TestVariousCompilerAst.kt | 43 +++++++++++++++++++ .../src/prog8/ast/antlr/Antlr2Kotlin.kt | 11 +++++ .../src/prog8/ast/statements/AstStatements.kt | 20 +++++++++ compilerAst/src/prog8/ast/walk/AstWalker.kt | 8 ++++ compilerAst/src/prog8/ast/walk/IAstVisitor.kt | 4 ++ docs/source/programming.rst | 4 ++ docs/source/syntaxreference.rst | 15 +++++++ parser/antlr/Prog8ANTLR.g4 | 3 ++ 11 files changed, 139 insertions(+) diff --git a/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt b/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt index 382f3882f..e4958e063 100644 --- a/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt +++ b/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt @@ -374,4 +374,11 @@ class StatementOptimizer(private val program: Program, return noModifications } + + override fun before(unrollLoop: UnrollLoop, parent: Node): Iterable { + return if(unrollLoop.iterations<1) + listOf(IAstModification.Remove(unrollLoop, parent as IStatementContainer)) + else + noModifications + } } diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index c56aa4b0e..20074a617 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -74,6 +74,19 @@ internal class AstChecker(private val program: Program, } } + override fun visit(unrollLoop: UnrollLoop) { + if(unrollLoop.iterations<0 || unrollLoop.iterations>65535) + errors.err("invalid number of unrolls", unrollLoop.position) + unrollLoop.body.statements.forEach { + if(it !is InlineAssembly && it !is Assignment && it !is BuiltinFunctionCallStatement && it !is FunctionCallStatement && it !is PostIncrDecr) + errors.err("invalid statement in unroll loop", it.position) + } + if(unrollLoop.iterations * unrollLoop.body.statements.size > 256) { + errors.warn("large number of unrolls, potential code size issue", unrollLoop.position) + } + super.visit(unrollLoop) + } + override fun visit(returnStmt: Return) { val expectedReturnValues = returnStmt.definingSubroutine?.returntypes ?: emptyList() if(expectedReturnValues.size>1) { diff --git a/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt b/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt index 15775be81..008bef367 100644 --- a/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt +++ b/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt @@ -55,6 +55,7 @@ class IntermediateAstMaker(private val program: Program, private val options: Co is Label -> transform(statement) is PostIncrDecr -> transform(statement) is RepeatLoop -> transform(statement) + is UnrollLoop -> transform(statement) is Return -> transform(statement) is Subroutine -> { if(statement.isAsmSubroutine) @@ -325,6 +326,16 @@ class IntermediateAstMaker(private val program: Program, private val options: Co return repeat } + private fun transform(srcUnroll: UnrollLoop): PtNodeGroup { + val result = PtNodeGroup() + repeat(srcUnroll.iterations) { + srcUnroll.body.statements.forEach { + result.add(transformStatement(it)) + } + } + return result + } + private fun transform(srcNode: Return): PtReturn { val ret = PtReturn(srcNode.position) if(srcNode.value!=null) diff --git a/compiler/test/ast/TestVariousCompilerAst.kt b/compiler/test/ast/TestVariousCompilerAst.kt index 70018fcd1..05a2645ee 100644 --- a/compiler/test/ast/TestVariousCompilerAst.kt +++ b/compiler/test/ast/TestVariousCompilerAst.kt @@ -3,6 +3,7 @@ package prog8tests.ast import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain import io.kotest.matchers.types.instanceOf import prog8.ast.IFunctionCall import prog8.ast.expressions.* @@ -12,6 +13,7 @@ import prog8.ast.statements.VarDecl import prog8.code.core.DataType import prog8.code.core.Position import prog8.code.target.C64Target +import prog8tests.helpers.ErrorReporterForTests import prog8tests.helpers.compileText class TestVariousCompilerAst: FunSpec({ @@ -214,5 +216,46 @@ main { """ compileText(C64Target(), optimize=false, src, writeAssembly=false) shouldNotBe null } + + test("unroll good") { + val src=""" +main { + sub start() { + unroll 200 { + cx16.r0++ + poke(2000,2) + } + } +} +""" + val errors = ErrorReporterForTests(keepMessagesAfterReporting = true) + compileText(C64Target(), optimize=false, src, writeAssembly=false, errors=errors) shouldNotBe null + errors.warnings.size shouldBe 1 + errors.warnings[0] shouldContain "large number of unrolls" + } + + test("unroll bad") { + val src=""" +main { + sub start() { + repeat { + unroll 80 { + cx16.r0++ + when cx16.r0 { + 1 -> cx16.r0++ + else -> cx16.r0++ + } + break + } + } + } +} +""" + val errors = ErrorReporterForTests() + compileText(C64Target(), optimize=false, src, writeAssembly=false, errors = errors) shouldBe null + errors.errors.size shouldBe 2 + errors.errors[0] shouldContain "invalid statement in unroll loop" + errors.errors[1] shouldContain "invalid statement in unroll loop" + } }) diff --git a/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt b/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt index b3515b8fd..5bc84ac1b 100644 --- a/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt +++ b/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt @@ -150,6 +150,9 @@ private fun Prog8ANTLRParser.StatementContext.toAst() : Statement { val whenstmt = whenstmt()?.toAst() if(whenstmt!=null) return whenstmt + val unrollstmt = unrollloop()?.toAst() + if(unrollstmt!=null) return unrollstmt + throw FatalAstException("unprocessed source text (are we missing ast conversion rules for parser elements?): $text") } @@ -573,6 +576,14 @@ private fun Prog8ANTLRParser.RepeatloopContext.toAst(): RepeatLoop { return RepeatLoop(iterations, scope, toPosition()) } +private fun Prog8ANTLRParser.UnrollloopContext.toAst(): UnrollLoop { + val iterations = integerliteral().toAst().number.toInt() + val statements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) + val scope = AnonymousScope(statements, statement_block()?.toPosition() + ?: statement().toPosition()) + return UnrollLoop(iterations, scope, toPosition()) +} + private fun Prog8ANTLRParser.UntilloopContext.toAst(): UntilLoop { val untilCondition = expression().toAst() val statements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) diff --git a/compilerAst/src/prog8/ast/statements/AstStatements.kt b/compilerAst/src/prog8/ast/statements/AstStatements.kt index 73bba388c..48e0928db 100644 --- a/compilerAst/src/prog8/ast/statements/AstStatements.kt +++ b/compilerAst/src/prog8/ast/statements/AstStatements.kt @@ -880,6 +880,26 @@ class RepeatLoop(var iterations: Expression?, var body: AnonymousScope, override override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent) } +class UnrollLoop(val iterations: Int, var body: AnonymousScope, override val position: Position) : Statement() { + override lateinit var parent: Node + + override fun linkParents(parent: Node) { + this.parent = parent + body.linkParents(this) + } + + override fun copy() = throw NotImplementedError("no support for duplicating a RepeatLoop") + + override fun replaceChildNode(node: Node, replacement: Node) { + if (node===body) body = replacement as AnonymousScope + else throw FatalAstException("invalid replace") + replacement.parent = this + } + + override fun accept(visitor: IAstVisitor) = visitor.visit(this) + override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent) +} + class UntilLoop(var body: AnonymousScope, var condition: Expression, override val position: Position) : Statement() { diff --git a/compilerAst/src/prog8/ast/walk/AstWalker.kt b/compilerAst/src/prog8/ast/walk/AstWalker.kt index 12687683e..99d031c57 100644 --- a/compilerAst/src/prog8/ast/walk/AstWalker.kt +++ b/compilerAst/src/prog8/ast/walk/AstWalker.kt @@ -106,6 +106,7 @@ abstract class AstWalker { open fun before(expr: PrefixExpression, parent: Node): Iterable = noModifications open fun before(forLoop: ForLoop, parent: Node): Iterable = noModifications open fun before(repeatLoop: RepeatLoop, parent: Node): Iterable = noModifications + open fun before(unrollLoop: UnrollLoop, parent: Node): Iterable = noModifications open fun before(functionCallExpr: FunctionCallExpression, parent: Node): Iterable = noModifications open fun before(bfc: BuiltinFunctionCall, parent: Node): Iterable = noModifications open fun before(functionCallStatement: FunctionCallStatement, parent: Node): Iterable = noModifications @@ -148,6 +149,7 @@ abstract class AstWalker { open fun after(expr: PrefixExpression, parent: Node): Iterable = noModifications open fun after(forLoop: ForLoop, parent: Node): Iterable = noModifications open fun after(repeatLoop: RepeatLoop, parent: Node): Iterable = noModifications + open fun after(unrollLoop: UnrollLoop, parent: Node): Iterable = noModifications open fun after(functionCallExpr: FunctionCallExpression, parent: Node): Iterable = noModifications open fun after(bfc: BuiltinFunctionCall, parent: Node): Iterable = noModifications open fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable = noModifications @@ -394,6 +396,12 @@ abstract class AstWalker { track(after(repeatLoop, parent), repeatLoop, parent) } + fun visit(unrollLoop: UnrollLoop, parent: Node) { + track(before(unrollLoop, parent), unrollLoop, parent) + unrollLoop.body.accept(this, unrollLoop) + track(after(unrollLoop, parent), unrollLoop, parent) + } + fun visit(untilLoop: UntilLoop, parent: Node) { track(before(untilLoop, parent), untilLoop, parent) untilLoop.condition.accept(this, untilLoop) diff --git a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt index c02170c08..a7712f1d0 100644 --- a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt +++ b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt @@ -131,6 +131,10 @@ interface IAstVisitor { repeatLoop.body.accept(this) } + fun visit(unrollLoop: UnrollLoop) { + unrollLoop.body.accept(this) + } + fun visit(untilLoop: UntilLoop) { untilLoop.condition.accept(this) untilLoop.body.accept(this) diff --git a/docs/source/programming.rst b/docs/source/programming.rst index 337cc9afa..1cafd3d76 100644 --- a/docs/source/programming.rst +++ b/docs/source/programming.rst @@ -485,6 +485,10 @@ You can also create loops by using the ``goto`` statement, but this should usual Breaking out of a loop prematurely is possible with the ``break`` statement. +The *unroll* loop is not really a loop, but looks like one. It actually duplicates the statements in its block on the spot by +the given number of times. It's meant to "unroll loops" - trade memory for speed by avoiding the actual repeat loop counting code. +Only simple statements are allowed to be inside an unroll loop (assignments, function calls etc.). + .. attention:: The value of the loop variable after executing the loop *is undefined*. Don't use it immediately after the loop without first assigning a new value to it! diff --git a/docs/source/syntaxreference.rst b/docs/source/syntaxreference.rst index 580445837..174ae061c 100644 --- a/docs/source/syntaxreference.rst +++ b/docs/source/syntaxreference.rst @@ -767,6 +767,21 @@ If you omit the iteration count, it simply loops forever. You can still ``break`` out of such a loop if you want though. +unroll loop +^^^^^^^^^^^ + +Like a repeat loop, but trades memory for speed by not generating the code +for the counter. Instead it duplicates the code inside the loop on the spot for +the given number of iterations. This means that only a constant number of iterations can be specified. +Also, only simple statements such as assignments and function calls can be inside the loop:: + + unroll 80 { + cx16.VERA_DATA0 = 255 + } + +A `break` statement cannot occur in an unroll loop, as there is not really a loop to break out of. + + Conditional Execution and Jumps ------------------------------- diff --git a/parser/antlr/Prog8ANTLR.g4 b/parser/antlr/Prog8ANTLR.g4 index 38c0fb059..03588246c 100644 --- a/parser/antlr/Prog8ANTLR.g4 +++ b/parser/antlr/Prog8ANTLR.g4 @@ -92,6 +92,7 @@ statement : | whileloop | untilloop | repeatloop + | unrollloop | whenstmt | breakstmt | labeldef @@ -289,6 +290,8 @@ untilloop: 'do' (statement | statement_block) EOL? 'until' expression ; repeatloop: 'repeat' expression? EOL? (statement | statement_block) ; +unrollloop: 'unroll' integerliteral? EOL? (statement | statement_block) ; + whenstmt: 'when' expression '{' EOL (when_choice | EOL) * '}' EOL? ; when_choice: (expression_list | 'else' ) '->' (statement | statement_block ) ;