1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
|
Notes for maintainers of M-Tx
=============================
This file is for someone who wishes to fix a bug, without actually being
willing to master the whole program of well over 4000 lines spread over
16 files.
1. Quick start
--------------
The main program is `prepmx.pas`. The other files are units.
There are three places in each file that give a lot of useful information.
When you have a spare moment, look at those. It will save time later.
1. Immediately after the first line, there should be a comment
containing a short description of what is in the file.
2. After the keyword `interface`, there may be a `uses` statement
listing the units needed in order that the interface (the Pascal
equivalent of a C header file) can be defined.
3. After the keyword `implementation`, there may be a `uses` statement
listing the further units needed in order that the implementation
(the equivalent of a C code file) can be defined.
Instead of some dull generalities, here is a blow-by-blow account of how an
actual bug reported by Wojtek Chemijewski on the Werner Icking Music Archive
mailing list `<tex-music@tug.org>` on 2014-01-29 was found and "corrected".
2. Case study
-------------
I'm doing this from a command line in Ubuntu Linux, so I use `grep`.
Any other tool that can search for text in the Pascal source files
will do equally well.
In M-Tx 0.60, the following behaviour was considered to be a bug:
> If the piece starts with a pickup, then the bar separator at the end
of the pickup is only recognized as terminating a pickup when it is
a plain bar line `|`, not when it is a left repeat `|:`.
There are workarounds for this, the best one being to not use plain bar
lines but to use the PMX command `Rl` in the lowest voice (which becomes
the first voice in PMX). That is not the point. We want M-Tx to do this
right.
We start by looking for the string `|:` in all the source files.
$ grep '|:' *.pas
prepmx.pas: else if bar='|:' then repcode:='Rl'
prepmx.pas: else if bar=':|:' then repcode:='Rlr'
(BTW, although I continue using `grep`, I won't again insult your
intelligence by quoting the exact invocation.)
On reading the source, we note that both references are inside
a procedure called `writeRepeat`.
procedure writeRepeat(var bar: string);
var repcode: string;
begin if bar='' then exit;
repcode:='';
if bar='||' then repcode:='Rd'
else if (bar='|]') then repcode:='RD'
else if bar='|:' then repcode:='Rl'
else if bar=':|:' then repcode:='Rlr'
else if bar=':|' then repcode:='Rr'
else if last_bar and (bar='|') then repcode:='Rb';
if repcode<>'' then putLine(' '+repcode);
bar:='';
end;
This clearly writes to the PMX file the PMX code to which the M-Tx
bar symbol translates. It can't be the cause of the bug.
Where is `writeRepeat` used? Another `grep` says: only in `prepmx.pas`
itself, four times.
The first one is in the following context:
barword: begin
if voice=nvoices then
if endOfBar(voice,bar_no) then repeat_sign := note
else writeRepeat(note);
if note<>barsym then note:='';
no_chords:=false;
end;
This code tests whether we are at the end of a bar; if so, the bar
word is remembered as `repeat_sign`, otherwise it is written
immediately. We are following a breadth-first strategy, so we don't
yet look at `endOfBar`, but continue with examining the references
to `writeRepeat`.
The second reference in context:
if bar_of_line>1 then putLine(comment+'Bar '+toString(bar_no));
last_bar := (bar_of_line=nbars) and final_paragraph;
if last_bar and (repeat_sign='|') then repeat_sign:='';
writeRepeat(repeat_sign); new_meter := '';
This occurs in a procedure called `processOneBar`. The first thing is
to write out the `repeat_sign` previously saved. It also does not
seem to be the cause of the bug.
The third reference is inside a loop over all voices. If that is the
problem, we may have to dig very deeply, so we postpone looking at it.
The last reference is inside the procedure `doMusic`.
if startsWithBracedWord(P[1]) then lyricsParagraph else
begin musicParagraph; first_paragraph:=false;
writeRepeat(repeat_sign);
end
That boolean `first_paragraph` sticks out like a sore thumb. The
program is doing something differently, depending on whether we are
doing the first paragraph or a later one! That cannot be unrelated
to the question of a pickup.
Let's look for where `first_paragraph` is used. In `files.pas` there
is something that appears to be handling inline TeX, the others are
in `prepmx.pas`. It appears in this conditional:
if first_paragraph then includeStartString;
Let's find that procedure. It's in `analyze.pas`. The procedure itself
is clearly blameless, but also near the top of `analyze.pas` our eye
alights on `pickup:=0;`. The statement appears twice, the second time
probably needlessly, but that's beside the point. We now know there is
a variable named `pickup` and unless the author of the program is
deliberately misleading us, it must be relevant to the pickup.
Further on in `analyze.pas`, we find:
scanMusic(voice,l);
if multi_bar_rest and (nv>1) then error(
'Multi-bar rest allows only one voice',print);
if not pmx_preamble_done then
if voice=top then pickup:=l
else if pickup<>l then
error3(voice,'The same pickup must appear in all voices');
I.e. the procedure `scanMusic` sets a value `l` that will be assigned
to `pickup`. We're getting warm!
That procedure is in `mtx.pas`. It is a very long procedure, but the
argument `l` is now the parameter `left_over`, easy to search for.
The only place where it seems to be used after being initialized is
in the following compound statement:
if (note=barsym) then
if meternum=0 then
error3(voice,'You may not use bar lines in barless music')
else if bar_length=0 then markBar(voice)
else if (numberOfBars(voice)=0) and (bar_length<bar) then
begin if has_next then
has_next:=false {Should check whether pickups are equal}
else if left_over>0 then error3(voice,'Bar is too short');
left_over:=bar_length; bar_length := 0;
end;
What is `barsym`? These are the places where the name appears.
analyze.pas: is_labelled := (w[l]=colon) and (w[l-1]<>barsym);
globals.pas: barsym = '|';
mtxline.pas: if chord>0 then if P[chord]=barsym then predelete(P[chord],1);
mtx.pas: if (note=barsym) then
prepmx.pas: if (w=barsym) or (w='') then no_chords:=true;
prepmx.pas: if note<>barsym then note:='';
uptext.pas: if (w=barsym) or (w='') then no_uptext:=true;
Our quest is over! This is the bug. The whole code starts with
`if (note=barsym) then`. The parentheses indicate that at some
previous stage of the code, the programmer thought that there may
have been extra clauses, such as testing for left repeat, but forgot
to put them in. We also note further evidence of not-quite-completed
code in the comment `{Should check whether pickups are equal}`.
How will we fix this? Should we have `if isBarSym(note) then` and supply
a function `isBarSym`, or merely add a clause? It appears as if the
other cases do not require the same fixing, so the function will only be
called this once. Let's try:
if note[1]=barsym then
That will cover `|`, `|:` and `||` but not `:|`, which seems fair
enough. It will also cover illegal cases like `|-`, but let's hope
those are caught elsewhere.
The change is enough to cure the reported bug, so we document it at the
top of `prepmx.pas` and in `Corrections`, changing the version number to
`0.60e`.
### It was not a bug
After carrying out the above exercise, I carefully reread the M-Tx
manual and found:
> If your piece starts with a pickup, it is defined by a bar line at the
> end of “bar 0”, i.e. the incomplete bar containing the pickup. Even
> when there is a repeat sign after the pickup, so that you don’t
> actually see a bar line in the printed music, you still need a bar
> line before the repeat sign, otherwise M-Tx cannot know where the
> pickup stops. The bar line defining the pickup is compulsory in the
> first voice found, and optional but recommended in the others.
Be warned, therefore, that M-Tx 0.60e is a dead end. The next version
of M-Tx will branch from 0.60d again.
3. Adding a preamble command
----------------------------
In M-Tx 0.61, the `TeX` preamble command was added. Obviously we start
in `preamble.pas`. There are some pretty obvious lists contained in
`type` and `const` statements.
The name lower-case name `tex` is inserted in the list of names
`command_type`, the upper case version `TEX` in the corresponding
position in `commands`, a default empty value in `cline` and an
extra `false` in `redefined`. This is enough to get the `TeX` command
recognized, to save the value given, and to record the fact that there
was a change.
We want the TeX string to appear immediately after `readmod`. If you
examine a `.tex` file made by PMX from source coded by M-Tx, you will
see that `\mtxInterInstrument` is there. The only line containing that
word is in `preamble.pas`:
if nspace[j]<>unspec then TeXtype2('\mtxInterInstrument{'+toString(i-1)+
I.e. it was inserted as a "Type 2 TeX string". The line in question is
inside the function `respace`, which we see is executed immediately
after the statement `pmx_preamble_done:=true;`. We need to squeeze
in between, adding a call
insertTeX;
At some respectable point before that, we define this procedure.
procedure insertTeX;
begin
if redefined[tex] then TeXtype2(cline[tex]);
end;
Test it; it works. However, if we issue more than one `TeX` command,
only the last one takes effect, whereas the user will want all.
Looking for assignments into `cline`, we find in procedure `doCommand`
the statement:
cline[last_command]:=line;
The entire loop containing that statement needs a bit of reorganization,
since we must not only allow for appending new text but also bypass the
test for redefinition warnings. First we supply a function
function mustAppend(command: command_type): boolean;
begin mustAppend := command=tex end;
which allows for possible future preamble commands that also append
rather than redefine. The new code of the loop starting with
if mustAppend(last_command) and redefined[last_command] then
is straightforward, almost. Do we put in `LineEnding` when we append?
If we don't, we may run into problems with `PMXlinelength` which is
only 128. If we do, `TeXtype2` needs to be made a little smarter,
i.e. able to re-split the lines. Actually, in that case an explicit
`#10` is better, since on some systems `LineEnding` has two characters.
`TeXtype2` is in `files.pas`. There is a line
else if first_paragraph then putLine('\'+s+'\')
that we replace by a call to a routine `putTeXlines` which does
the necessary.
|