diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2e0a3b..4b7f2e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current version +* Added goto. + * Added arrays of elements of size greater than byte. * Improved passing of register parameters to assembly functions. diff --git a/docs/abi/undefined-behaviour.md b/docs/abi/undefined-behaviour.md index d253b61d..8961d939 100644 --- a/docs/abi/undefined-behaviour.md +++ b/docs/abi/undefined-behaviour.md @@ -39,4 +39,6 @@ Currently, such functions may be evaluated either once or twice. This might be f * when using modifying operators: calling functions on the right-hand-side index expression than modify any of the variables used on the left hand side +* jumping across the scope of for loop that uses a fixed list or across functions + The above list is not exhaustive. diff --git a/docs/lang/syntax.md b/docs/lang/syntax.md index 5307d624..3794220e 100644 --- a/docs/lang/syntax.md +++ b/docs/lang/syntax.md @@ -328,6 +328,7 @@ for : [ ] { * `` – traverse every value in the list, in the given order. Values do not have to be constant. If a value is not a constant and its value changes while executing the loop, the behaviour is undefined. +Jumps using `goto` across the scope of this kind of loop are disallowed. ### `break` and `continue` statements @@ -345,6 +346,31 @@ continue while continue do continue ``` + +### `goto` and `label` + +Syntax: + +``` +goto +label +``` + +The `label` statement defines a constant pointer that refers to the current position in the code. +Such labels are only visible in the scope of the local function. + +The `goto` expression jumps to the pointer value of the expression. + +Jumping using `goto` across the scope of for loop that uses a fixed list or across functions is not allowed. + +Computed gotos are supported: + +``` +pointer p +p = x +goto p +label x +``` ### `asm` statements diff --git a/millfork-udl.xml b/millfork-udl.xml index 1b829ce3..a46ce87a 100644 --- a/millfork-udl.xml +++ b/millfork-udl.xml @@ -25,7 +25,7 @@ void byte sbyte ubyte array word farword pointer farpointer long word_be word_le long_be long_le file int8 int16 int24 int32 int40 int48 int56 int64 signed8 fast - if else for return while do asm extern import segment break continue default alias enum struct union + if else for return while do asm extern import segment break continue default alias enum struct union goto label defaultz petscii ascii scr petscr pet atascii atari bbc sinclair apple2 jis jisx iso_de iso_yu iso_no iso_dk iso_se iso_fi petsciiz asciiz scrz petscrz petz atasciiz atariz bbcz sinclairz apple2z jisz jisxz iso_dez iso_yuz iso_noz iso_dkz iso_sez iso_fiz until to downto parallelto static stack ref const volatile paralleluntil inline noinline macro register kernal_interrupt interrupt reentrant hi lo sin cos tan nonet align false true nullptr "sta " "lda " "jmp " "bit " "eor " "adc " "sbc " "ora " "and " "ldx " "ldy " "stx " "sty " "tax" "tay" "tya" "txa" "txs" "tsx" "sei" "cli" "clv" "clc" "cld" "sed" "sec" "bra " "beq " "bne " "bmi " "bpl " "bcc " "bcs " "bvs " bvc " "jsr " rts" "rti" "brk" "rol" "ror" "asl" "lsr" "inc " "dec " "cmp " "cpx " "cpy " inx iny dex dey pla pha plp hp phx plx phy ply "stz " "ldz " tza taz "tsb " "trb " ra txy tyx pld plb phb phd phk xce "STA " "LDA " "JMP " "BIT " "EOR " "ADC " "SBC " "ORA " "AND " "LDX " "LDY " "STX " "STY " "TAX" "TAY" "TYA" "TXA" "TXS" "TSX" "SEI" "CLI" "CLV" "CLC" "CLD" "SED" "SEC" "BEQ " "BRA " "BNE " "BMI " "BPL " "BCC " "BCS " "BVS " BVC " "JSR " RTS" "RTI" "BRK" "ROL" "ROR" "ASL" "LSR" "INC " "DEC " "CMP " "CPX " "CPY " INX INY DEX DEY PLA PHA PLP HP PHX PLX PHY PLY "STZ " "LDZ " TZA TAZ "TSB " "TRB " RA TXY TYX PLD PLB PHB PHD PHK XCE "sbx " "isc " "dcp " "lax " "sax " "anc " "alr " "arr " "rra " "rla " "lxa " "ane " "xaa " "SBX " "ISC " "DCP " "LAX " "SAX " "ANC " "ALR " "ARR " "RRA " "RLA " "LXA " "ANE " "XAA " diff --git a/src/main/scala/millfork/compiler/mos/MosStatementCompiler.scala b/src/main/scala/millfork/compiler/mos/MosStatementCompiler.scala index 9527b5f5..1ca84b64 100644 --- a/src/main/scala/millfork/compiler/mos/MosStatementCompiler.scala +++ b/src/main/scala/millfork/compiler/mos/MosStatementCompiler.scala @@ -277,6 +277,14 @@ object MosStatementCompiler extends AbstractStatementCompiler[AssemblyLine] { } } }) -> Nil + case s: GotoStatement => + env.eval(s.target) match { + case Some(e) => List(AssemblyLine.absolute(JMP, e)) -> Nil + case None => + MosExpressionCompiler.compileToZReg(ctx, s.target) ++ List(AssemblyLine(JMP, Indirect, env.get[ThingInMemory]("__reg.loword").toAddress)) -> Nil + } + case s: LabelStatement => + List(AssemblyLine.label(env.prefix + s.name)) -> Nil case s: IfStatement => compileIfStatement(ctx, s) case s: WhileStatement => diff --git a/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala b/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala index 85af8e55..dbbe8fff 100644 --- a/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala +++ b/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala @@ -83,6 +83,14 @@ object Z80StatementCompiler extends AbstractStatementCompiler[ZLine] { } }) -> Nil + case s: GotoStatement => + env.eval(s.target) match { + case Some(e) => List(ZLine(JP, NoRegisters, e)) -> Nil + case None => + Z80ExpressionCompiler.compileToHL(ctx, s.target) ++ List(ZLine(JP, OneRegister(ZRegister.HL), Constant.Zero)) -> Nil + } + case s: LabelStatement => + List(ZLine.label(env.prefix + s.name)) -> Nil case Assignment(destination, source) => val sourceType = AbstractExpressionCompiler.getExpressionType(ctx, source) val targetType = AbstractExpressionCompiler.getExpressionType(ctx, destination) diff --git a/src/main/scala/millfork/env/Environment.scala b/src/main/scala/millfork/env/Environment.scala index 90306866..e1c6c5f7 100644 --- a/src/main/scala/millfork/env/Environment.scala +++ b/src/main/scala/millfork/env/Environment.scala @@ -210,6 +210,7 @@ class Environment(val parent: Option[Environment], val prefix: String, val cpuFa val things: mutable.Map[String, Thing] = mutable.Map() val pointiesUsed: mutable.Map[String, Set[String]] = mutable.Map() val removedThings: mutable.Set[String] = mutable.Set() + val knownLocalLabels: mutable.Set[(String, Option[Position])] = mutable.Set() private def addThing(t: Thing, position: Option[Position]): Unit = { if (assertNotDefined(t.name, position)) { @@ -938,6 +939,7 @@ class Environment(val parent: Option[Environment], val prefix: String, val cpuFa val pointies = collectPointies(stmt.statements.getOrElse(Seq.empty)) pointiesUsed(stmt.name) = pointies val w = get[Type]("word") + val p = get[Type]("pointer") val name = stmt.name val resultType = get[Type](stmt.resultType) if (stmt.name == "main") { @@ -1022,6 +1024,50 @@ class Environment(val parent: Option[Environment], val prefix: String, val cpuFa case a: ArrayDeclarationStatement => env.registerArray(a, options) case _ => () } + def scanForLabels(statement: Statement): Unit = statement match { + case c: CompoundStatement => c.getChildStatements.foreach(scanForLabels) + case LabelStatement(labelName) => env.knownLocalLabels += (labelName -> statement.position) + case _ => () + } + statements.foreach(scanForLabels) + for ((knownLabel, position) <- env.knownLocalLabels) { + env.addThing(knownLabel, ConstantThing(env.prefix + knownLabel, Label(env.prefix + knownLabel).toAddress, p), position) + } + + // not all in-function gotos are allowed; warn about the provably wrong ones: + def checkLabels(statements: Seq[Statement]) = { + def getAllSafeLabels(statements: Seq[Statement]): Seq[String] = statements.flatMap { + case _: ForEachStatement => Nil + case c: CompoundStatement => getAllSafeLabels(c.getChildStatements) + case LabelStatement(labelName) => Seq(labelName) + case _ => Nil + } + def getAllSafeGotos(statements: Seq[Statement]):Seq[String] = statements.flatMap { + case _: ForEachStatement => Nil + case c: CompoundStatement => getAllSafeGotos(c.getChildStatements) + case GotoStatement(VariableExpression(labelName)) if env.knownLocalLabels.exists(_._1.==(labelName)) => Seq(labelName) + case _ => Nil + } + def doOnlyCheck(position: Option[Position], statements: Seq[Statement]): Unit = { + val l = getAllSafeLabels(statements).toSet + val g = getAllSafeGotos(statements).toSet + val bad = g.&(env.knownLocalLabels.map(_._1)).--(l) + if (bad.nonEmpty) { + log.warn("Detected cross-loop gotos to labels " + bad.mkString(", "), position) + } + } + def recurse(statements: Seq[Statement]):Unit = statements.foreach { + case c: ForEachStatement => + doOnlyCheck(c.position, c.body) + recurse(c.body) + case c: CompoundStatement => recurse(c.getChildStatements) + case _ => Nil + } + doOnlyCheck(stmt.position, statements) + recurse(statements) + } + checkLabels(statements) + val executableStatements = statements.flatMap { case e: ExecutableStatement => Some(e) case _ => None diff --git a/src/main/scala/millfork/node/Node.scala b/src/main/scala/millfork/node/Node.scala index d38ae0d4..ff4cec54 100644 --- a/src/main/scala/millfork/node/Node.scala +++ b/src/main/scala/millfork/node/Node.scala @@ -467,6 +467,14 @@ case class ReturnStatement(value: Option[Expression]) extends ExecutableStatemen override def getAllExpressions: List[Expression] = value.toList } +case class GotoStatement(target: Expression) extends ExecutableStatement { + override def getAllExpressions: List[Expression] = List(target) +} + +case class LabelStatement(name: String) extends ExecutableStatement { + override def getAllExpressions: List[Expression] = Nil +} + case class EmptyStatement(toTypecheck: List[ExecutableStatement]) extends ExecutableStatement { override def getAllExpressions: List[Expression] = toTypecheck.flatMap(_.getAllExpressions) } diff --git a/src/main/scala/millfork/parser/MfParser.scala b/src/main/scala/millfork/parser/MfParser.scala index 19b30c6e..54a445dc 100644 --- a/src/main/scala/millfork/parser/MfParser.scala +++ b/src/main/scala/millfork/parser/MfParser.scala @@ -380,6 +380,8 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri def keywordStatement: P[Seq[ExecutableStatement]] = P( returnOrDispatchStatement | + gotoStatement | + labelStatement | ifStatement | whileStatement | forStatement | @@ -437,6 +439,10 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri val returnOrDispatchStatement: P[Seq[ExecutableStatement]] = "return" ~ !letterOrDigit ~/ HWS ~ (dispatchStatementBody | mfExpression(nonStatementLevel, false).?.map(ReturnStatement).map(Seq(_))) + val gotoStatement: P[Seq[ExecutableStatement]] = "goto" ~ !letterOrDigit ~/ HWS ~ mfExpression(nonStatementLevel, false).map(GotoStatement).map(Seq(_)) + + val labelStatement: P[Seq[ExecutableStatement]] = "label" ~ !letterOrDigit ~/ HWS ~ identifier.map(LabelStatement).map(Seq(_)) + def ifStatement: P[Seq[ExecutableStatement]] = for { condition <- "if" ~ !letterOrDigit ~/ HWS ~/ mfExpression(nonStatementLevel, false) thenBranch <- AWS ~/ executableStatements diff --git a/src/test/scala/millfork/test/GotoSuite.scala b/src/test/scala/millfork/test/GotoSuite.scala new file mode 100644 index 00000000..a6effe0e --- /dev/null +++ b/src/test/scala/millfork/test/GotoSuite.scala @@ -0,0 +1,57 @@ +package millfork.test + +import millfork.Cpu +import millfork.test.emu.{EmuCrossPlatformBenchmarkRun, EmuUnoptimizedCrossPlatformRun} +import org.scalatest.{FunSuite, Matchers} + +/** + * @author Karol Stasiak + */ +class GotoSuite extends FunSuite with Matchers { + + test("Goto 1") { + EmuCrossPlatformBenchmarkRun(Cpu.Mos, Cpu.Cmos, Cpu.Z80, Cpu.Intel8080, Cpu.Sharp)( + """ + | + | byte output @$c000 + | + | void main() { + | byte x + | x = 0 + | if x == 0 { + | label a + | } + | x += 1 + | if x < 100 { + | goto a + | } + | output = x + | } + | + """.stripMargin){m => + m.readByte(0xc000) should equal(100) + } + } + + test("Cross-loop goto") { + EmuUnoptimizedCrossPlatformRun(Cpu.Mos, Cpu.Z80)( + """ + | + | byte output @$c000 + | + | void main() { + | byte x + | label a + | for x : [1,2,3] { + | if x == 0 { goto a } + | goto b + | label b + | output = x + | } + | } + | + """.stripMargin){m => + m.readByte(0xc000) should equal(3) + } + } +}