-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPOL.txt
4208 lines (3385 loc) · 204 KB
/
POL.txt
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% Programming a Program-Oriented-Language
% Charles H. Moore
% 2011
\clearpage
\begin{center}
\LARGE\textsf{Preface}\par
\end{center}
This is an unpublished book I wrote long ago. Just after I'd written
the first versions of Forth. Perhaps it explains the motivation behind
Forth. There is some interest in it, and publishing is no longer
relevant since I can post it on my website.
I have a typescript that I recovered from Forth, Inc long ago. I had
typed it on my Smith-Corona portable, complete with overstrikes and
annotations. It is illegible enough to discourage a casual reader, so
I'm re-keying it in HTML.
This is useful, since I used to be a good typist and that skill has
deteriorated. My fingers can use the exercise and I'm curious if I can
reduce my error rate.
I'm making minimal changes to the text; just enough to fit HTML. The
language remains quaint, ungrammatical or unclear. Any remaining typos
are modern.
-- Chuck Moore 2011
written \~ June 1970
\thispagestyle{empty}
\pagenumbering{arabic}
# Introduction
I'm not sure why you're reading this book. It's taken me a while to
discover why I'm writing it. Let's examine the title: *Programming a
Problem-Oriented-Language*. The key word is programming. I've written
many programs over the years. I've tried to write *good* programs, and
I've observed the manner in which I write them rather critically. My
goal has been to decrease the effort required and increase the quality
produced.
In the course of these observations, I've found myself making the same
mistakes repeatedly. Mistakes that are obvious in retrospect, but
difficult to recognise in context. I thought that if I wrote a
prescription for programming, I could at least remind myself of
problems. And if the result is of value to me, it should be of value to
others; if what I say is new to you, you may learn something of value;
if I cover familiar ground, you at least get a new point of view.
I've also been distressed at the lack of concern from others about
problems I consider significant. It amounts to a general indifference to
quality; a casual attitude of confidence that one's programs are pretty
good, in any case as good as necessary. I'm convinced this confidence is
misplaced. Moreover this attitude is reinforced by the massive trend to
high-level languages and a placid acceptance of their inefficiencies:
What's the use of designing a really good algorithm if the compiler's
going to botch it up anyway?
So I've written a book about programming. I have no great taste for
debating over a one-way communication link and no real interest in
convincing you that I'm right in what I say. So you'll probably find
that I'm being brusk. I'm quite likely to state bluntly something you
may take issue with. Please do! My intention is to document an approach
I've found useful, and perhaps to stimulate critical interest in
programming. If you care enough to take issue, I'm delighted.
Back to the title. What about Problem-Oriented-Language? I didn't start
out to write about that; and I'm not sure that I'm qualified to do so.
But I discovered that in order to justify what I was doing and identify
the appropriate circumstances for doing it, the term became essential.
A problem-oriented-language is a language tailored to a particular
application. To avoid that uniquely clumsy term, I'll usually substitute
*application language* as synonymous. Very often such a language isn't
recognised for what it is. For instance, if your program reads a code in
column 80 to identify an input card, you are implementing an application
language. A very crude one, a very awkward one; mostly because you
hadn't given the matter any thought. Recognising the problem, I'm sure
you can design a better solution. This book will show you how.
## The Basic Principle
We have a large number of subjects to talk about. I'm going to throw
before you a lot of techniques that you may be able to use. This is
basically the result of the nature of a digital computer: a general
purpose tool for processing information.
A computer can do anything. I hope that your realize that, providing you
allow me to define "anything", I can prove this. I mean real,
incontrovertible, mathematical-type proof. A computer cannot do
everything. I can prove this, too. But most important, with only you and
I to program it, a computer can not even do very much. This is of the
nature of an empirical discovery.
So to offer guidance when the trade-offs become obscure, I am going to
define the Basic Principle:
> Keep it Simple
As the number of capabilities you add to a program increases, the
complexity of the program increases exponentially. The problem of
maintaining compatibility among these capabilities, to say nothing of
some sort of internal consistency in the program, can easily get out of
hand. You can avoid this if you apply the Basic Principle. You may be
acquainted with an operating system that ignored the Basic Principle.
It is very hard to apply. All the pressures, internal and external,
conspire to add features to your program. After all, it only takes a
half-dozen instructions; so why not? The only opposing pressure is the
Basic Principle, and if you ignore it, there is no opposing pressure.
In order to help you apply the Basic Principle, I'm going to tell you
how many instructions you should use in some routines. And how large a
program with certain capabilities should be. These numbers are largely
machine independent; basically they measure the complexity of the task.
They are based upon routines I have used in my programs, so I can
substantiate them. Let me warn you now that I'll be talking about
programs that will fit comfortably in 4K words of core.
The Basic Principle has a corollary:
> Do Not Speculate!
Do not put code in your program that *might* be used. Do not leave hooks
on which you can hang extensions. The things you might want to do are
infinite; that means that each one has 0 probability of realization. If
you need an extension later, you can code it later - and probably do a
better job than if you did it now. And if someone else adds the
extension, will they notice the hooks you left? Will you document that
aspect of your program?
The Basic Principle has another corollary:
> Do It Yourself!
Now we get down the the nitty-gritty. This is our first clash with the
establishment. The conventional approach, enforced to a greater or
lesser extent, is that you shall use a standard subroutine. I say that
you should write your own subroutines.
Before you can write your own subroutine, you have to know how. This
means, to be practical, that you have written it before; which makes it
difficult to get started. But give it a try. After writing the same
subroutine a dozen times on as many computers and languages, you'll be
pretty good at it. If you don't plan to be programming that long, you
won't be interested in this book.
What sort of subroutines do you write for yourself? I have acquired
respect for SQRT subroutines. They're tricky things; seem to attract a
lot of talent. You can use the library routine to good advantage. Input
subroutines now. They seem to have crawled out from under a rock. I
somehow can't agree that the last word was said 15 years ago when FORMAT
statements were invented.
As I will detail later, the input routine is the most important code in
your program. After all, no one sees your program; but everyone sees
your input. To abdicate to a system subroutine that hasn't the slightest
interest in your particular problem is foolish. The same can be said for
output subroutine and disk-access subroutine.
Moreover, the task is not that great as to deter you. Although it takes
hundreds of instructions to write a general purpose subroutine, you can
do what you need with tens of instructions. In fact, I would advise
against writing a subroutine longer that a hundred instructions.
So if you want to read double-precision, complex integers; don't rely on
the COBOL input subroutine, or wait till the manufacturer revises it.
It's a lot easier to write your own.
But suppose everyone wrote their own subroutines? Isn't that a step
backward; away from the millennium when our programs are machine
independent, when we all write in the same language, maybe even on the
same computer? Let me take a stand: I can't solve the problems of the
world. With luck, I can write a good program.
## Preview
I'm going to tell you how to write a program. It is a specific program;
that is, a program with a specific structure and capabilities. In
particular, it is a program that can be expanded from simple to complex
along a well defined path, to handle a wide range of problems, likewise
varying from simple to complex. One of the problems it considers is
exactly the problem of complexity. How can you control your program so
that it doesn't grow more complicated than your application warrants?
First I'll define "input", and mention some general rules of programming
that apply to all programs, whether they have input or not. Actually we
will be almost exclusively concerned with input, so I've not much to say
about programs lacking input.
By admitting input, a program acquires a control language by which a
user can guide the program through a maze of possibilities. Naturally,
this increases the flexibility of the program, it also requires a more
complex application to justify it. However it is possible to achieve a
considerable simplification of the program, by recognising that it needs
a control language as a tool of implementation.
The next step is a problem-oriented-language. By permitting the program
to dynamically modify its control language, we mark a qualitative change
in capability. We also change our attention from the program to the
language it implements. This is an important, and dangerous, diversion.
For it's easy to lose sight of the problem amidst the beauty of the
solution.
In a sense, our program has evolved into a meta-language, which
describes a language we apply to the application. But having mentioned
meta-language, I want to explain why I won't use the term again. You see
things get pretty complicated, particularly on a philosophic level. To
precisely describe our situation requires not 2 levels of language -
language and meta-language - but a least 4 levels. To distinguish
between these levels requires subtle arguments that promote not clarity
but confusion. Moreover, the various levels can often be interchanged in
practice, which reduces the philosophic arguments to hair-splitting.
A problem-oriented-language can express any problem I've encountered.
And remember, we're not concerned with the language, but with the
program that makes the language work. By modifying the language we can
apply the same program to many applications. However there are a class
of extensions to the language that constitute another qualitative
change. They don't increase the capacity of the program, but the
increase the capability of the language. That is, they make the language
more expressive. We will consider some such extensions in Chapter 8. I
gathered them together chiefly because they share the common property
that I don't quite comprehend their potential. For example, I think the
language applies the concepts of English.
Finally, I want to describe a process whereby you can implement this
program in machine language. That is, a bootstrap technique whereby a
basic program can modify and expand itself.
I hope you find the ideas I describe of value to you. In particular, I
hope that you will agree that the program I describe has a certain
inevitability; that it must do certain things, it must do them in a
certain order, and that a certain set of conventions yield an optimal
solution.
I've gone to some lengths to simplify. I hope that you don't find too
many violations of the Basic Principle, for it's much easier to
elaborate upon a program than it is to strip it to basics. You should
feel free to build upon my basic routines, provided that you recognise
that you are adding a convenience. If you confuse what is expedient with
what is necessary, I guarantee your program will never stop growing.
You will notice a lack of flow-charts. I've never liked them, for they
seem to include a useless amount of information - either too little or
too much. Besides they imply a greater rigidity in program structure
than usually exists. I will be quite specific about what I think you
should do and how you should do it. But I will use words, and not
diagrams. I doubt that you would give a diagram the attention it
deserved, anyway. Or that I would in preparing it.
# Programs without input
The simplest possible program is one that has no input. That is a
somewhat foolish statement, but if you'll give me a chance to explain we
can establish some useful definitions.
First consider the word "input". I want to use it in a specific sense:
> Input is information that controls a program.
In particular, I do not consider as input:
* Moving data between media within the computer. For instance,
* copying tape onto disk, or disk into core.
* Reading data into the computer. This is really a transfer between media:
* from card to core.
However, data very often has input mixed with it - information that
identifies or disposes of the data. For example, a code in col. 80 might
identify a card. It is input, the rest of the card probably data.
Many programs have input of a kind I shall disregard: operating systems
use control cards to specify which files to assign, which subroutines to
collect, etc. Such information is definitely input to the operating
system. Although it may affect the operation of your program, ignore it
because it is not under your control - unless your program is the
operating system itself.
In order to sharpen your recognition of input, let me describe a program
that has input. Consider a program that fits a smooth curve through
measured data points. It needs a lot of information in order to run: the
number of data points, the spacing between points, the number of
iterations to perform, perhaps even which function to fit. This
information might be built into the program; if it is not, it must be
supplied as input. The measured data itself, the object of the entire
program, is not input; but must be accompanied by input in order to to
intelligible.
A program that has no input may be extremely complex. Lacking input
simply means the program knows what to do without being told. That built
into the code is all the information needed to run. If you are willing
to re-compile the program, you can even modify it without input.
But I'll be viewing programs from the input side. I'll be ranking
programs according to the complexity of their input and I plan to
demonstrate that a modest increase in the complexity of input can
provide a substantial decrease in the complexity of the program. From
this point of view, a program with no input is simple.
Since I'm going to be talking about input, a program without input
leaves me nothing to talk about. But I want to make some points about
programs in general, so I'll make them here. For one thing, we will be
climbing a tree. When we reach the higher branches we'll have enough
trouble keeping our balance without worrying about the roots.
## Choosing a language
We shall be less interested in computer language than most programmers.
For 3 reasons: First, we will eventually define our own
application-oriented language. How we implement that language is of
lesser concern. Second, you probably aren't in a position to pick a
language. Your installation probably has reduced your choice to nil.
Third, we won't be talking about problems at the language level.
This last comment deserves elaboration. I assume that you are already a
competent programmer. I'm not interested in teaching you how a computer
works, or how a language conceals the computer. I want to talk about
problems common to all programs in a machine-independent and
language-independent manner. I will leave to you the details of
implementation. I am not going to write a program, I am going to show
you how to write a program.
I hope that you are a good enough programmer to think in computerese.
That is, as someone discusses their application, you interpret it in
terms of computer operations: a loop here, a calculation there, a
decision . . . The details are largely irrelevant, the gross structure
of the program is of concern.
As you put more thought into the problem, you begin to relate it to your
particular machine: this data comes off tape, that loop is stopped by .
. ., this is really a 3-way branch. you modify the problem as required
by your particular hardware configuration.
Finally, you must translate your program into a particular language. You
encounter a new class of problem: your FORTRAN won't run that loop
backwards, COBOL doesn't have a 3-way branch, you couldn't access the
data that way. . . Current languages put more constraints on this last
coding process than they should.
I'll have a bit more to say about languages, but mostly we'll stay at
the most abstract level - talking computerese. We won't be talking in
meta-language exclusively. I may tell you to load an index-register or
to jump on negative and you'll have to translate that into the
equivalent for your computer and language.
Now let's look at the major failing of higher-level languages. In
attempting to achieve machine-independence and to be applicable to a
wide range of applications, they only give you access to a fraction of
the capabilities of your computer. If you compare the number of loop
control instructions on your computer to the number of loop constructs
in your language, you'll see what I mean.
Let me indulge in a 1-sentence characterization of 3 popular languages
to illustrate their restricted capabilities:
* FORTRAN is great at evaluating complicated algebraic expressions.
* COBOL is great a processing packed decimal data.
* ALGOL is great a providing loops and conditional statements.
Each language can be very efficient at its sort of job. But if you want
conditional loops involving complicated decimal expressions you have a
problem.
We are going to be concerned with efficiency. We are going to do some
things that if we don't do efficiently, we can't do at all. Most of
these things will not fit in the framework of a higher-level language.
Some will; others will demand controlled use of the hardware that a
compiler doesn't permit. For example, upon entering a FORTRAN subroutine
it may save the registers it uses. If you didn't need to save them
you've wasted time and space. An ALGOL subroutine may expect registers
available that you have reserved; then you have to save them. It may
well cost you more effort to interface with the compiler than it saves
you in return.
Moreover, none of these languages are very good at moving things around.
Most statements are data transfers - count them in your latest program.
There is a profound philosophical truth concealed in how much we can
accomplish by moving numbers around. If we can move several things with
one instruction, or put the same register several places - we can't
afford not to.
You will have to code in assembler! Not the whole program, if you
insist, but the important parts that we'll be concentrating on. You
might be able to do some of these in FORTRAN, but it simply isn't worth
the effort. I'll show you where higher-level subroutines can go, and I
think you'll agree there is good reason to restrict them to that
function.
I recognise the drawbacks of assembler and chafe at them as much as
anyone. I don't like to punch and debug 10 times as many cards either.
But I will in order to get the performance I need. By the way, I will
use the word "compiler" to include assembler; we will *compile* an
assembly language program.
Later I'll show you how to write a program in a forgotten language:
machine language. By that I mean sitting at the console and entering
absolute, binary instructions with the switches. Depending on the
hardware and software available, and the nature of your application, it
may just be the best language of all.
## Choosing a computer
Of course I don't expect that you're in a position to choose a computer.
Nor am I going to discuss hardware at all. But I do have a mental image
of the kind of computer, and explaining it may help you understand some
of my comments.
Most applications can be programmed very nicely on a small computer: say
4K of 16-bit words with a typical instruction set, floating-point
hardware if needed. If, that is, the computer is augmented with random
access secondary memory, which I will call disk. The capacity of disk is
unimportant, even a small disk providing plenty for our purposes, and is
determined by the application. However, it is important to be able to
copy the disk onto another disk, or tape, for back-up. Thus I envisage a
small computer with 2 secondary memories, and of course a keyboard or
card-reader and printer or scope for input and output.
Instead of running applications in serial on a small computer, you can
run them in parallel on a large one. I see no advantage, for the amount
of core and disk you can afford to use for a single application is about
that available on a small computer. You don't gain speed, you suffer
from a complex operating system, and you have a enormous capital
investment. But the configuration I have in mind remains the same: 4K of
core, secondary memory and input/output device.
## Arrangement and formatting
Now I'm going to tell you how to write a program. Independent of
language or computer. Things you ought to be doing already, but probably
aren't because on one ever told you to. Little things. But if you don't
do them you won't have a good program; and we're going to write a good
program.
Remember the Basic Principle! If you didn't read the Introduction, do it
now.
Declare all variables. Even in FORTRAN when you don't have to. Everyone
likes to know what parameters you are using, presumably need to use;
likes to count them, to see if they could use fewer; is annoyed if you
slip one in without mentioning it.
Define everything you can before you reference it. Even in FORTRAN when
you don't have to. Why not? You don't like to read a program backwards
either. 'Everything you can' means everything except forward jumps. You
better not have many forward jumps.
Make variables as GLOBAL as possible. Why not? You can save some space
and clarify your requirements. For instance, how many Is, Js and Ks do
you need? In most cases a single copy in COMMON would suffice (you have
to declare them, remember, and may as well put them in COMMON); you can
redefine it locally if you must; and it is of interest that you must.
Indent! High-level languages, even modern assemblers, fail to insist
that you start in column x. But you do! The unbelievable appeal of a
straight left margin! Paper is 2-dimensional. Use it! If you indent all
statements inside a loop, it's obvious at a glance the extent of the
loop. If you indent conditionally executed statements you'll find that
nested conditions sort themselves out - automatically. If you indent
little statements you wish you didn't have to include (I = I) you'll
find they intrude less as you glance through the listing. Always indent
the same amount, 3 spaces/level is good. Be consistently and be accurate.
Sloppy indenting is obvious.
## Mnemonics
You will find as you read, that I have strong opinions on some subjects
and no opinion of others. Actually I have strong opinions on all, but
sometimes I can't make up my mind which to express. Fortunately it
leaves you some decisions to make for yourself.
Use words with mnemonic value. Unfortunately what is mnemonic to you may
not be mnemonic to me; and I'm the one who judges. Also unfortunately,
mnemonic words tend to be long, which conflicts with:
Use short words. You don't want to type long words, and I don't want to
read them. In COBOL this means avoid dashes and avoid qualification,
though both can be useful upon occasion.
So let me suggest a compromise: abbreviate in some consistent fashion
and stick to your own rules. I can probably figure out the rules you're
using. You might even mention them in a comment.
Use words with the correct grammatical connotations: nouns for
variables, verbs for subroutines, adjectives for . . . Do *not* use
clever words (GO TO HELL). Their cuteness wears off very fast and their
mnemonic value is too subjective. Besides they offer an unwanted insight
into your personality.
Use comments sparingly! (I bet that's welcome.) Remember that program
you looked through - the one with all the comments? How helpful were all
those comments? How soon did you quit reading them? Programs are
self-documenting, even assembler programs, with a modicum of help from
mnemonics. It does no good to say:
LA B . Load A with B
In fact it does positive bad: if I see comments like that I'll quit
reading them - and miss the helpful ones.
What comments should say is *what* the program is doing. I have to
figure out *how* it's doing it from the instructions anyway. A comment
like this is welcome:
> COMMENT SEARCH FOR DAMAGED SHIPMENTS
Mnemonics apply to variables and labels (You can even get mnemonic value
in FORTRAN statement numbers). Where possible you should apply them to
registers also. You may do well to assign several names to the same
entity, to indicate its current use. However, don't waste effort naming
things that don't need names. If you need a counter, use I, J, K; to
assign a big name (EXC-CNTR) to an insignificant variable is no help.
## Routines and subroutines
There are two words I need to establish precise definitions for: A
*subroutine* is a set of instructions that return from whence they came.
A *routine* is a set of instructions that return to some standard place.
To put it another way, you *jump* to a routine, you *call* a subroutine.
The difference is retained in higher-level languages: GO TO versus CALL
or ENTER.
So what? Subroutines suffer from nesting. If you call a subroutine from
within a subroutine you must somehow save the original return address.
I'm sure you can rattle-off a dozen hardware/software ways of doing
this. They're all expensive.
If you jump somewhere, not intending to come back, you can save trouble,
time and space. But only if you really never come back. To simulate a
subroutine call is worse than ever.
Higher-level languages conceal this by nesting automatically. The best
solution is to nest if you must, but only when you must, and never to
save the same address more than once. That is, upon entering a
subroutine, save the return address if you intend to call other
subroutines. When you're finally ready to return, then un-nest.
Obvious? Perhaps. But it's usually done wrong! Sometimes the problem
only arises with recursive subroutine calls; depending on hardware. It
always arises with re-entrant programming.
So we can get in and out of routines and subroutines. How do we pass
parameters? Again, there are as many answers as computers, languages and
programmers. We shall standardize: you pass what you can in registers;
the rest via a push-down stack.
It is extremely important for routines to be able to communicate
*efficiently*. I hope you are aware of the cost of a FORTRAN subroutine
call. I consider it a basic flaw in the language. We will be moving
among so many subroutines that failing to minimize overhead could easily
halve our running speed.
You must also consider the value of a subroutine. It isolates a logical
function and it eliminates repeated instructions. The first is
acceptable only at minimal cost. The second only if space is saved: a
1-instruction subroutine is ridiculous; a 2-instruction must be called
from 3 places to break even. Be careful!
Finally, it is important to use registers efficiently. Assign registers
for specific purposes and use them consistently. Re-assign registers if
you must to avoid conflicts. Do not move data from one register to
another; see that it is where it belongs in the first place.
When I say register, I'm obviously thinking assembler. However, you will
have to simulate the function of registers with subscripts, etc. in
other languages, and the same considerations apply.
# Programs with input
A program without input is a program with a single task. A program with
input may have many tasks, which it will perform as directed by its
input. Thus I consider input to be control information, and the control
information to define a control language.
We shall have a problem in this chapter, for we are discussing a loop.
Each element of the loop depends on its predecessor and successor, and
we have nowhere to start. I have done the best I could, but am obliged
to refer to things before I define them. Especially in the next section
where I try to justify some of the details we'll encounter immediately
after.
This chapter is full of details, more than I anticipated when I started
it. Although I'm surprised there's so much to say, I think it's all of
value. I only caution you not to get lost in the details; the structure,
the concept of the program are what is important.
To set the stage, let me briefly outline how our program must operate.
You are sitting at a keyboard typing input. You type a string of
characters that the computer breaks into words. It finds each word in a
dictionary, and executes the code indicated by the dictionary entry,
perhaps using parameters also supplied by the entry. The process of
reading words, identifying them and executing code for them is certainly
not unusual. I am simply trying to systematize the process, to extract
the inevitable functions and see that they are efficiently performed.
## Nouns and verbs
I've mentioned the dictionary and we'll soon examine the details
required to implement it. But first I'd like to talk a bit about
individual entries to try and give you a feel for what we're doing.
We're going to read words from your input, find them in the dictionary,
and execute their code. A particular kind of word is a literal, a word
that identifies itself:
1 17 -3 .5
We won't find such words in the dictionary, but we can identify them by
their appearance. Such words act as if they were in the dictionary, and
the code executed for them places them on a push-down stack.
Other words act upon arguments found on this stack, for example:
`+` -- add the last 2 numbers placed on the stack, leave the sum there.
`,` -- type the number on top of the stack, and remove it from the stack.
If we type a phrase such as:
1 17 + ,
We are saying: put 1 onto the stack, 17 onto the stack, add them, and
type their sum. Each word performs its specific, limited function;
independently of any other word. Yet the combination of words achieves
something useful. In fact if we type:
4837 758 + -338 + 23 + 4457 + -8354 + ,
we can even do something non-trivial: each number is added to the sum of
its predecessors, and the result typed.
This is basically the value of our program. It lets us combine simple
operations in a flexible way to accomplish a task.
Let's look more closely at the words we used above. They fall into 2
distinct classes; English even provides names for them:
* Nouns place arguments onto the stack.
* Verbs operate upon arguments on the stack.
All words cause code to be executed. However in the case of nouns, the
code does very little: simply place a number on the stack. Verbs are
considerably more varied in their effects. They may do as little as add
2 arguments, or as much as type out a result - which requires a great
deal of code.
In effect, nouns place arguments onto the stack in anticipation of verbs
that will act upon them. The word anticipation is a good one. In order
to keep our verbs simple, we promise that their arguments are available.
We could define a verb that reads the next word and uses it as an
argument; but in general we don't. It is not the business of a verb to
provide its own arguments; we use nouns to provide arguments before we
execute the verb. In fact, this substantially simplifies our program.
We can extend the characterization of entries a little further. Verbs
have different numbers of arguments:
* Unary verbs modify the number on the stack.
* Binary verbs combine 2 arguments to leave a single result.
Arithmetic operations are binary, arithmetic functions are usually
unary. However, there are more verbs than we can usefully categorize.
For example, the verb "," that types the stack is not unary, since it
removes the number from the stack. Although it does have a single
argument.
Another way of distinguishing verbs is:
* Destructive verb removes its arguments from the stack.
* Non-destructive verb leaves its arguments on the stack.
Unary and binary verbs, as well as the type verb ",", are destructive.
The verb DUP, which I define to duplicate the top of the stack, is
non-destructive. In general verbs are destructive. In fact, I
deliberately define verbs to be destructive in order to simplify the
task of remembering which are and which aren't. I recommend that you do
the same.
Literals are nouns. We can define other words as nouns; words that use
their parameter field to place numbers onto the stack:
* Constants place the contents of their parameter field onto the stack.
* Variables place the address of their parameter field onto the stack.
For example, if PI is a constant, it places 3.14 onto the stack. Thus:
1. PI 2. \* / ,
reads: place 1. onto the stack, place 3.14 onto the stack, place 2. onto
the stack, multiply (2. and PI), divide (1. by 2PI), and type. Constants
are particularly useful when you're using code numbers. It lets you give
names to numbers that might otherwise be hard to remember.
However the most important nouns by far are literals and variables. A
variable gives a name to a location and not to a value, as elementary
programming texts laboriously explain. However, what higher-level
languages conceal is that variables may be used in 2 distinct ways:
* To name a location from which a value is to be taken.
* To name a location into which a value is to be stored.
A constant automatically performs the first; and inherently prevents the
second (you can't store a value into a constant, for you don't know
where the constant came from). Rather than try to distinguish function
by context, as compilers do, we shall define 2 verbs that act upon
variables:
* `@` replace the address on the stack with its contents.
* `=` Store into the address on the stack, the value just beneath it on the stack.
Thus if I type, where X is a variable,
X @ ,
I mean: place the address of X onto the stack, fetch its value, and
type. And if I type,
X @ Y @ + ,
I mean: fetch the value of X, the value of Y, add, and type. On the
other hand,
X @ Y =
will: fetch the address of X, then its value, fetch the address of Y,
and store the value of X into Y. But if I type
X Y =
I'm saying: fetch the address of X, the address of Y, and store the
address of X into Y. Maybe this is that I mean to do, it's not
unreasonable.
I don't want to belabor the point, for we're getting ahead of ourselves.
But variables require special verbs, one of which (@) is not ordinarily
explicit. Incidentally, I originally used the word VALUE for @. But the
verb is used so often it deserves a single character name, and I thought
@ (at) had some mnemonic value, besides being otherwise useless.
I urge you to adopt the verb @. Although you can conceal it in various
ways - we'll discuss one later - it adds needless complication. Such a
useful verb oughtn't be invisible. Besides it lets you store addresses
in variables - indirect addressing
X Y = Y @ @ ,
reads: store the address of X in Y; place the address of Y on the stack,
fetch its value (the address of X) fetch *its* value (the contents of
X), and type.
I hope I've given you some idea of how you can put arguments onto the
stack and act on them with verbs. Although I define constants and
variables, unary and binary verbs, I hope it's clear that these are only
examples. You must define the nouns and verbs and perhaps other kinds of
words that are useful for your application. In fact, I think that is
what programming is all about. If you have available a program such as I
will now describe, once you decide what entries an application requires,
you'll find it absolutely trivial to code those entries, and thus
complete your problem.
## Control loop
Our program has a structure that is easy to miss: it is a single loop.
However, it is a loop that is diffuse - scattered among all the code in
the program. Very few instructions are gathered together to form an
identifiable loop, so the loop warrants some explanation.
We are going to read a word from the input string, look up that word in
the dictionary, and jump to the routine it specifies. Each routine will
return to the top of the loop to read another word. We will be
discussing many routines and it will be helpful to have a term to
identify "return to the top of the loop to read another word". I will
use the word RETURN; you should provide a standard macro or label in
your program for the same purpose.
Actually, you accomplish 2 purposes: you mark the end of a routine. And
you identify the preceding code as being a routine, as distinct from a
subroutine. Thus, I use the word RETURN with a totally different meaning
from the FORTRAN RETURN statement. I shall speak of EXITing from a
subroutine.
Included in your control loop should be a check that the parameter stack
has not exceeded its limits. This is best done after RETURNing from a
routine, and only needs to be done for routines that use the stack. Thus
there are 2 possible RETURN points (actually 3).
The control loop must be efficient. If you count the instructions it
contains, you measure the overhead associated with your program. You
will be executing some very small routines, and it's embarrassing to
find overhead dominating machine use. In particular you don't need to
check other than the parameter stack.
One more routine belongs in this section: an error routine. Whenever an
error is detected, a routine should jump to ERROR which will type the
offending word and an error message. It will then reset all stacks and
the input pointer and RETURN normally.
The problem of how to treat error messages is an important one. We are
in a position to do a good job: to avoid setting and testing flags; to
avoid cascading back through subroutine calls. By clearing the return
stack we eliminate any pending subroutine returns. By not returning with
an error flag, we avoid having the subroutine have to worry about
errors. This simplifies the code, but we must have a standard method of
handling problems.
The image of a person at a keyboard in invaluable for this purpose. No
matter what problem arises, we needn't worry about what to do. Pass the
buck; ask the user. For example, he types a word not in the dictionary.
What to do? Ask him: type the word and an error message, in this case
"?". He tries to add 2 numbers and there's only 1 on the stack: type the
word and "STACK!". He tries to access a field beyond the limit of his
memory: type the word and "LIMIT!".
Of course you want to be careful not to pose the user problems he can't
solve. Faced with a message "MEMORY PARITY" what can he do about it? But
he's certainly in a better position than your program to take corrective
action to most problems. And of course it's up to you to decide what
situations are problems.
By the way. Since you don't check the stack until after you executed a
routine, it will exceed stack limits before you know it. Thus stack
overflow and underflow should be non-fatal. A good solution is to let
the parameter stack overflow into the return stack, and underflow into
the message buffer. The return stack should never underflow.
## Word subroutine
I've described the control loop that will run our program. The first
thing it does is to read a word; so the first thing we shall discuss is
how to read a word.
What is a word? Not a computer word, as I'm sure you realise, although
we shall have to use the word "word" in that sense. A word is a string
of characters bounded by spaces. It is extracted from a larger string of
characters by the routine we are discussing.
Let me contrast this definition with more conventional input routines.
FORTRAN formatted input, for example, doesn't speak of words but of
fields. The meaning of a number is determined by the field it resides
in; that is, by its position on a card. Since we are not using cards,
the notion of position becomes clumsy and we replace it with order: The
order of the words we read is significant, though their position is not.
We lose, however, the ability to leave a field empty, since we cannot
recognise an empty word. All our data must be explicit, which is
probably a good idea but a slow one to learn. Decide now that you will
not specify input conventions that have optional parameters.
Very well, let's write the WORD subroutine. It uses the input pointer to
point at the current position in the source text, the output pointer to
point at the current position in memory where we will move the word. We
must move it; partly to align it on a computer-word boundary and partly
because we may want to modify it.
Fetch input characters and discard them so long as they're spaces.
Thereafter deposit them until you find another space. Deposit this space
and as many others as needed to fill out the last computer-word. If you
have a character-oriented machine you may be amused at my insistence on
word-alignment. Mainly I'm anticipating the search subroutine when we'll
want to compare as large a piece of the word as possible. If a word
holds 6 characters (or even 2) it's much more efficient to compare them
in parallel than serially, even if you have the hardware.
You may want to set an upper limit on word length. Such a limit should
include the largest number you will be using. Then the question arises
as to what to do with a longer word. You might simply discard the excess
characters, providing you don't plan to dissect the word (Chapter 8).
Better, perhaps, that you force a space into the word at the limit. That
is, break the word into 2 words. Presumably something's wrong and you
will eventually discover it in attempting to process the fragments.
However this limit should be large enough - 10 to 20 characters - so
that it does not constitute a real restriction on your input. It should
also be 1 character less than a multiple of your computer-word length,
so that you can always include the terminal space in the aligned word.
Words are bounded by spaces. You can probably find objections to such a
simple definition. For instance, arithmetic expressions often do not
have spaces between words. We shall discuss this in Chapter 9. Let me
just say that we need to embed periods, dashes, and other characters in
words in order not to unreasonably restrict our potential vocabulary.
We'd like these to be words:
1,000 1.E-6 I.B.M. B&O 4'3" $4.95
### Message I/O
The WORD subroutine presumably examines input characters. Where does it
get these characters?
Although it's possible to read cards, I'm going to assume that you have
a keyboard to type input. Now there are 2 kinds of keyboards, buffered
and unbuffered. A buffered keyboard stores the message until you type an
end-of-message character. An unbuffered keyboard sends each character as
you type it. Your hardware, in turn, may buffer input for you or not.
In any case we may want to examine each character more than once, so we
want buffered input. Even if you can process characters as they arrive,
don't. Store them into a message buffer.
Set aside a 1-line message buffer. Its size is the maximum size of a
message, either input or output, so if you plan to use a 132 position
printer make it large enough.
If you simulate buffering, you should implement a backspace character
and a cancel message character. For you will make a lot of typing
errors. If your hardware buffers, but does not provide these
capabilities, you should do so. This probably means a prescan of the
input; any other technique gets too complicated, and probably costs more
in the end.
Mark the end of an input message with an end-of-message word. This is a
word bounded by spaces like any other. It may or may not coincide with
the end-of-message character that you typed, depending on your hardware
and character set as to whether the required spaces can be provided.
This word permits ready detection of the last word in a message. It will
have a specific definition and perform a valuable task.
In addition to a keyboard, you must have some sort of output device: a
printer or scope. Again it may be buffered or unbuffered. Unlike input,
we have no reason not to use unbuffered output. However if you have
several output devices, odds are one is buffered. If so, treat them all
as buffered, simulating the buffering where needed.
We will use the same message buffer for both input and output. My
motivation is to save space, or rather to increase the utilization of
space. My reasoning is that input and output are mutually exclusive.
There are exceptions, but we don't usually read input and prepare output
simultaneously. At least we never have to.
However, we do need a switch (1 bit) that states whether the message
buffer still contains input. The first time (or perhaps every time) we
type output, we must reset this switch. We'll use it later.
We need a receive subroutine that will exit when we have a complete
input message. Likewise a transmit subroutine that will exit after
sending an output message. It should await an acknowledgement if the
hardware provides one. Don't try to overlap transmission of one message
with preparation of the next. Transmission is so slow and preparation so
fast that no noticeable increase in speed is available. And it
complicates the program considerably.
### Moving characters
I will speak of fetching and depositing characters several times, mostly
concerned with input and output. For example, the WORD subroutine moves
characters from the message buffer to a word buffer. A simple task
conceptually, but a difficult one to implement. We would have exactly
the same problem moving arrays from place to place. But in fact we
needn't move arrays and we must move characters.
Let us define 2 entities: an input pointer and an output pointer. For
the moment you can think of them as index registers, although we will
have to generalize later. Let's also write 2 subroutines, although your
hardware may permit them to be instructions: FETCH will load the
character identified by the input pointer into a register, and advance
the input pointer; DEPOSIT will store that register at the position
identified by the output pointer, and advance the output pointer.
Depending on your computer, FETCH and DEPOSIT can be veery simple, or
extremely complex. If they require more than 1 instruction, they should
be subroutines, for we'll use them often. By combining them, we can
perform a move. However, it's important to be able to examine the
character before depositing it. A hardware move instruction is of little
value.
The input and output pointers use index registers. However, those
registers should only be used during a move. They should be loaded prior
to a move and saved after it, for they will be used for a number of
purposes, and it becomes impractical to store anything there
permanently.
## Decimal conversion
After isolating and aligning a word from the input string, your control
loop searches the dictionary for it. If it isn't in the dictionary, it
might be a number. A number is a special kind of word that doesn't need
a dictionary entry; by examining the word itself we can decide what to
do with it. The code executed for a number will place the binary
representation of the number onto the stack.
We will discuss the stack in the next section. First let's define a
number more precisely.
### Numbers
It is very hard to state exactly what is a number and what is not. You
will have to write a NUMBER subroutine to convert numbers to binary, and
this subroutine is the definition of a number. If it can convert a word
to binary, that word is a number; otherwise not.
It is foolish to examine a word to see if it is a number, and then to
convert the number to binary. Examination and conversion can be combined
into one process very easily.