All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.adrodoc55.minecraft.mpl.interpretation.MplInterpreterSpec2.groovy Maven / Gradle / Ivy

/*
 * Minecraft Programming Language (MPL): A language for easy development of command block
 * applications including an IDE.
 *
 * © Copyright (C) 2016 Adrodoc55
 *
 * This file is part of MPL.
 *
 * MPL is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MPL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with MPL. If not, see
 * .
 *
 *
 *
 * Minecraft Programming Language (MPL): Eine Sprache für die einfache Entwicklung von Commandoblock
 * Anwendungen, inklusive einer IDE.
 *
 * © Copyright (C) 2016 Adrodoc55
 *
 * Diese Datei ist Teil von MPL.
 *
 * MPL ist freie Software: Sie können diese unter den Bedingungen der GNU General Public License,
 * wie von der Free Software Foundation, Version 3 der Lizenz oder (nach Ihrer Wahl) jeder späteren
 * veröffentlichten Version, weiterverbreiten und/oder modifizieren.
 *
 * MPL wird in der Hoffnung, dass es nützlich sein wird, aber OHNE JEDE GEWÄHRLEISTUNG,
 * bereitgestellt; sogar ohne die implizite Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN
 * BESTIMMTEN ZWECK. Siehe die GNU General Public License für weitere Details.
 *
 * Sie sollten eine Kopie der GNU General Public License zusammen mit MPL erhalten haben. Wenn
 * nicht, siehe .
 */
package de.adrodoc55.minecraft.mpl.interpretation

import static de.adrodoc55.minecraft.mpl.MplTestBase.someIdentifier
import static de.adrodoc55.minecraft.mpl.ast.chainparts.MplNotify.NOTIFY
import static de.adrodoc55.minecraft.mpl.commands.Conditional.*
import static de.adrodoc55.minecraft.mpl.commands.Mode.*

import org.junit.Test

import spock.lang.Unroll

import com.google.common.collect.ListMultimap

import de.adrodoc55.minecraft.mpl.MplSpecBase
import de.adrodoc55.minecraft.mpl.ast.chainparts.ChainPart
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplBreakpoint
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplCommand
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplIf
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplIntercept
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplNotify
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplStart
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplStop
import de.adrodoc55.minecraft.mpl.ast.chainparts.MplWaitfor
import de.adrodoc55.minecraft.mpl.ast.chainparts.loop.MplBreak;
import de.adrodoc55.minecraft.mpl.ast.chainparts.loop.MplContinue;
import de.adrodoc55.minecraft.mpl.ast.chainparts.loop.MplWhile;
import de.adrodoc55.minecraft.mpl.ast.chainparts.program.MplProcess
import de.adrodoc55.minecraft.mpl.ast.chainparts.program.MplProgram
import de.adrodoc55.minecraft.mpl.commands.Conditional

class MplInterpreterSpec2 extends MplSpecBase {

  static List commandOnlyModifier = ['impulse', 'chain', 'repeat', 'always active', 'needs redstone']

  @Test
  public void "Each file can only define one project"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    project ${id1} {}
    project ${id2} {}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "A file can only contain a single project"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'project'
    program.exceptions[0].source.token.line == 2
    program.exceptions[1].message == "A file can only contain a single project"
    program.exceptions[1].source.file == lastTempFile
    program.exceptions[1].source.token.text == 'project'
    program.exceptions[1].source.token.line == 3
    program.exceptions.size() == 2
  }

  @Test
  public void "A project and processes can be defined in the same file"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    project ${id1} {}
    process ${id2} {}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()
  }

  @Test
  public void "Each project can only have a single orientation"() {
    given:
    String id1 = someIdentifier()
    String programString = """
    project ${id1} {
      orientation "zxy"
      orientation "z-xy"
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "A project can only have a single orientation"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'orientation'
    program.exceptions[0].source.token.line == 3
    program.exceptions[1].message == "A project can only have a single orientation"
    program.exceptions[1].source.file == lastTempFile
    program.exceptions[1].source.token.text == 'orientation'
    program.exceptions[1].source.token.line == 4
    program.exceptions.size() == 2
  }

  @Test
  public void "Each script can only have a single orientation"() {
    given:
    String programString = """
    orientation "zxy"
    orientation "z-xy"
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions[0].message == "A script can only have a single orientation"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'orientation'
    program.exceptions[0].source.token.line == 2
    program.exceptions[1].message == "A script can only have a single orientation"
    program.exceptions[1].source.file == lastTempFile
    program.exceptions[1].source.token.text == 'orientation'
    program.exceptions[1].source.token.line == 3
    program.exceptions.size() == 2
  }

  @Test
  public void "A file may not contain duplicate processes"() {
    given:
    String id = someIdentifier()
    String programString = """
    process ${id} {
    /say I am a process
    }

    process ${id} {
    /say I am the same process
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions[0].message == "Duplicate process ${id}"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == id
    program.exceptions[0].source.token.line == 2
    program.exceptions[1].message == "Duplicate process ${id}"
    program.exceptions[1].source.file == lastTempFile
    program.exceptions[1].source.token.text == id
    program.exceptions[1].source.token.line == 6
    program.exceptions.size() == 2
  }

  @Test
  public void "A file may contain multiple processes"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String id3 = someIdentifier()
    String programString = """
    process ${id1} {
    /say I am a default process
    }
    impulse process ${id2} {
    /say I am an impulse process, wich is actually equivalent to the default
    }
    repeat process ${id3} {
    /say I am a repeating process. I am completely different :)
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    Collection processes = program.processes
    processes.size() == 3

    MplProcess process1 = processes.find { it.name == id1 }
    process1.repeating == false
    List chainParts1 = process1.chainParts
    chainParts1[0] == new MplCommand("/say I am a default process")
    chainParts1.size() == 1

    MplProcess process2 = processes.find { it.name == id2 }
    process2.repeating == false
    List chainParts2 = process2.chainParts
    chainParts2[0] == new MplCommand("/say I am an impulse process, wich is actually equivalent to the default")
    chainParts2.size() == 1

    MplProcess process3 = processes.find { it.name == id3 }
    process3.repeating == true
    List chainParts3 = process3.chainParts
    chainParts3[0] == new MplCommand("/say I am a repeating process. I am completely different :)")
    chainParts3.size() == 1
  }

  @Test
  public void "Multiple install/uninstall blocks are concatenated"() {
    given:
    String id1 = someIdentifier()
    String programString = """
    install {
      /say hi
    }

    install {
      /say hi2
    }

    uninstall {
      /say hi3
    }

    uninstall {
      /say hi4
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.install.chainParts[0] == new MplCommand('/say hi')
    program.install.chainParts[1] == new MplCommand('/say hi2')
    program.install.chainParts.size() == 2

    program.uninstall.chainParts[0] == new MplCommand('/say hi3')
    program.uninstall.chainParts[1] == new MplCommand('/say hi4')
    program.uninstall.chainParts.size() == 2
  }

  // ----------------------------------------------------------------------------------------------------
  //    ___                                 _        __  ___               _             _
  //   |_ _| _ __ ___   _ __    ___   _ __ | |_     / / |_ _| _ __    ___ | | _   _   __| |  ___
  //    | | | '_ ` _ \ | '_ \  / _ \ | '__|| __|   / /   | | | '_ \  / __|| || | | | / _` | / _ \
  //    | | | | | | | || |_) || (_) || |   | |_   / /    | | | | | || (__ | || |_| || (_| ||  __/
  //   |___||_| |_| |_|| .__/  \___/ |_|    \__| /_/    |___||_| |_| \___||_| \__,_| \__,_| \___|
  //                   |_|
  // ----------------------------------------------------------------------------------------------------

  @Test
  void "an interpreter will always include mpl subfiles of it's parent directory, including it's own file"() {
    given:
    File programFile = newTempFile()
    File neighbourFile = new File(programFile.parentFile, 'neighbour.mpl')
    neighbourFile.createNewFile()
    MplInterpreter interpreter = new MplInterpreter(programFile)

    expect:
    interpreter.imports.containsAll([programFile, neighbourFile])
    interpreter.imports.size() == 2
  }

  @Test
  void "an interpreter will not include non mpl subfiles of it's parent directory by default"() {
    given:
    File programFile = newTempFile()
    File neighbourFile = new File(programFile.parentFile, 'neighbour.txt')
    neighbourFile.createNewFile()
    MplInterpreter interpreter = new MplInterpreter(programFile)

    expect:
    interpreter.imports.containsAll([programFile])
    interpreter.imports.size() == 1
  }

  @Test
  void "addFileImport will add a file that is not in the same folder"() {
    given:
    File programFile = newTempFile()
    File otherFile = new File(programFile.parentFile, 'folder/other.txt')
    otherFile.parentFile.mkdirs()
    otherFile.createNewFile()
    MplInterpreter interpreter = new MplInterpreter(programFile)

    when:
    interpreter.addFileImport(null, otherFile)

    then:
    interpreter.imports.containsAll([programFile, otherFile])
    interpreter.imports.size() == 2
  }

  @Test
  void "the same file cannot be imported twice"() {
    given:
    String programString = """
    import "newFolder/newFile.txt"
    import "newFolder/newFile.txt"
    """
    File newFolder = new File(tempFolder.root, "newFolder")
    newFolder.mkdirs()
    File newFile = new File(newFolder, "newFile.txt")
    newFile.createNewFile()

    when:
    MplInterpreter interpreter = interpret(programString)

    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == 'Duplicate import'
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == '"newFolder/newFile.txt"'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  public void "the same file cannot be included twice"() {
    given:
    String programString = """
    project main {
      include "newFolder/newFile.txt"
      include "newFolder/newFile.txt"
    }
    """
    File newFolder = new File(tempFolder.root, "newFolder")
    newFolder.mkdirs()
    File newFile = new File(newFolder, "newFile.txt")
    newFile.createNewFile()

    when:
    MplInterpreter interpreter = interpret(programString)

    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == 'Duplicate include'
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == '"newFolder/newFile.txt"'
    program.exceptions[0].source.token.line == 4
    program.exceptions.size() == 1
  }

  @Test
  public void "a project can include files and directories"() {
    given:
    String id1 = someIdentifier()
    String programString = """
    project ${id1} {
      include "datei1.mpl"
      include "ordner2"
    }
    """
    File folder = tempFolder.root
    new File(folder, 'datei1.mpl').createNewFile()
    new File(folder, 'ordner2').mkdirs()
    new File(folder, 'ordner2/datei4.mpl').createNewFile()
    new File(folder, 'ordner2/datei5.txt').createNewFile()

    when:
    MplInterpreter interpreter = interpret(programString)

    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    File parent = lastTempFile.parentFile

    ListMultimap includeMap = interpreter.includes
    includeMap.size() == 2
    List includes = includeMap.get(null); // null indicates that the whole file should be included
    includes[0].files.containsAll([new File(parent, "datei1.mpl")])
    includes[0].files.size() == 1
    includes[0].processName == null
    includes[1].files.containsAll([new File(parent, "ordner2/datei4.mpl")])
    includes[1].files.size() == 1
    includes[1].processName == null
    includes.size() == 2
  }

  @Test
  public void "starting a foreign process creates an include"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    process ${id1} {
      start ${id2}
    }
    """

    when:
    MplInterpreter interpreter = interpret(programString)

    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    ListMultimap includeMap = interpreter.includes
    includeMap.size() == 1
    List includes = includeMap.get(id1);
    includes[0].files.containsAll([lastTempFile])
    includes[0].files.size() == 1
    includes[0].processName == id2
    includes.size() == 1
  }

  @Test
  public void "starting a foreign process from a script does not create an include"() {
    given:
    String programString = """
    start other
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    interpreter.includes.isEmpty()
  }

  @Test
  public void "imported files are used for implicit includes"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    import "newFolder/newFile"

    process ${id1} {
      start ${id2}
    }
    """
    File file = newTempFile()
    File newFile = new File(file.parentFile, "newFolder/newFile")
    newFile.parentFile.mkdirs()
    newFile.createNewFile()

    when:
    MplInterpreter interpreter = interpret(programString, file)

    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    ListMultimap includeMap = interpreter.includes
    includeMap.size() == 1
    List includes = includeMap.get(id1);
    includes[0].files.containsAll([lastTempFile, newFile])
    includes[0].files.size() == 2
    includes[0].processName == id2
    includes.size() == 1
  }
  @Test
  public void "the subfiles of imported directories are used for implicit includes"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    import "newFolder"

    process ${id1} {
      start ${id2}
    }
    """
    File file = newTempFile()
    File newFolder = new File(file.parentFile, "newFolder")
    newFolder.mkdirs()
    File newFile = new File(newFolder, "newFile.mpl")
    newFile.createNewFile()

    when:
    MplInterpreter interpreter = interpret(programString, file)

    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    ListMultimap includeMap = interpreter.includes
    includeMap.size() == 1
    List includes = includeMap.get(id1);
    includes[0].files.containsAll([lastTempFile, newFile])
    includes[0].files.size() == 2
    includes[0].processName == id2
    includes.size() == 1
  }

  /**
   * Grund hierfür ist: die Abhängigkeiten eines jeden Prozesses müssen durch die Includes
   * dokumentiert werden, da bei imports nur einzelne Prozesse includiert werden und deren
   * Abhängigkeiten sonst verloren gehen würden.
   */
  @Test
  public void "starting a process in the same file creates an include (process definition after call)"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    process ${id1} {
      start ${id2}
    }

    process ${id2} {
      /say I am the second process
    }
    """

    when:
    MplInterpreter interpreter = interpret(programString)

    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    ListMultimap includeMap = interpreter.includes
    includeMap.size() == 1
    List includes = includeMap.get(id1);
    includes[0].files.containsAll([lastTempFile])
    includes[0].files.size() == 1
    includes[0].processName == id2
    includes.size() == 1
  }

  /**
   * Grund hierfür ist: die Abhängigkeiten eines jeden Prozesses müssen durch die Includes
   * dokumentiert werden, da bei imports nur einzelne Prozesse includiert werden und deren
   * Abhängigkeiten sonst verloren gehen würden.
   */
  @Test
  public void "starting a process in the same file creates an include (process definition before call)"() {
    given:
    String id1 = someIdentifier()
    String id2 = someIdentifier()
    String programString = """
    process ${id1} {
      /say I am a process
    }

    process ${id2} {
      start ${id1}
    }
    """

    when:
    MplInterpreter interpreter = interpret(programString)

    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    ListMultimap includeMap = interpreter.includes
    includeMap.size() == 1
    List includes = includeMap.get(id2);
    includes[0].files.containsAll([lastTempFile])
    includes[0].files.size() == 1
    includes[0].processName == id1
    includes.size() == 1
  }

  // ----------------------------------------------------------------------------------------------------
  //    __  __             _  _   __  _
  //   |  \/  |  ___    __| |(_) / _|(_)  ___  _ __
  //   | |\/| | / _ \  / _` || || |_ | | / _ \| '__|
  //   | |  | || (_) || (_| || ||  _|| ||  __/| |
  //   |_|  |_| \___/  \__,_||_||_|  |_| \___||_|
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("leading #conditional #command with identifier in script")
  public void "leading conditional/invert with identifier in script"(String conditional, String command) {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${conditional}: ${command} ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == conditional
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1

    where:
    [conditional, command]<< [['conditional', 'invert'], ['start', 'stop', 'waitfor', 'intercept']].combinations()*.flatten()
  }

  @Test
  @Unroll("leading #conditional #command with identifier in process")
  public void "leading conditional/invert with identifier in process"(String conditional, String command) {
    given:
    String identifier = someIdentifier()
    String programString = """
    process main {
      ${conditional}: ${command} ${identifier}
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == conditional
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1

    where:
    [conditional, command]<< [['conditional', 'invert'], ['start', 'stop', 'waitfor', 'intercept']].combinations()*.flatten()
  }

  @Test
  @Unroll("leading #conditional #command without identifier in script")
  public void "leading conditional/invert without identifier in script"(String conditional, String command) {
    given:
    String programString = """
    ${conditional}: ${command}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == conditional
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1

    where:
    [conditional, command]<< [['conditional', 'invert'], ['breakpoint']].combinations()*.flatten()
  }

  @Test
  @Unroll("leading #conditional #command without identifier in process")
  public void "leading conditional/invert without identifier in process"(String conditional, String command) {
    given:
    String programString = """
    process main {
      ${conditional}: ${command}
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == conditional
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1

    where:
    [conditional, command]<< [['conditional', 'invert'], ['notify', 'breakpoint']].combinations()*.flatten()
  }

  @Test
  public void "leading skip in repeating process"() {
    given:
    String testString = """
    repeat process main {
      skip
    }
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "skip cannot be the first command of a repeating process"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'skip'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  public void "conditional depending on skip throws exception"() {
    given:
    String testString = """
    skip
    conditional: /say conditional
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "conditional cannot depend on skip"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'conditional'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  public void "invert depending on skip throws exception"() {
    given:
    String testString = """
    skip
    invert: /say invert
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "invert cannot depend on skip"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'invert'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  // ----------------------------------------------------------------------------------------------------
  //    ____   _                _
  //   / ___| | |_  __ _  _ __ | |_
  //   \___ \ | __|/ _` || '__|| __|
  //    ___) || |_| (_| || |   | |_
  //   |____/  \__|\__,_||_|    \__|
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier start with identifier")
  public void "start with identifier"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    /say hi
    ${modifier} start ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);
    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = process.chainParts[0]
    }

    process.chainParts[0] == new MplCommand('/say hi')
    process.chainParts[1] == new MplStart(identifier, modifierBuffer, previous)
    process.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  public void "start with identifier in script"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    start ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplStart(identifier)
    process.chainParts.size() == 1
  }

  @Test
  @Unroll("start with illegal modifier: '#modifier'")
  public void "start with illegal modifier"(String modifier) {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${modifier}: start ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for start; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //    ____   _
  //   / ___| | |_  ___   _ __
  //   \___ \ | __|/ _ \ | '_ \
  //    ___) || |_| (_) || |_) |
  //   |____/  \__|\___/ | .__/
  //                     |_|
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier stop with identifier")
  public void "stop with identifier"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    /say hi
    ${modifier} stop ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);
    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = process.chainParts[0]
    }

    process.chainParts[0] == new MplCommand('/say hi')
    process.chainParts[1] == new MplStop(identifier, modifierBuffer, previous)
    process.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  public void "stop without identifier in script"() {
    given:
    String programString = """
    stop
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Missing identifier"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'stop'
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1
  }

  @Test
  public void "stop without identifier in repeat process"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    repeat process ${identifier} {
      stop
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()
    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplStop(identifier)
    process.chainParts.size() == 1
  }

  @Test
  public void "stop without identifier in impulse process"() {
    given:
    String programString = """
    impulse process main {
      stop
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "An impulse process cannot be stopped"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'stop'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  @Unroll("stop with illegal modifier: '#modifier'")
  public void "stop with illegal modifier"(String modifier) {
    given:
    String identifier = someIdentifier()
    String programString = """
    process main {
      ${modifier}: stop ${identifier}
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for stop; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //   __        __      _  _     __
  //   \ \      / /__ _ (_)| |_  / _|  ___   _ __
  //    \ \ /\ / // _` || || __|| |_  / _ \ | '__|
  //     \ V  V /| (_| || || |_ |  _|| (_) || |
  //      \_/\_/  \__,_||_| \__||_|   \___/ |_|
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier waitfor with identifier")
  public void "waitfor with identifier"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    /say hi
    ${modifier} waitfor ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);
    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = process.chainParts[0]
    }

    process.chainParts[0] == new MplCommand('/say hi')
    process.chainParts[1] == new MplWaitfor(identifier, modifierBuffer, previous)
    process.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  public void "waitfor notify with identifier"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    waitfor notify ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWaitfor(identifier + NOTIFY);
    process.chainParts.size() == 1
  }

  @Test
  public void "waitfor without identifier after start"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    start ${identifier}
    waitfor
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplStart(identifier)
    process.chainParts[1] == new MplWaitfor(identifier + NOTIFY);
    process.chainParts.size() == 2
  }

  @Test
  public void "waitfor without identifier without start"() {
    given:
    String programString = """
    waitfor
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Missing identifier; no previous start was found to wait for"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'waitfor'
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1
  }

  @Test
  @Unroll("waitfor with illegal modifier: '#modifier'")
  public void "waitfor with illegal modifier"(String modifier) {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${modifier}: waitfor ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for waitfor; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //    _   _         _    _   __
  //   | \ | |  ___  | |_ (_) / _| _   _
  //   |  \| | / _ \ | __|| || |_ | | | |
  //   | |\  || (_) || |_ | ||  _|| |_| |
  //   |_| \_| \___/  \__||_||_|   \__, |
  //                               |___/
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier notify in process")
  public void "notify in process"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    process ${identifier} {
      /say hi
      ${modifier} notify
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()
    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);
    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = process.chainParts[0]
    }

    process.chainParts[0] == new MplCommand('/say hi')
    process.chainParts[1] == new MplNotify(identifier, modifierBuffer, previous)
    process.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  public void "notify in script"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    notify
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "notify can only be used in a process"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'notify'
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1
  }

  @Test
  @Unroll("notify with illegal modifier: '#modifier'")
  public void "notify with illegal modifier"(String modifier) {
    given:
    String identifier = someIdentifier()
    String programString = """
    process ${identifier} {
      ${modifier}: notify
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for notify; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //    ___         _                                _
  //   |_ _| _ __  | |_  ___  _ __  ___  ___  _ __  | |_
  //    | | | '_ \ | __|/ _ \| '__|/ __|/ _ \| '_ \ | __|
  //    | | | | | || |_|  __/| |  | (__|  __/| |_) || |_
  //   |___||_| |_| \__|\___||_|   \___|\___|| .__/  \__|
  //                                         |_|
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier intercept with identifier")
  public void "intercept with identifier"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    /say hi
    ${modifier} intercept ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);
    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = process.chainParts[0]
    }

    process.chainParts[0] == new MplCommand('/say hi')
    process.chainParts[1] == new MplIntercept(identifier, modifierBuffer, previous)
    process.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  @Unroll("intercept with illegal modifier: '#modifier'")
  public void "intercept with illegal modifier"(String modifier) {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${modifier}: intercept ${identifier}
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for intercept; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //    ____                     _                   _         _
  //   | __ )  _ __  ___   __ _ | | __ _ __    ___  (_) _ __  | |_
  //   |  _ \ | '__|/ _ \ / _` || |/ /| '_ \  / _ \ | || '_ \ | __|
  //   | |_) || |  |  __/| (_| ||   < | |_) || (_) || || | | || |_
  //   |____/ |_|   \___| \__,_||_|\_\| .__/  \___/ |_||_| |_| \__|
  //                                  |_|
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier breakpoint")
  public void "breakpoint"(String modifier, Conditional conditional) {
    given:
    String programString = """
    /say hi
    ${modifier} breakpoint
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);
    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = process.chainParts[0]
    }

    process.chainParts[0] == new MplCommand('/say hi')
    process.chainParts[1] == new MplBreakpoint("${lastTempFile.name} : line 3" , modifierBuffer, previous)
    process.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  @Unroll("breakpoint with illegal modifier: '#modifier'")
  public void "breakpoint with illegal modifier"(String modifier) {
    given:
    String programString = """
    ${modifier}: breakpoint
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for breakpoint; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //    ___   __             _____  _                              _____  _
  //   |_ _| / _|           |_   _|| |__    ___  _ __             | ____|| | ___   ___
  //    | | | |_              | |  | '_ \  / _ \| '_ \            |  _|  | |/ __| / _ \
  //    | | |  _|  _  _  _    | |  | | | ||  __/| | | |  _  _  _  | |___ | |\__ \|  __/
  //   |___||_|   (_)(_)(_)   |_|  |_| |_| \___||_| |_| (_)(_)(_) |_____||_||___/ \___|
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  public void "if with leading conditional in then"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if: /say if
    then {
      conditional: /say then
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'conditional'
    program.exceptions[0].source.token.line == 4
    program.exceptions.size() == 1
  }

  @Test
  public void "if with leading invert in then"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if: /say if
    then {
      invert: /say then
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'invert'
    program.exceptions[0].source.token.line == 4
    program.exceptions.size() == 1
  }

  @Test
  public void "if with leading conditional in else"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if: /say if
    then {
    } else {
      conditional: /say else
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'conditional'
    program.exceptions[0].source.token.line == 5
    program.exceptions.size() == 1
  }

  @Test
  public void "if with leading invert in else"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if: /say if
    then {
    } else {
      invert: /say else
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'invert'
    program.exceptions[0].source.token.line == 5
    program.exceptions.size() == 1
  }

  @Test
  public void "if then else"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if: /say if
    then {
      /say then1
      /say then2
    } else {
      /say else1
      /say else2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplIf(false, '/say if')
    process.chainParts.size() == 1

    MplIf mplIf = process.chainParts[0]
    mplIf.thenParts[0] == new MplCommand('/say then1')
    mplIf.thenParts[1] == new MplCommand('/say then2')
    mplIf.thenParts.size() == 2
    mplIf.elseParts[0] == new MplCommand('/say else1')
    mplIf.elseParts[1] == new MplCommand('/say else2')
    mplIf.elseParts.size() == 2
  }

  @Test
  public void "if not then else"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if not: /say if
    then {
      /say then1
      /say then2
    } else {
      /say else1
      /say else2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplIf(true, '/say if')
    process.chainParts.size() == 1

    MplIf mplIf = process.chainParts[0]
    mplIf.thenParts[0] == new MplCommand('/say then1')
    mplIf.thenParts[1] == new MplCommand('/say then2')
    mplIf.thenParts.size() == 2
    mplIf.elseParts[0] == new MplCommand('/say else1')
    mplIf.elseParts[1] == new MplCommand('/say else2')
    mplIf.elseParts.size() == 2
  }

  @Test
  public void "nested if"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    if: /outer condition
    then {
      /say outer then1

      if: /inner then condition
      then {
        /say inner then then
      } else {
        /say inner then else
      }

      /say outer then2
    } else {
      /say outer else1

      if: /inner else condition
      then {
        /say inner else then
      } else {
        /say inner else else
      }

      /say outer else2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplIf(false, '/outer condition')
    process.chainParts.size() == 1

    MplIf outerIf = process.chainParts[0]
    outerIf.thenParts[0] == new MplCommand('/say outer then1')
    outerIf.thenParts[1] == new MplIf(false, '/inner then condition')
    outerIf.thenParts[2] == new MplCommand('/say outer then2')
    outerIf.thenParts.size() == 3
    outerIf.elseParts[0] == new MplCommand('/say outer else1')
    outerIf.elseParts[1] == new MplIf(false, '/inner else condition')
    outerIf.elseParts[2] == new MplCommand('/say outer else2')
    outerIf.elseParts.size() == 3

    MplIf innerThenIf = outerIf.thenParts[1]
    innerThenIf.thenParts[0] == new MplCommand('/say inner then then')
    innerThenIf.thenParts.size() == 1
    innerThenIf.elseParts[0] == new MplCommand('/say inner then else')
    innerThenIf.elseParts.size() == 1

    MplIf innerElseIf = outerIf.elseParts[1]
    innerElseIf.thenParts[0] == new MplCommand('/say inner else then')
    innerElseIf.thenParts.size() == 1
    innerElseIf.elseParts[0] == new MplCommand('/say inner else else')
    innerElseIf.elseParts.size() == 1
  }

  // ----------------------------------------------------------------------------------------------------
  //   __        __ _      _  _
  //   \ \      / /| |__  (_)| |  ___
  //    \ \ /\ / / | '_ \ | || | / _ \
  //     \ V  V /  | | | || || ||  __/
  //      \_/\_/   |_| |_||_||_| \___|
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  public void "while repeat with leading conditional in repeat"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    while: /say while
    repeat {
      conditional: /say repeat
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'conditional'
    program.exceptions[0].source.token.line == 4
    program.exceptions.size() == 1
  }

  @Test
  public void "while repeat with leading invert in repeat"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    while: /say while
    repeat {
      invert: /say repeat
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'invert'
    program.exceptions[0].source.token.line == 4
    program.exceptions.size() == 1
  }

  @Test
  public void "repeat while with leading conditional in repeat"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    repeat {
      conditional: /say repeat
    } do while: /say while
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'conditional'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  public void "repeat while with leading invert in repeat"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    repeat {
      invert: /say repeat
    } do while: /say while
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "The first part of a chain must be unconditional"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'invert'
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  public void "repeat"() {
    given:
    String programString = """
    repeat {
      /say repeat1
      /say repeat2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(false, false, null)
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "repeat with label"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${identifier}: repeat {
      /say repeat1
      /say repeat2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(identifier, false, false, null)
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "while repeat"() {
    given:
    String programString = """
    while: /say while
    repeat {
      /say repeat1
      /say repeat2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(false, false, '/say while')
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "while repeat with label"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${identifier}: while: /say while
    repeat {
      /say repeat1
      /say repeat2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(identifier, false, false, '/say while')
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "while not repeat"() {
    given:
    String programString = """
    while not: /say while
    repeat {
      /say repeat1
      /say repeat2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(true, false, '/say while')
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "repeat while"() {
    given:
    String programString = """
    repeat {
      /say repeat1
      /say repeat2
    } do while: /say while
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(false, true, '/say while')
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "repeat while with label"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${identifier}: repeat {
      /say repeat1
      /say repeat2
    } do while: /say while
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(identifier, false, true, '/say while')
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "repeat while not"() {
    given:
    String programString = """
    repeat {
      /say repeat1
      /say repeat2
    } do while not: /say while
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(true, true, '/say while')
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]
    mplWhile.chainParts[0] == new MplCommand('/say repeat1')
    mplWhile.chainParts[1] == new MplCommand('/say repeat2')
    mplWhile.chainParts.size() == 2
  }

  @Test
  public void "nested while"() {
    given:
    String identifier = someIdentifier()
    String programString = """
    while: /outer condition
    repeat {
      /say outer repeat1

      while: /inner condition
      repeat {
        /say inner repeat
      }

      /say outer repeat2
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    process.chainParts[0] == new MplWhile(false, false, '/outer condition')
    process.chainParts.size() == 1

    MplWhile outerWhile = process.chainParts[0]
    outerWhile.chainParts[0] == new MplCommand('/say outer repeat1')
    outerWhile.chainParts[1] == new MplWhile(false, false, '/inner condition')
    outerWhile.chainParts[2] == new MplCommand('/say outer repeat2')
    outerWhile.chainParts.size() == 3

    MplWhile innerWhile = outerWhile.chainParts[1]
    innerWhile.chainParts[0] == new MplCommand('/say inner repeat')
    innerWhile.chainParts.size() == 1
  }

  // ----------------------------------------------------------------------------------------------------
  //    ____                     _
  //   | __ )  _ __  ___   __ _ | | __
  //   |  _ \ | '__|/ _ \ / _` || |/ /
  //   | |_) || |  |  __/| (_| ||   <
  //   |____/ |_|   \___| \__,_||_|\_\
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier break with identifier")
  public void "break with identifier"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${identifier}: repeat {
      /say hi
      ${modifier} break ${identifier}
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);

    process.chainParts[0] == new MplWhile(false, false, null)
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]

    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = mplWhile.chainParts[0]
    }

    mplWhile.chainParts[0] == new MplCommand('/say hi')
    mplWhile.chainParts[1] == new MplBreak(identifier, mplWhile, modifierBuffer, previous)
    mplWhile.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  @Unroll("#modifier break without identifier")
  public void "break without identifier"(String modifier, Conditional conditional) {
    given:
    String programString = """
    repeat {
      /say hi
      ${modifier} break
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);

    process.chainParts[0] == new MplWhile(false, false, null)
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]

    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = mplWhile.chainParts[0]
    }

    mplWhile.chainParts[0] == new MplCommand('/say hi')
    mplWhile.chainParts[1] == new MplBreak(null, mplWhile, modifierBuffer, previous)
    mplWhile.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  public void "break outside of loop"() {
    given:
    String testString = """
    break
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "break can only be used in a loop"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'break'
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1
  }

  @Test
  public void "break with missing label"() {
    given:
    String identifier = someIdentifier()
    String testString = """
    repeat {
      break ${identifier}
    }
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Missing label ${identifier}"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == identifier
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  @Unroll("break with illegal modifier: '#modifier'")
  public void "break with illegal modifier"(String modifier) {
    given:
    String programString = """
    repeat {
      ${modifier}: break
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for break; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

  // ----------------------------------------------------------------------------------------------------
  //     ____               _    _
  //    / ___| ___   _ __  | |_ (_) _ __   _   _   ___
  //   | |    / _ \ | '_ \ | __|| || '_ \ | | | | / _ \
  //   | |___| (_) || | | || |_ | || | | || |_| ||  __/
  //    \____|\___/ |_| |_| \__||_||_| |_| \__,_| \___|
  //
  // ----------------------------------------------------------------------------------------------------

  @Test
  @Unroll("#modifier continue with identifier")
  public void "continue with identifier"(String modifier, Conditional conditional) {
    given:
    String identifier = someIdentifier()
    String programString = """
    ${identifier}: repeat {
      /say hi
      ${modifier} continue ${identifier}
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);

    process.chainParts[0] == new MplWhile(false, false, null)
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]

    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = mplWhile.chainParts[0]
    }

    mplWhile.chainParts[0] == new MplCommand('/say hi')
    mplWhile.chainParts[1] == new MplContinue(identifier, mplWhile, modifierBuffer, previous)
    mplWhile.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  @Unroll("#modifier continue without identifier")
  public void "continue without identifier"(String modifier, Conditional conditional) {
    given:
    String programString = """
    repeat {
      /say hi
      ${modifier} continue
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program
    program.exceptions.isEmpty()

    program.processes.size() == 1
    MplProcess process = program.processes.first()

    ModifierBuffer modifierBuffer = new ModifierBuffer()
    modifierBuffer.setConditional(conditional);

    process.chainParts[0] == new MplWhile(false, false, null)
    process.chainParts.size() == 1

    MplWhile mplWhile = process.chainParts[0]

    ChainPart previous = null
    if (conditional != UNCONDITIONAL) {
      previous = mplWhile.chainParts[0]
    }

    mplWhile.chainParts[0] == new MplCommand('/say hi')
    mplWhile.chainParts[1] == new MplContinue(null, mplWhile, modifierBuffer, previous)
    mplWhile.chainParts.size() == 2
    where:
    modifier        | conditional
    ''              | UNCONDITIONAL
    'unconditional:'| UNCONDITIONAL
    'conditional:'  | CONDITIONAL
    'invert:'       | INVERT
  }

  @Test
  public void "continue outside of loop"() {
    given:
    String testString = """
    continue
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "continue can only be used in a loop"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == 'continue'
    program.exceptions[0].source.token.line == 2
    program.exceptions.size() == 1
  }

  @Test
  public void "continue with missing label"() {
    given:
    String identifier = someIdentifier()
    String testString = """
    repeat {
      continue ${identifier}
    }
    """
    when:
    MplInterpreter interpreter = interpret(testString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Missing label ${identifier}"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == identifier
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1
  }

  @Test
  @Unroll("continue with illegal modifier: '#modifier'")
  public void "continue with illegal modifier"(String modifier) {
    given:
    String programString = """
    repeat {
      ${modifier}: continue
    }
    """
    when:
    MplInterpreter interpreter = interpret(programString)
    then:
    MplProgram program = interpreter.program

    program.exceptions[0].message == "Illegal modifier for continue; only unconditional, conditional and invert are permitted"
    program.exceptions[0].source.file == lastTempFile
    program.exceptions[0].source.token.text == modifier
    program.exceptions[0].source.token.line == 3
    program.exceptions.size() == 1

    where:
    modifier << commandOnlyModifier
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy