diff --git a/build_helper/precommit.pl b/build_helper/precommit.pl
index c4bf7bc..9f94f0c 100644
--- a/build_helper/precommit.pl
+++ b/build_helper/precommit.pl
@@ -98,22 +98,29 @@
my $branch_suffix = 'master' eq $branch ? '' : '-' . $branch;
# get OLD major and minor version number
-my $major_version = undef;
-my $minor_version = undef;
+my $major_version = undef;
+my $middle_version = undef;
+my $minor_version = undef;
open my $fh, '<', $java_file or die "Cannot read $java_file: $!\n";
while (my $line = <$fh>) {
if ($line =~ /(\bint\s+VERSION_MAJOR\s*=)\s*(\d+)/) {
$major_version = $2;
}
+ elsif ($line =~ /(\bint\s+VERSION_MIDDLE\s*=)\s*(\d+)/) {
+ $middle_version = $2;
+ }
elsif ($line =~ /(\bint\s+VERSION_MINOR\s*=)\s*(\-?\d+)/) {
$minor_version = $2;
}
- last if defined $major_version && defined $minor_version;
+ last if defined $major_version && defined $middle_version && defined $minor_version;
}
close $fh or die "Cannot close $java_file: $!\n";
if (! defined $major_version) {
die "Did not find major version in $java_file.\n";
}
+if (! defined $middle_version) {
+ die "Did not find middle version in $java_file.\n";
+}
if (! defined $minor_version) {
die "Did not find minor version in $java_file.\n";
}
@@ -123,7 +130,7 @@
$minor_version++;
}
my $commit_time = time();
-my $version = $major_version . '.' . $minor_version . $branch_suffix;
+my $version = $major_version . '.' . $middle_version . '.' . $minor_version . $branch_suffix;
# Build up the commands to replace VERSION_MINOR, BRANCH and COMMIT_TIME in Midica.java.
# 1. perl and options and opening single quote (') for the regex
diff --git a/midica.jar b/midica.jar
index 292c827..6a3d0de 100644
Binary files a/midica.jar and b/midica.jar differ
diff --git a/src/org/midica/Midica.java b/src/org/midica/Midica.java
index 4793f8f..0f79754 100644
--- a/src/org/midica/Midica.java
+++ b/src/org/midica/Midica.java
@@ -23,23 +23,26 @@
public class Midica {
/** Major version number. This has to be incremented manually. */
- public static final int VERSION_MAJOR = 1;
+ private static final int VERSION_MAJOR = 1;
+
+ /** Middle version number. This has to be incremented manually. */
+ private static final int VERSION_MIDDLE = 3;
/**
* Minor version number. This is intended to be incremented automatically by precommit.pl.
* After switching to a new major version, this has to be set to "-1" manually, so that
* precommit.pl starts with "0" again.
*/
- public static final int VERSION_MINOR = 3;
+ private static final int VERSION_MINOR = 1;
/** UNIX timestamp of the last commit */
- public static final int COMMIT_TIME = 1618087070;
+ public static final int COMMIT_TIME = 1619630078;
/** Branch name. Automatically changed by precommit.pl */
public static final String BRANCH = "master";
/** Full version string. */
- public static final String VERSION = VERSION_MAJOR + "." + VERSION_MINOR + ("master".equals(BRANCH) ? "" : "-" + BRANCH);
+ public static final String VERSION = VERSION_MAJOR + "." + VERSION_MIDDLE + "." + VERSION_MINOR + ("master".equals(BRANCH) ? "" : "-" + BRANCH);
/** Author name */
public static final String AUTHOR = "Jan Trukenmüller";
diff --git a/src/org/midica/config/Dict.java b/src/org/midica/config/Dict.java
index c875d63..bd2ff8c 100644
--- a/src/org/midica/config/Dict.java
+++ b/src/org/midica/config/Dict.java
@@ -1494,6 +1494,7 @@ public class Dict {
public static final String ERROR_CHORD_CONTAINS_ALREADY = "error_chord_contains_already";
public static final String ERROR_CHORD_DEF_NOT_ALLOWED_HERE = "error_chord_def_not_allowed_here";
public static final String ERROR_CHORD_NUM_OF_ARGS = "error_chord_num_of_args";
+ public static final String ERROR_CHORD_REDUNDANT_SEP = "error_chord_redundant_sep";
public static final String ERROR_CONST_NUM_OF_ARGS = "error_const_num_of_args";
public static final String ERROR_CONST_ALREADY_DEFINED = "error_const_already_defined";
public static final String ERROR_CONST_NAME_EQ_VALUE = "error_const_name_eq_value";
@@ -1529,6 +1530,7 @@ public class Dict {
public static final String ERROR_PATTERN_INDEX_INVALID = "error_pattern_index_invalid";
public static final String ERROR_PATTERN_INDEX_TOO_HIGH = "error_pattern_index_too_high";
public static final String ERROR_PATTERN_RECURSION_DEPTH = "error_pattern_recursion_depth";
+ public static final String ERROR_PATTERN_UNDEFINED = "error_pattern_undefined";
public static final String ERROR_META_NUM_OF_ARGS = "error_meta_num_of_arts";
public static final String ERROR_META_UNKNOWN_CMD = "error_meta_unknown_cmd";
public static final String ERROR_SOFT_KARAOKE_UNKNOWN_CMD = "error_soft_karaoke_unknown_cmd";
@@ -3034,6 +3036,7 @@ private static void initLanguageEnglish() {
set( ERROR_CHORD_CONTAINS_ALREADY, "Note cannot be defined more than once in the same chord: " );
set( ERROR_CHORD_DEF_NOT_ALLOWED_HERE, "a chord definition is not allowed inside a block
maybe you forgot to close the block." );
set( ERROR_CHORD_NUM_OF_ARGS, "wrong number of arguments in CHORD command" );
+ set( ERROR_CHORD_REDUNDANT_SEP, "redundant separator in chord definition" );
set( ERROR_CONST_NUM_OF_ARGS, "wrong number of arguments in CONSTANT definition" );
set( ERROR_CONST_ALREADY_DEFINED, "constant already defined: " );
set( ERROR_CONST_NAME_EQ_VALUE, "constant name must be different from it's value: " );
@@ -3069,6 +3072,7 @@ private static void initLanguageEnglish() {
set( ERROR_PATTERN_INDEX_INVALID, "pattern index not a number: " );
set( ERROR_PATTERN_INDEX_TOO_HIGH, "pattern index too high: " );
set( ERROR_PATTERN_RECURSION_DEPTH, "Recursion depth in pattern too big." );
+ set( ERROR_PATTERN_UNDEFINED, "Pattern not defined: " );
set( ERROR_META_NUM_OF_ARGS, "no arguments allowed in meta command" );
set( ERROR_META_UNKNOWN_CMD, "unknown meta command: " );
set( ERROR_SOFT_KARAOKE_UNKNOWN_CMD, "unknown soft karaoke command: " );
diff --git a/src/org/midica/config/Laf.java b/src/org/midica/config/Laf.java
index 5ea9d6b..c857fda 100644
--- a/src/org/midica/config/Laf.java
+++ b/src/org/midica/config/Laf.java
@@ -106,7 +106,7 @@ public class Laf {
public static final Color COLOR_BORDER = new Color( 23, 121, 186 );
private static final Color COLOR_BORDER_LIGHT = new Color( 148, 204, 242 );
private static final Color COLOR_ALERT = new Color( 255, 0, 0 );
- public static final Color COLOR_PANEL = new Color( 238, 238, 238 );
+ public static final Color COLOR_PANEL = new Color( 243, 240, 233 );
private static final Color COLOR_WHITE = new Color( 255, 255, 255 );
private static final Color COLOR_BLACK = new Color( 0, 0, 0 );
private static final Color COLOR_INACTIVE = new Color( 228, 228, 228 );
diff --git a/src/org/midica/file/read/MidicaPLParser.java b/src/org/midica/file/read/MidicaPLParser.java
index 5b04c41..9c8c662 100644
--- a/src/org/midica/file/read/MidicaPLParser.java
+++ b/src/org/midica/file/read/MidicaPLParser.java
@@ -243,6 +243,10 @@ public class MidicaPLParser extends SequenceParser {
private static Pattern crlfSkPattern = null;
private static Pattern sharpPattern = null;
private static Pattern flatPattern = null;
+ private static Pattern optAssignPattern = null;
+ private static Pattern chordAssignPattern = null;
+ private static Pattern varAssignPattern = null;
+ private static Pattern chordSepPattern = null;
private static boolean isSoftKaraoke = false;
private static boolean isDefineParsRun = false; // parsing run for define commands
@@ -636,6 +640,18 @@ private void compilePatterns() {
// find sharp(s) or flat(s) in a note name
sharpPattern = Pattern.compile("^.+" + Pattern.quote(Config.getConfiguredSharpOrFlat(true)) + ".*");
flatPattern = Pattern.compile("^.+" + Pattern.quote(Config.getConfiguredSharpOrFlat(false)) + ".*");
+
+ // splitting option name from option value: '=' and/or whitespace(s)
+ optAssignPattern = Pattern.compile("\\s*" + Pattern.quote(OPT_ASSIGNER) + "\\s*|\\s+");
+
+ // splitting chord name from chord: '=' and/or whitespace(s)
+ chordAssignPattern = Pattern.compile("\\s*" + Pattern.quote(CHORD_ASSIGNER) + "\\s*|\\s+");
+
+ // splitting variable name from assignment: '=' and/or whitespace(s)
+ varAssignPattern = Pattern.compile("\\s*" + Pattern.quote(VAR_ASSIGNER) + "\\s*|\\s+");
+
+ // splitting notes of the chord: ',' and/or whitespace(s)
+ chordSepPattern = Pattern.compile("\\s*" + Pattern.quote(CHORD_SEPARATOR) + "\\s*|\\s+");
}
/**
@@ -653,19 +669,17 @@ public void parseLine(String line) throws ParseException {
line = replaceConstants(line);
}
- // replace variables?
- boolean mustReplaceVars = isDefaultParsRun // only in the default run
- && MODE_DEFAULT == currentMode // not inside of functions
- && 0 == nestableBlkDepth; // not inside of a block
- if (mustReplaceVars) {
- String[] tokens = line.split("\\s+", 2);
+ currentLineContent = line;
+
+ // replace variables in stack traces
+ // (the real replacement will be later)
+ if (isDefaultParsRun // only in the default run
+ && MODE_DEFAULT == currentMode // not inside of functions
+ && 0 == nestableBlkDepth // not inside of a block
+ && line.startsWith(VAR_SYMBOL)) { // not for VAR definitions
- // inside of VAR definitions the replacement is done later
- if (! VAR.equals(tokens[0])) {
- line = replaceVariables(line);
- }
+ currentLineContent = replaceVariables(currentLineContent);
}
- currentLineContent = line;
String[] tokens = line.split("\\s+", 3);
@@ -847,6 +861,16 @@ private void parsingRun(ArrayList lines) throws ParseException, IOExcept
*/
private void parseTokens(String[] tokens) throws ParseException {
+ // replace variables for channel commands
+ if (isDefaultParsRun && MODE_DEFAULT == currentMode && tokens.length > 2) {
+
+ // channel command?
+ if (tokens[0].matches("^\\d{1,2}$") || tokens[0].contains(VAR_SYMBOL)) {
+ tokens[0] = replaceVariables(tokens[0]); // replace channel
+ tokens[1] = replaceVariables(tokens[1]); // replace note/chord
+ }
+ }
+
// reset if/elsif/else conditions in blocks
if (isDefaultParsRun
&& 0 == nestableBlkDepth
@@ -1945,6 +1969,11 @@ else if (BLOCK_CLOSE.equals(cmd)) {
throw new ParseException(Dict.get(Dict.ERROR_BLOCK_UNMATCHED_CLOSE));
}
+ // nothing more needed for functions
+ if (isFuncParsRun) {
+ return;
+ }
+
// construct options string from tokens
StringBuilder optionsStrBuf = new StringBuilder("");
String optionsStr = "";
@@ -1965,8 +1994,10 @@ else if (BLOCK_CLOSE.equals(cmd)) {
String tuplet = null;
boolean mustCheckChain = isDefaultParsRun && nestableBlkDepth == 0;
if (optionsStr.length() > 0) {
+
ArrayList options = parseOptions(optionsStr, isFake);
for (CommandOption opt : options) {
+
String optName = opt.getName();
if (OPT_QUANTITY.equals(optName))
quantity = opt.getQuantity();
@@ -2151,7 +2182,7 @@ else if (3 == tokens.length) {
throw new ParseException(Dict.get(Dict.ERROR_CHORD_NUM_OF_ARGS));
// get and process chord name
- String[] chordParts = chordDef.split("[" + Pattern.quote(CHORD_ASSIGNER) + "\\s]+", 2); // chord name and chords can be separated by CHORD_ASSIGNER (e,g, "=") and/or whitespace(s)
+ String[] chordParts = chordAssignPattern.split(chordDef, 2); // element 0: name; element 1: value
if (chordParts.length < 2) {
throw new ParseException(Dict.get(Dict.ERROR_CHORD_NUM_OF_ARGS));
}
@@ -2169,9 +2200,13 @@ else if (Dict.percussionExists(chordName)) {
// get and process chord elements
TreeSet chord = new TreeSet<>();
- String[] notes = chordValue.split("[" + CHORD_SEPARATOR + "\\s]+"); // notes of the chord can be separated by CHORD_SEPARATOR (e,g, "=") and/or whitespace(s)
+ String[] notes = chordSepPattern.split(chordValue, -1);
for (String note : notes) {
+
+ if ("".equals(note))
+ throw new ParseException(Dict.get(Dict.ERROR_CHORD_REDUNDANT_SEP));
+
int noteVal = parseNote(note);
if (chord.contains(noteVal)) {
throw new ParseException(Dict.get(Dict.ERROR_CHORD_CONTAINS_ALREADY) + note);
@@ -2214,8 +2249,10 @@ private void parseCALL(String[] tokens, boolean isFake) throws ParseException {
boolean multiple = false;
String condIf = null;
if (optionString != null && ! "".equals(optionString)) {
+
ArrayList options = parseOptions(optionString, isFake);
for (CommandOption opt : options) {
+
String optName = opt.getName();
if (OPT_QUANTITY.equals(optName))
quantity = opt.getQuantity();
@@ -2383,11 +2420,11 @@ private void parsePatternCall(String[] tokens, boolean isFake) throws ParseExcep
int outerShift = 0;
String outerSyllable = null;
if (outerOptStr != null) {
- ArrayList callOptions = parseOptions(outerOptStr, false);
+ ArrayList callOptions = parseOptions(outerOptStr, false);
for (CommandOption opt : callOptions) {
- String optName = opt.getName();
+ String optName = opt.getName();
if (OPT_VELOCITY.equals(optName)) {
int velocity = opt.getVelocity();
if (! isFake)
@@ -2483,7 +2520,13 @@ else if (OPT_SHIFT.equals(optName)) {
// replace variables
currentLineContent = patternLine;
- patternLine = replaceVariables(patternLine);
+ String[] rawPatLineTokens = whitespace.split(patternLine);
+ if (BLOCK_OPEN.equals(rawPatLineTokens[0]) || BLOCK_CLOSE.equals(rawPatLineTokens[0])) {
+ // variables will be replaced later
+ }
+ else {
+ patternLine = replaceVariables(patternLine);
+ }
currentLineContent = patternLine;
// special line inside the pattern?
@@ -2516,11 +2559,11 @@ else if (OPT_SHIFT.equals(optName)) {
Float innerDuration = null;
String innerTremolo = null;
if (patternTokens.length > 2) {
- ArrayList patternOptions = parseOptions(patternTokens[2], false);
+ ArrayList patternOptions = parseOptions(patternTokens[2], false);
for (CommandOption opt : patternOptions) {
- String optName = opt.getName();
+ String optName = opt.getName();
if (OPT_VELOCITY.equals(optName)) {
innerVelocity = opt.getVelocity();
}
@@ -2638,6 +2681,10 @@ private Object[] parseParameters(String paramStr) throws ParseException {
HashMap paramsNamed = new HashMap<>();
if (paramStr != null && ! "".equals(paramStr)) {
+
+ if (isDefaultParsRun)
+ paramStr = replaceVariables(paramStr);
+
paramStr = clean(paramStr);
String[] params = paramStr.split("\\s*" + Pattern.quote(PARAM_SEPARATOR) + "\\s*", -1);
for (String rawParam : params) {
@@ -2736,11 +2783,11 @@ else if (REST.equals(tokens[0])) {
// check options
if (tokens.length > 2) {
- ArrayList patternOptions = parseOptions(tokens[2], true);
+ ArrayList patternOptions = parseOptions(tokens[2], true);
for (CommandOption opt : patternOptions) {
- String optName = opt.getName();
+ String optName = opt.getName();
if (OPT_VELOCITY.equals(optName) || OPT_DURATION.equals(optName) || OPT_MULTIPLE.equals(optName)
|| OPT_QUANTITY.equals(optName) || OPT_TREMOLO.equals(optName)) {
// ok
@@ -2760,7 +2807,7 @@ else if (0 == tokens.length) {
}
/**
- * Ensured that (nested or real) pattern call tokens contain the pattern call
+ * Ensures that (nested or real) pattern call tokens contain the pattern call
* and the options token in the right position.
*
* For real pattern calls:
@@ -2779,8 +2826,9 @@ else if (0 == tokens.length) {
* @param tokens tokens to be checked or changed
* @param indexOffset pattern call index (**2** for real calls, **1** for nested calls)
* @return the corrected tokens, if changed, or the original tokens, if no change was necessary.
+ * @throws ParseException on syntax errors.
*/
- private String[] reorganizePatternCallTokens(String[] tokens, int indexOffset) {
+ private String[] reorganizePatternCallTokens(String[] tokens, int indexOffset) throws ParseException {
int iPat = indexOffset; // index containing the pattern call
int iOpt = indexOffset + 1; // index containing the options
@@ -2797,8 +2845,15 @@ private String[] reorganizePatternCallTokens(String[] tokens, int indexOffset) {
String paramString = patCallMatcher.group(3);
String options = patCallMatcher.group(4);
- // unknown pattern name (probably a duration) - don't change anything
+ // unknown pattern name? (maybe a length)
if (! definedPatternNames.contains(patternName)) {
+
+ // parameters? - not existing pattern name
+ if (paramString != null)
+ throw new ParseException(Dict.get(Dict.ERROR_PATTERN_UNDEFINED) + patternName);
+
+ // probably a note length
+ // don't change anything
return tokens;
}
@@ -3136,7 +3191,7 @@ else if (3 == tokens.length) {
}
// split definition string by OPT_ASSIGNER (e,g, "=") and/or whitespace(s)
- String[] defParts = def.split("[" + Pattern.quote(OPT_ASSIGNER) + "\\s]+", 2);
+ String[] defParts = def.split("\\s*" + Pattern.quote(OPT_ASSIGNER) + "\\s*|\\s+", 2);
if (defParts.length < 2)
throw new ParseException(Dict.get(Dict.ERROR_DEFINE_NUM_OF_ARGS));
String cmdId = clean(defParts[0]);
@@ -3277,7 +3332,7 @@ private void parseCONST(String[] tokens) throws ParseException {
}
// CONST with name but without value?
- String[] assignParts = tokens[1].split("[" + Pattern.quote(VAR_ASSIGNER) + "\\s]+", 2); // const name and value can be separated by "=" and/or whitespace(s)
+ String[] assignParts = tokens[1].split("\\s*" + Pattern.quote(VAR_ASSIGNER) + "\\s*|\\s+", 2); // const name and value can be separated by "=" and/or whitespace(s)
if (assignParts.length < 2) {
throw new ParseException(Dict.get(Dict.ERROR_CONST_NUM_OF_ARGS));
}
@@ -3318,7 +3373,7 @@ private void parseVAR(String[] tokens, boolean isFake) throws ParseException {
}
// VAR with name but without value?
- String[] assignParts = tokens[1].split("[" + Pattern.quote(VAR_ASSIGNER) + "\\s]+", 2); // var name and value can be separated by "=" and/or whitespace(s)
+ String[] assignParts = varAssignPattern.split(tokens[1], 2); // element 0: name; element 1: value
if (assignParts.length < 2) {
throw new ParseException(Dict.get(Dict.ERROR_VAR_NUM_OF_ARGS));
}
@@ -3910,6 +3965,10 @@ private void parseChannelCmd(String[] tokens, boolean isFake) throws ParseExcept
if (0 == subTokens.length)
throw new ParseException(Dict.get(Dict.ERROR_CH_CMD_NUM_OF_ARGS));
+ // replace variables in duration
+ if (isDefaultParsRun && ! isFake)
+ subTokens[0] = replaceVariables(subTokens[0]);
+
// process duration
String durationStr = subTokens[0];
int duration;
@@ -3943,11 +4002,11 @@ else if (patterns.containsKey(durationStr)) {
int shift = 0;
String syllable = null;
if (2 == subTokens.length) {
- ArrayList options = parseOptions(subTokens[1], isFake);
+ ArrayList options = parseOptions(subTokens[1], isFake);
for (CommandOption opt : options) {
- String optName = opt.getName();
+ String optName = opt.getName();
if (OPT_VELOCITY.equals(optName)) {
int velocity = opt.getVelocity();
if (! isFake)
@@ -4071,142 +4130,197 @@ else if (currentDuration > tremolo) {
/**
* Parses the options part of a command.
*
- * @param optString The options string of the channel command to be parsed.
- * @param isFake **true**, if this is called inside a function definition, pattern definition or block.
- * @return All options and their values that have been found in the
- * provided options string.
- * @throws ParseException If the command cannot be parsed.
+ * The options may still contain unreplaced variables or parameters.
+ * These are replaced or left as-is. This depends on the current parsing run,
+ * the given **isFake** parameter, and other criteria.
+ *
+ * @param optString The raw options string of the command to be parsed.
+ * @param isFake **true**, if this is called inside a function definition, pattern definition or block.
+ * @return All options and their values that have been found in the
+ * provided options string.
+ * @throws ParseException if the options cannot be parsed.
*/
private ArrayList parseOptions(String optString, boolean isFake) throws ParseException {
- ArrayList options = new ArrayList<>();
+ // split (raw) option string (variables are not yet replaced)
+ ArrayList rawOptTokens = new ArrayList<>();
String[] optTokens = optString.split(OPT_SEPARATOR, -1);
for (String opt : optTokens) {
opt = clean(opt);
- String[] optParts = opt.split("[" + Pattern.quote(OPT_ASSIGNER) + "\\s]+", 2); // name and value can be separated by OPT_ASSIGNER (e,g, "=") and/or whitespace(s)
- if (optParts.length > 2)
- throw new ParseException(Dict.get(Dict.ERROR_CANT_PARSE_OPTIONS) + opt);
- // value is a variable? - don't check if this is fake
- if (isFake && optParts.length > 1 && varPattern.matcher(optParts[1]).find())
- return options;
+ // separate name and value of one option
+ String[] optParts = optAssignPattern.split(opt, 2);
- // construct name and value
- String optName = optParts[0];
- CommandOption optValue = new CommandOption();
-
- if (isCondCheckParsRun && ! IF.equals(optName) && ! ELSIF.equals(optName) && ! ELSE.equals(optName))
- continue;
+ rawOptTokens.add(optParts);
+ }
+
+ // parse the options
+ ArrayList options = new ArrayList<>();
+ for (String[] rawOptParts : rawOptTokens) {
- if (V.equals(optName) || VELOCITY.equals(optName)) {
- if (optParts.length < 2 || "".equals(optParts[1])) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
- }
- optName = OPT_VELOCITY;
- int val = toInt(optParts[1]);
- if (val > 127)
- throw new ParseException(Dict.get(Dict.ERROR_VEL_NOT_MORE_THAN_127));
- if (val < 1)
- throw new ParseException(Dict.get(Dict.ERROR_VEL_NOT_LESS_THAN_1));
- optValue.set(optName, val);
- }
- else if (D.equals(optName) || DURATION.equals(optName)) {
- if (optParts.length < 2 || "".equals(optParts[1])) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
- }
- optName = OPT_DURATION;
- String[] valueParts = optParts[1].split(Pattern.quote(DURATION_PERCENT), -1);
- float val = toFloat(valueParts[0]);
- if (valueParts.length > 1)
- val /= 100; // percentage --> numeric
- if (val <= 0.0)
- throw new ParseException(Dict.get(Dict.ERROR_DURATION_MORE_THAN_0));
- optValue.set(optName, val);
- }
- else if (Q.equals(optName) || QUANTITY.equals(optName)) {
- if (optParts.length < 2 || "".equals(optParts[1])) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
- }
- optName = OPT_QUANTITY;
- optValue.set(optName, toInt(optParts[1], true));
- }
- else if (M.equals(optName) || MULTIPLE.equals(optName)) {
- optName = OPT_MULTIPLE;
- optValue.set(optName, true);
- if (optParts.length > 1)
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_VAL_NOT_ALLOWED) + optName);
- }
- else if (L.equals(optName) || LYRICS.equals(optName)) {
- if (optParts.length < 2 || "".equals(optParts[1])) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
- }
- optName = OPT_LYRICS;
- optValue.set(optName, optParts[1]);
- }
- else if (T.equals(optName) || TUPLET.equals(optName)) {
- optName = OPT_TUPLET;
- if (1 == optParts.length) {
- optValue.set(optName, TRIPLET);
+ CommandOption opt;
+ if (isDefaultParsRun) {
+ if (OPT_IF.equals(rawOptParts[0]) || OPT_ELSIF.equals(rawOptParts[0])) {
+ opt = parseOption(rawOptParts, isFake);
+ if (null == opt)
+ continue;
+
+ String condition = opt.getCondition();
+ condition = replaceVariables(condition);
+ opt.set(opt.getName(), condition);
}
else {
- Pattern pattern = Pattern.compile("^(\\d+)" + Pattern.quote(TUPLET_FOR) + "(\\d+)$");
- Matcher matcher = pattern.matcher(optParts[1]);
- if (matcher.matches()) {
- String num1 = matcher.group(1);
- String num2 = matcher.group(2);
- if ("0".equals(num1) || "0".equals(num2))
- throw new ParseException(Dict.get(Dict.ERROR_TUPLET_INVALID) + optParts[1]);
- optValue.set(optName, optParts[1]);
- }
- else {
- throw new ParseException(Dict.get(Dict.ERROR_TUPLET_INVALID) + optParts[1]);
+ if (rawOptParts.length > 1) {
+ rawOptParts[1] = replaceVariables(rawOptParts[1]);
}
+ opt = parseOption(rawOptParts, isFake);
}
}
- else if (TR.equals(optName) || TREMOLO.equals(optName)) {
- if (optParts.length < 2 || "".equals(optParts[1])) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
- }
- optName = OPT_TREMOLO;
- optValue.setValueString(optParts[1]);
- optValue.set(optName, parseDuration(optParts[1]));
+ else {
+ opt = parseOption(rawOptParts, isFake);
}
- else if (S.equals(optName) || SHIFT.equals(optName)) {
- if (optParts.length < 2 || "".equals(optParts[1])) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
- }
- optName = OPT_SHIFT;
- optValue.set(optName, toInt(optParts[1], false));
+
+ if (opt != null)
+ options.add(opt);
+ }
+
+ return options;
+ }
+
+ /**
+ * Parses one single command option.
+ *
+ * The given optParts may have one or two elements.
+ *
+ * The first element is the option name. The second element is the option's value.
+ *
+ * @param optParts first element: **name**; second element: **value** (optional)
+ * @param isFake **true**, if this is called inside a function definition, pattern definition or block.
+ * @return a parsed option, or **null** if it's too early to parse the option.
+ * @throws ParseException if the options cannot be parsed.
+ */
+ CommandOption parseOption(String[] optParts, boolean isFake) throws ParseException {
+ if (optParts.length > 2)
+ throw new ParseException(Dict.get(Dict.ERROR_CANT_PARSE_OPTIONS) + String.join(" ", optParts));
+
+ // value is a variable? - don't check if this is fake
+ if (isFake && optParts.length > 1 && varPattern.matcher(optParts[1]).find())
+ return null;
+
+ // construct name and value
+ String optName = optParts[0];
+ CommandOption cmdOpt = new CommandOption();
+
+ if (isCondCheckParsRun && ! IF.equals(optName) && ! ELSIF.equals(optName) && ! ELSE.equals(optName))
+ return null;
+
+ if (V.equals(optName) || VELOCITY.equals(optName)) {
+ if (optParts.length < 2 || "".equals(optParts[1])) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ }
+ optName = OPT_VELOCITY;
+ int val = toInt(optParts[1]);
+ if (val > 127)
+ throw new ParseException(Dict.get(Dict.ERROR_VEL_NOT_MORE_THAN_127));
+ if (val < 1)
+ throw new ParseException(Dict.get(Dict.ERROR_VEL_NOT_LESS_THAN_1));
+ cmdOpt.set(optName, val);
+ }
+ else if (D.equals(optName) || DURATION.equals(optName)) {
+ if (optParts.length < 2 || "".equals(optParts[1])) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ }
+ optName = OPT_DURATION;
+ String[] valueParts = optParts[1].split(Pattern.quote(DURATION_PERCENT), -1);
+ float val = toFloat(valueParts[0]);
+ if (valueParts.length > 1)
+ val /= 100; // percentage --> numeric
+ if (val <= 0.0)
+ throw new ParseException(Dict.get(Dict.ERROR_DURATION_MORE_THAN_0));
+ cmdOpt.set(optName, val);
+ }
+ else if (Q.equals(optName) || QUANTITY.equals(optName)) {
+ if (optParts.length < 2 || "".equals(optParts[1])) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ }
+ optName = OPT_QUANTITY;
+ cmdOpt.set(optName, toInt(optParts[1], true));
+ }
+ else if (M.equals(optName) || MULTIPLE.equals(optName)) {
+ optName = OPT_MULTIPLE;
+ cmdOpt.set(optName, true);
+ if (optParts.length > 1)
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_VAL_NOT_ALLOWED) + optName);
+ }
+ else if (L.equals(optName) || LYRICS.equals(optName)) {
+ if (optParts.length < 2 || "".equals(optParts[1])) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ }
+ optName = OPT_LYRICS;
+ cmdOpt.set(optName, optParts[1]);
+ }
+ else if (T.equals(optName) || TUPLET.equals(optName)) {
+ optName = OPT_TUPLET;
+ if (1 == optParts.length) {
+ cmdOpt.set(optName, TRIPLET);
}
- else if (IF.equals(optName)) {
- if (isCondCheckParsRun && (optParts.length < 2 || "".equals(optParts[1]))) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ else {
+ Pattern pattern = Pattern.compile("^(\\d+)" + Pattern.quote(TUPLET_FOR) + "(\\d+)$");
+ Matcher matcher = pattern.matcher(optParts[1]);
+ if (matcher.matches()) {
+ String num1 = matcher.group(1);
+ String num2 = matcher.group(2);
+ if ("0".equals(num1) || "0".equals(num2))
+ throw new ParseException(Dict.get(Dict.ERROR_TUPLET_INVALID) + optParts[1]);
+ cmdOpt.set(optName, optParts[1]);
}
- String val = optParts.length >= 2 ? optParts[1] : "";
- optName = OPT_IF;
- optValue.set(optName, val);
- }
- else if (ELSIF.equals(optName)) {
- if (isCondCheckParsRun && (optParts.length < 2 || "".equals(optParts[1]))) {
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ else {
+ throw new ParseException(Dict.get(Dict.ERROR_TUPLET_INVALID) + optParts[1]);
}
- String val = optParts.length >= 2 ? optParts[1] : "";
- optName = OPT_ELSIF;
- optValue.set(optName, val);
}
- else if (ELSE.equals(optName)) {
- optName = OPT_ELSE;
- optValue.set(optName, true);
- if (optParts.length > 1)
- throw new ParseException(Dict.get(Dict.ERROR_OPTION_VAL_NOT_ALLOWED) + optName);
+ }
+ else if (TR.equals(optName) || TREMOLO.equals(optName)) {
+ if (optParts.length < 2 || "".equals(optParts[1])) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
}
- else {
- throw new ParseException(Dict.get(Dict.ERROR_UNKNOWN_OPTION) + optName);
+ optName = OPT_TREMOLO;
+ cmdOpt.setValueString(optParts[1]);
+ cmdOpt.set(optName, parseDuration(optParts[1]));
+ }
+ else if (S.equals(optName) || SHIFT.equals(optName)) {
+ if (optParts.length < 2 || "".equals(optParts[1])) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
}
- options.add(optValue);
+ optName = OPT_SHIFT;
+ cmdOpt.set(optName, toInt(optParts[1], false));
+ }
+ else if (IF.equals(optName)) {
+ if (isCondCheckParsRun && (optParts.length < 2 || "".equals(optParts[1]))) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ }
+ String val = optParts.length >= 2 ? optParts[1] : "";
+ optName = OPT_IF;
+ cmdOpt.set(optName, val);
+ }
+ else if (ELSIF.equals(optName)) {
+ if (isCondCheckParsRun && (optParts.length < 2 || "".equals(optParts[1]))) {
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_NEEDS_VAL) + optName);
+ }
+ String val = optParts.length >= 2 ? optParts[1] : "";
+ optName = OPT_ELSIF;
+ cmdOpt.set(optName, val);
+ }
+ else if (ELSE.equals(optName)) {
+ optName = OPT_ELSE;
+ cmdOpt.set(optName, true);
+ if (optParts.length > 1)
+ throw new ParseException(Dict.get(Dict.ERROR_OPTION_VAL_NOT_ALLOWED) + optName);
+ }
+ else {
+ throw new ParseException(Dict.get(Dict.ERROR_UNKNOWN_OPTION) + optName);
}
- return options;
+ return cmdOpt;
}
/**
@@ -4370,8 +4484,8 @@ private String addMultiple(String original) {
return subTokens[0] + " " + MULTIPLE;
}
- // cut away trailing whitespaces and option separators (e.g. ',')
- String cleanedOptString = optStr.replaceFirst("[" + Pattern.quote(OPT_SEPARATOR) + "\\s]+$", "");
+ // cut away trailing whitespaces
+ String cleanedOptString = optStr.replaceFirst("\\s+$", "");
// append the option MULTIPLE
subTokens[1] = cleanedOptString + OPT_SEPARATOR + MULTIPLE;
diff --git a/test/org/midica/file/read/MidicaPLParserTest.java b/test/org/midica/file/read/MidicaPLParserTest.java
index 9536ef2..10c5299 100644
--- a/test/org/midica/file/read/MidicaPLParserTest.java
+++ b/test/org/midica/file/read/MidicaPLParserTest.java
@@ -784,6 +784,55 @@ void testParseFilesWorking() throws ParseException {
assertEquals( 23040, instruments.get(5).getCurrentTicks() );
}
+
+ parse(getWorkingFile("condition"));
+ messages = getMessagesByStatus("91");
+ {
+ int i = 0;
+
+ // CALL try_1(/32, ...)
+ assertEquals( "0/1/91/f+2 / 64", messages.get(i++).toString() );
+ assertEquals( "60/1/91/f+2 / 64", messages.get(i++).toString() );
+ assertEquals( "120/1/91/a+2 / 64", messages.get(i++).toString() );
+
+ // CALL try_2(/32, ...)
+ assertEquals( "180/1/91/f+3 / 64", messages.get(i++).toString() );
+ assertEquals( "240/1/91/f+3 / 64", messages.get(i++).toString() );
+ assertEquals( "300/1/91/a+3 / 64", messages.get(i++).toString() );
+
+ // call pattern pat_try_1(/16, ...)
+ assertEquals( "360/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "480/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "600/1/91/a+3 / 64", messages.get(i++).toString() );
+
+ // call pattern pat_try_2(/16, ...)
+ assertEquals( "720/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "840/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "960/1/91/a+3 / 64", messages.get(i++).toString() );
+
+ // CALL try_3(/32, ...)
+ assertEquals( "1080/1/91/f+2 / 64", messages.get(i++).toString() );
+ assertEquals( "1140/1/91/f+2 / 64", messages.get(i++).toString() );
+ assertEquals( "1200/1/91/a+2 / 64", messages.get(i++).toString() );
+
+ // CALL try_4(/32, ...)
+ assertEquals( "1260/1/91/f+3 / 64", messages.get(i++).toString() );
+ assertEquals( "1320/1/91/f+3 / 64", messages.get(i++).toString() );
+ assertEquals( "1380/1/91/a+3 / 64", messages.get(i++).toString() );
+
+ // call pattern pat_try_3(/16, ...)
+ assertEquals( "1440/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "1560/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "1680/1/91/a+3 / 64", messages.get(i++).toString() );
+
+ // call pattern pat_try_4(/16, ...)
+ assertEquals( "1800/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "1920/1/91/f+4 / 64", messages.get(i++).toString() );
+ assertEquals( "2040/1/91/a+3 / 64", messages.get(i++).toString() );
+ }
+
+ parse(getWorkingFile("condition-2"));
+ assertEquals( "2:ELSE,4:ELSE,\n1:IF,3:IF,\n2:ELSE,4:ELSE,\n", getLyrics() );
}
/**
@@ -999,6 +1048,21 @@ void testParseFilesFailing() {
assertEquals( "CHORD test c,d,c", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_CHORD_CONTAINS_ALREADY)) );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("chord-separator-double")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "CHORD crd=c,d,,e", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_CHORD_REDUNDANT_SEP)) );
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("chord-separator-leading")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "CHORD crd = ,c,d,e", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_CHORD_REDUNDANT_SEP)) );
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("chord-separator-trailing")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "CHORD crd = c,d,e,", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_CHORD_REDUNDANT_SEP)) );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("call-with-invalid-option")) );
assertEquals( 6, e.getLineNumber() );
assertEquals( "CALL test v=50", e.getLineContent() );
@@ -1144,6 +1208,11 @@ void testParseFilesFailing() {
assertEquals( "CHORD test c d 128", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_NOTE_TOO_BIG) + "128") );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("chord-assigner-double")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "CHORD crd==c,d,e", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_UNKNOWN_NOTE) + "=c") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("channel-cmd-missing-param")) );
assertEquals( 3, e.getLineNumber() );
assertEquals( "0 c", e.getLineContent() );
@@ -1180,7 +1249,7 @@ void testParseFilesFailing() {
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PARAM_OUTSIDE_FUNCTION) + "$[1]") );
e = assertThrows( ParseException.class, () -> parse(getFailingFile("param-i-outside-function-nested")) );
- assertEquals( 5, e.getLineNumber() );
+ assertEquals( 4, e.getLineNumber() );
assertEquals( "$[1]", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PARAM_OUTSIDE_FUNCTION) + "$[1]") );
@@ -1190,7 +1259,7 @@ void testParseFilesFailing() {
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PARAM_OUTSIDE_FUNCTION) + "${x}") );
e = assertThrows( ParseException.class, () -> parse(getFailingFile("param-n-outside-function-nested")) );
- assertEquals( 5, e.getLineNumber() );
+ assertEquals( 4, e.getLineNumber() );
assertEquals( "${x}", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PARAM_OUTSIDE_FUNCTION) + "${x}") );
@@ -1330,6 +1399,11 @@ void testParseFilesFailing() {
assertEquals( "{ if $x in 1;2;;5", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_COND_EMPTY_ELEM_IN_IN_LIST)) );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("opt-assigner-double")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "0 c /4 v==100", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_NOT_AN_INTEGER) + "=100") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("opt-without-value-velocity")) );
assertEquals( 3, e.getLineNumber() );
assertEquals( "0 c /4 v", e.getLineContent() );
@@ -1480,6 +1554,11 @@ void testParseFilesFailing() {
assertEquals( "-1 c /4", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_UNKNOWN_CMD) + "-1") );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("channel-negative-2")) );
+ assertEquals( 3, e.getLineNumber() );
+ assertEquals( "-1 c /4", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_UNKNOWN_CMD) + "-1") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("channel-too-high")) );
assertEquals( 3, e.getLineNumber() );
assertEquals( "16 c /4", e.getLineContent() );
@@ -1515,6 +1594,11 @@ void testParseFilesFailing() {
assertEquals( "CALL test(a=b=c)", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_CALL_PARAM_MORE_ASSIGNERS)) );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("call-param-named-more-assigners-2")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "CALL f(vel==102)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_CALL_PARAM_MORE_ASSIGNERS)) );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("include-directory")) );
assertEquals( 1, e.getLineNumber() );
assertEquals( "INCLUDE inc/", e.getLineContent() );
@@ -1525,6 +1609,11 @@ void testParseFilesFailing() {
assertEquals( "INCLUDE", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FILE_NUM_OF_ARGS)) );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("const-assigner-double")) );
+ assertEquals( 7, e.getLineNumber() );
+ assertEquals( "0 c /4 v==103", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_NOT_AN_INTEGER) + "=103") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("include-too-many-args")) );
assertEquals( 1, e.getLineNumber() );
assertEquals( "INCLUDE inc/instruments.midica inc/instruments.midica", e.getLineContent() );
@@ -1564,6 +1653,16 @@ void testParseFilesFailing() {
assertEquals( "DEFINE CHORD crd crd", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_DEFINE_NUM_OF_ARGS)) );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("define-assigner-double")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "DEFINE CHORD == CRD", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_DEFINE_NUM_OF_ARGS)) );
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("define-assigner-double-2")) );
+ assertEquals( 8, e.getLineNumber() );
+ assertEquals( "CRD crd c,d,e", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_UNKNOWN_CMD) + "CRD") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("const-without-args")) );
assertEquals( 3, e.getLineNumber() );
assertEquals( "CONST", e.getLineContent() );
@@ -1594,6 +1693,11 @@ void testParseFilesFailing() {
assertEquals( "VAR", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_VAR_NUM_OF_ARGS)) );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("var-assigner-double")) );
+ assertEquals( 7, e.getLineNumber() );
+ assertEquals( "0 c /4 v=$x", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_NOT_AN_INTEGER) + "=100") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("var-not-enough-args")) );
assertEquals( 3, e.getLineNumber() );
assertEquals( "VAR $x", e.getLineContent() );
@@ -1714,6 +1818,26 @@ void testParseFilesFailing() {
assertEquals( "0 /1 s=1", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PATTERN_INVALID_INNER_OPT) + "shift") );
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("pattern-with-if")) );
+ assertEquals( 3, e.getLineNumber() );
+ assertEquals( "0 /1 if y == x", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PATTERN_INVALID_INNER_OPT) + "if") );
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("pattern-with-if-2")) );
+ assertEquals( 3, e.getLineNumber() );
+ assertEquals( "0 pat_b(x, y, z) if y == x", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PATTERN_INVALID_INNER_OPT) + "if") );
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("pattern-undefined")) );
+ assertEquals( 6, e.getLineNumber() );
+ assertEquals( "0 not_existing_pattern( x, y, z ) v = 120 , d = 80%", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_PATTERN_UNDEFINED) + "not_existing_pattern") );
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("pattern-undefined-2")) );
+ assertEquals( 3, e.getLineNumber() );
+ assertEquals( "0 /42", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_NOTE_LENGTH_INVALID) + "/42") );
+
e = assertThrows( ParseException.class, () -> parse(getFailingFile("pattern-call-index-wrong")) );
assertEquals( 5, e.getLineNumber() );
assertEquals( "1.2 /4", e.getLineContent() );
diff --git a/test/org/midica/testfiles/failing/call-param-named-more-assigners-2.midica b/test/org/midica/testfiles/failing/call-param-named-more-assigners-2.midica
new file mode 100755
index 0000000..be54d7f
--- /dev/null
+++ b/test/org/midica/testfiles/failing/call-param-named-more-assigners-2.midica
@@ -0,0 +1,9 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CALL f(vel==102)
+
+FUNCTION f
+ 0 c /4 v=${vel}
+END
diff --git a/test/org/midica/testfiles/failing/channel-negative-2.midica b/test/org/midica/testfiles/failing/channel-negative-2.midica
new file mode 100644
index 0000000..e7a28af
--- /dev/null
+++ b/test/org/midica/testfiles/failing/channel-negative-2.midica
@@ -0,0 +1,3 @@
+INCLUDE inc/instruments.midica
+
+-1 c /4
diff --git a/test/org/midica/testfiles/failing/chord-assigner-double.midica b/test/org/midica/testfiles/failing/chord-assigner-double.midica
new file mode 100755
index 0000000..157406b
--- /dev/null
+++ b/test/org/midica/testfiles/failing/chord-assigner-double.midica
@@ -0,0 +1,7 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CHORD crd==c,d,e
+
+0 crd /4 v=101
diff --git a/test/org/midica/testfiles/failing/chord-separator-double.midica b/test/org/midica/testfiles/failing/chord-separator-double.midica
new file mode 100755
index 0000000..c43a63c
--- /dev/null
+++ b/test/org/midica/testfiles/failing/chord-separator-double.midica
@@ -0,0 +1,7 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CHORD crd=c,d,,e
+
+0 crd /4 v=101
diff --git a/test/org/midica/testfiles/failing/chord-separator-leading.midica b/test/org/midica/testfiles/failing/chord-separator-leading.midica
new file mode 100755
index 0000000..12edf8c
--- /dev/null
+++ b/test/org/midica/testfiles/failing/chord-separator-leading.midica
@@ -0,0 +1,7 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CHORD crd = ,c,d,e
+
+0 crd /4 v=101
diff --git a/test/org/midica/testfiles/failing/chord-separator-trailing.midica b/test/org/midica/testfiles/failing/chord-separator-trailing.midica
new file mode 100755
index 0000000..e19622c
--- /dev/null
+++ b/test/org/midica/testfiles/failing/chord-separator-trailing.midica
@@ -0,0 +1,7 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CHORD crd = c,d,e,
+
+0 crd /4 v=101
diff --git a/test/org/midica/testfiles/failing/const-assigner-double.midica b/test/org/midica/testfiles/failing/const-assigner-double.midica
new file mode 100755
index 0000000..865e68b
--- /dev/null
+++ b/test/org/midica/testfiles/failing/const-assigner-double.midica
@@ -0,0 +1,7 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CONST $x==103
+
+0 c /4 v=$x
diff --git a/test/org/midica/testfiles/failing/define-assigner-double-2.midica b/test/org/midica/testfiles/failing/define-assigner-double-2.midica
new file mode 100755
index 0000000..c10c15a
--- /dev/null
+++ b/test/org/midica/testfiles/failing/define-assigner-double-2.midica
@@ -0,0 +1,10 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+DEFINE CHORD==CRD
+
+
+CRD crd c,d,e
+
+0 crd /4 v=99
diff --git a/test/org/midica/testfiles/failing/define-assigner-double.midica b/test/org/midica/testfiles/failing/define-assigner-double.midica
new file mode 100755
index 0000000..a0f90e0
--- /dev/null
+++ b/test/org/midica/testfiles/failing/define-assigner-double.midica
@@ -0,0 +1,10 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+DEFINE CHORD == CRD
+
+
+CRD crd c,d,e
+
+0 crd /4 v=99
diff --git a/test/org/midica/testfiles/failing/opt-assigner-double.midica b/test/org/midica/testfiles/failing/opt-assigner-double.midica
new file mode 100755
index 0000000..2459523
--- /dev/null
+++ b/test/org/midica/testfiles/failing/opt-assigner-double.midica
@@ -0,0 +1,5 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+0 c /4 v==100
diff --git a/test/org/midica/testfiles/failing/pattern-undefined-2.midica b/test/org/midica/testfiles/failing/pattern-undefined-2.midica
new file mode 100644
index 0000000..72a203c
--- /dev/null
+++ b/test/org/midica/testfiles/failing/pattern-undefined-2.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+0 c,d,e pat_a( y )
+
+PATTERN pat_a
+ 0 /42
+END
diff --git a/test/org/midica/testfiles/failing/pattern-undefined.midica b/test/org/midica/testfiles/failing/pattern-undefined.midica
new file mode 100644
index 0000000..0e6f46a
--- /dev/null
+++ b/test/org/midica/testfiles/failing/pattern-undefined.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+0 c,d,e pat_a( y )
+
+PATTERN pat_a
+ 0 not_existing_pattern( x, y, z ) v = 120 , d = 80%
+END
diff --git a/test/org/midica/testfiles/failing/pattern-with-if-2.midica b/test/org/midica/testfiles/failing/pattern-with-if-2.midica
new file mode 100644
index 0000000..f1ab0e9
--- /dev/null
+++ b/test/org/midica/testfiles/failing/pattern-with-if-2.midica
@@ -0,0 +1,11 @@
+INCLUDE inc/instruments.midica
+
+0 c,d,e pat_a( y )
+
+PATTERN pat_a
+ 0 pat_b( x, y, z ) if $[0] == x
+END
+
+PATTERN pat_b
+ 0 /1
+END
diff --git a/test/org/midica/testfiles/failing/pattern-with-if.midica b/test/org/midica/testfiles/failing/pattern-with-if.midica
new file mode 100644
index 0000000..9a9df55
--- /dev/null
+++ b/test/org/midica/testfiles/failing/pattern-with-if.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+0 c,d,e pat_a( y )
+
+PATTERN pat_a
+ 0 /1 if $[0] == x
+END
diff --git a/test/org/midica/testfiles/failing/var-assigner-double.midica b/test/org/midica/testfiles/failing/var-assigner-double.midica
new file mode 100755
index 0000000..7fd42a0
--- /dev/null
+++ b/test/org/midica/testfiles/failing/var-assigner-double.midica
@@ -0,0 +1,7 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+VAR $x==100
+
+0 c /4 v=$x
diff --git a/test/org/midica/testfiles/working/condition-2.midica b/test/org/midica/testfiles/working/condition-2.midica
new file mode 100644
index 0000000..b126158
--- /dev/null
+++ b/test/org/midica/testfiles/working/condition-2.midica
@@ -0,0 +1,22 @@
+INSTRUMENTS
+ 0 PERCUSSIVE_ORGAN PERCUSSIVE_ORGAN
+END
+
+CALL x()
+CALL x(IF)
+CALL x(UNKNOWN)
+
+FUNCTION x
+ CALL y( 1:IF\c ) if $[0] == IF
+ CALL y( 2:ELSE\c ) if $[0] != IF
+ CALL y( 3:IF\c ) if=$[0]==IF
+ CALL y( 4:ELSE\c ) if=$[0]!=IF
+ 0 - /4 l=\r
+END
+
+FUNCTION y
+ 0 f /4 l=$[0]
+END
+
+
+* tempo 95
diff --git a/test/org/midica/testfiles/working/condition.midica b/test/org/midica/testfiles/working/condition.midica
new file mode 100644
index 0000000..e1a0b72
--- /dev/null
+++ b/test/org/midica/testfiles/working/condition.midica
@@ -0,0 +1,131 @@
+INCLUDE inc/instruments.midica
+
+CHORD crd a+3 f+4
+
+// call function with base-level if/else blocks
+CALL try_1(/32)
+CALL try_1(/32, ELSE)
+CALL try_1(/32, IF)
+
+// call function with nested if/else blocks
+CALL try_2(/32)
+CALL try_2(/32, ELSE)
+CALL try_2(/32, IF)
+
+// call pattern with base-level if/else blocks
+1 crd pat_try_1(/16)
+1 crd pat_try_1(/16, ELSE)
+1 crd pat_try_1(/16, IF)
+
+// call pattern with nested if/else blocks
+1 crd pat_try_2(/16)
+1 crd pat_try_2(/16, ELSE)
+1 crd pat_try_2(/16, IF)
+
+// same as above but with "if=$[1]==IF" instead of "if $[1] == IF"
+CALL try_3(/32)
+CALL try_3(/32, ELSE)
+CALL try_3(/32, IF)
+
+CALL try_4(/32)
+CALL try_4(/32, ELSE)
+CALL try_4(/32, IF)
+
+1 crd pat_try_3(/16)
+1 crd pat_try_3(/16, ELSE)
+1 crd pat_try_3(/16, IF)
+
+1 crd pat_try_4(/16)
+1 crd pat_try_4(/16, ELSE)
+1 crd pat_try_4(/16, IF)
+
+
+FUNCTION try_1
+ { if $[1] == IF
+ 1 a+2 $[0] l=IF_
+ }
+ { else
+ 1 f+2 $[0] l=ELSE_
+ }
+END
+
+FUNCTION try_2
+ {
+ { if $[1] == IF
+ 1 a+3 $[0] l=IF_
+ }
+ { else
+ 1 f+3 $[0] l=ELSE_
+ }
+ }
+END
+
+PATTERN pat_try_1
+
+ { if $[1] == IF
+ 0 $[0] m
+ } m
+
+ { else
+ 1 $[0] m
+ } m
+ - $[0]
+END
+
+PATTERN pat_try_2
+ {
+ { if $[1] == IF
+ 0 $[0] m
+ } m
+
+ { else
+ 1 $[0] m
+ } m
+ - $[0]
+ }
+END
+
+FUNCTION try_3
+ { if=$[1]==IF
+ 1 a+2 $[0] l=IF_
+ }
+ { else
+ 1 f+2 $[0] l=ELSE_
+ }
+END
+
+FUNCTION try_4
+ {
+ { if=$[1]==IF
+ 1 a+3 $[0] l=IF_
+ }
+ { else
+ 1 f+3 $[0] l=ELSE_
+ }
+ }
+END
+
+PATTERN pat_try_3
+
+ { if=$[1]==IF
+ 0 $[0] m
+ } m
+
+ { else
+ 1 $[0] m
+ } m
+ - $[0]
+END
+
+PATTERN pat_try_4
+ {
+ { if=$[1]==IF
+ 0 $[0] m
+ } m
+
+ { else
+ 1 $[0] m
+ } m
+ - $[0]
+ }
+END
diff --git a/test/org/midica/testfiles/working/inc/definitions.midica b/test/org/midica/testfiles/working/inc/definitions.midica
index 02cd8a8..d944cca 100644
--- a/test/org/midica/testfiles/working/inc/definitions.midica
+++ b/test/org/midica/testfiles/working/inc/definitions.midica
@@ -1,5 +1,5 @@
-DEFINE DEFINE def
-def INCLUDE incl_f
+DEFINE DEFINE=def
+def INCLUDE = incl_f
def INSTRUMENTS instruments[
def END ]
def FUNCTION procedure[
diff --git a/test/org/midica/testfiles/working/var.midica b/test/org/midica/testfiles/working/var.midica
index 06e0879..268185e 100644
--- a/test/org/midica/testfiles/working/var.midica
+++ b/test/org/midica/testfiles/working/var.midica
@@ -1,10 +1,10 @@
INCLUDE inc/instruments.midica
CONST $cent= cent
-VAR $fa = 1 // 1
-VAR $fb = 2 // 2
-VAR $fc = 3 // 0
-VAR $fd = $fb$fc // 23
+VAR $fa = 1 // 1
+ VAR $fb = 2 // 2
+VAR $fc = 3 // 0
+VAR $fd = $fb$fc // 23
VAR $forte = $fa$fd // 123
VAR $piano= 30 // 30
VAR $mezzoforte =80 // 80