-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbasic.sol
More file actions
2774 lines (2476 loc) · 119 KB
/
Copy pathbasic.sol
File metadata and controls
2774 lines (2476 loc) · 119 KB
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
; basic.sol -- an interpreter for BASIC: tokenise, parse, run.
;
; Run with: ./bin/solas programs/basic.sol && ./bin/solvm programs/basic.sob
;
; The eleventh program here, and the first that is an interpreter for *another
; language* rather than a tool for this one. The other ten read text, walk
; trees, copy files or run commands; this one holds a second language's state --
; a variable table, a program counter, a listing -- and is judged by whether a
; program written fifty years ago in a different notation gives the answer it
; gave then.
;
; It is deliberately not a BASIC of its own. The dialect is **ECMA-55 Minimal
; BASIC (1978)**, because a published standard means "done" is decided by
; somebody other than the author of the interpreter, and because Minimal BASIC
; is small enough to finish: twenty statements and eleven supplied functions.
;
; ---------------------------------------------------------------------------
; What is here so far
;
; **All twenty statements of the standard are here.**
;
; one `LET`, `PRINT`, `REM`, `END`, and the whole numeric expression
; grammar. The tokeniser, the expression parser and its tree, the line
; table, the run loop and its program counter, and `PRINT`'s output
; rules -- which are stranger than they look, and are why the
; demonstrations at the bottom have spaces in them where you would not
; expect any.
;
; two `GOTO`, `IF-THEN`, `FOR/NEXT`, `GOSUB/RETURN`, `ON-GOTO` and `STOP`.
; Three passes over the listing at load, so that a jump is an array
; index rather than a search, a jump to a line that does not exist is
; reported before anything runs, and a `FOR` knows where its `NEXT` is.
; It runs about 420,000 BASIC statements a second.
;
; four Text, arrays, `DIM`, `OPTION BASE`, `DATA`/`READ`/`RESTORE`, `INPUT`,
; `DEF FN`, `RANDOMIZE`, and five of the eleven supplied functions. A
; fourth pass at load, because `DATA`, `DEF` and `OPTION BASE` are all
; in force for the whole listing wherever they are written.
;
; three All eleven supplied functions and the `^` operator. Six of them and
; the operator waited two days on
; [3.14](../docs/COMPLETED.md#314-the-mathematics-that-is-not-here--done),
; an entry that had been waiting since it was written for a program
; that wanted an angle. This was that program, and it wanted six.
;
; five The rest of `PRINT`: six significant digits, no nought before a
; point, exponential form outside the range that can describe, `TAB(n)`,
; and a margin a comma wraps at. And a **recorded transcript** for each
; listing in [basic/](basic/), compared byte for byte on every build --
; which is what the claims in comments here cannot be, `programs/` not
; being one of the documentation checker's subjects.
;
; six **A prompt**, which is the interface BASIC actually had: a line with a
; number goes into the program, a line without one happens now, and six
; commands -- `LIST`, `RUN`, `NEW`, `LOAD`, `SAVE`, `BYE` -- do the
; rest. `./bin/solvm programs/basic.sob --repl`.
;
; **It is finished as a language.** Twenty statements, eleven functions, and
; every rule of the standard this file has found a way to check.
;
; ---------------------------------------------------------------------------
; What the conformance suite says
;
; The **NBS Minimal BASIC Test Programs, Version 2** are 208 programs written at
; the National Bureau of Standards in 1980 to test an implementation against
; ANSI X3.60-1978, which is the standard ECMA-55 mirrors. They are a US
; government work and in the public domain, and they are the only test of this
; interpreter that somebody else wrote. `basic/conformance.sh` fetches and runs
; them.
;
; ran to the end 99
; refused, and the test wanted that 99
; refused, and the test did not 5 -- all of them want a person
; at a keyboard; the harness
; has no one to offer
; over the harness's step limit 5 -- the statistical RND tests
; accepted where the standard is stricter 30 -- the table below
;
; **They found seven things, and every one of them was a real defect.** Not one
; had been caught by the eighty-three claims in this file, because those check
; what the author of the interpreter thought to check:
;
; `DATA` is raw text an unquoted datum runs to the next comma and may
; hold anything but one -- `DATA +. -` is legal.
; Reading it with the tokeniser refused a fifth of
; the suite.
; a datum has no type until a `READ` takes it. `DATA F,6` into `D$` is
; the string "6"; the same `6` into `A` is a number.
; `DEF` needs no parameter `DEF FNM=123`, referenced as a bare `FNM`.
; `NEXT` searches a listing may `GOTO` out of an inner loop, and
; then its `NEXT` must find its own `FOR` further
; down the stack.
; `FOR` always pushes two loops may run on one variable when the inner
; one is reached through `GOSUB`. Abandoning the
; outer frame was invented here, not read anywhere.
; `DIM` is a declaration the suite references arrays *before* the line that
; dimensions them, and says so in a comment.
; exceptions that continue `TAB(0)` must use 1, carry on, **and say so**.
; Every failure here was fatal until then.
;
; ---------------------------------------------------------------------------
; Where this is not the standard
;
; One place, and it is written here rather than left to be discovered:
;
; **Spaces between tokens are required, and ECMA-55 says they are not.** In the
; standard a space is insignificant outside a string, so `FORI=1TO10` and
; `PRI NT` are both legal and mean what you would guess. Here they are
; `'FORI' is not a statement` and `'PRI' is not a statement`.
;
; The cost of fixing it is why it has not been: a tokeniser that ignores spaces
; cannot work left to right on characters alone. `FORI` is `FOR I` only because
; a statement begins with a keyword, and `1TO10` is three tokens only because
; `TO` cannot continue a number -- so the scanner has to know where it is in the
; grammar, and this one deliberately does not. It is a day's work and a
; different design, not a missing branch.
;
; **What nobody writes is not the same as what the standard allows**, so this is
; a real gap rather than a pedantic one, and it is the only one.
;
; ---------------------------------------------------------------------------
; And where the standard let this choose
;
; Four things ECMA-55 leaves to the implementation, decided here and recorded so
; they read as decisions:
;
; six significant digits `PRINT 1/3` is `.333333`. The standard requires at
; least six and no more than that.
; print zones of 15 and a margin at 72, which is the paper everybody
; had.
; `END` need not be last the standard has exactly one and it closes the
; program; a listing typed to try something out is
; not improved by being told so.
; `INPUT` gives up where the standard asks again. A listing being fed
; from a file cannot usefully be asked twice.
;
; ---------------------------------------------------------------------------
; And where it accepts what the standard refuses
;
; Thirty of the suite's programs are legal here and are meant not to be. That is
; allowed -- P054 says a processor may *either* reject such a program *or*
; accept it and be accompanied by documentation describing what it does with it.
; This is that documentation.
;
; lower case accepted everywhere. Keywords fold; text in quotes
; does not, so `PRINT "Hello"` prints Hello.
; lines out of order a file's lines are sorted by number, not required
; to arrive in order. At a prompt that is the whole
; point, and the two cannot sensibly differ.
; any line number the standard allows 1 to 9999. Any positive whole
; number works here, and zero is refused because
; zero is this interpreter's word for *no line*.
; lines of any length the standard stops at 72 characters.
; `END` anywhere, or none the standard has exactly one and it comes last.
; strings of any length so the string overflow the standard requires a
; report for cannot happen.
; `A` and `A(1)` together the standard forbids one letter being both a
; variable and an array; here they are separate.
; `FOR I` inside `FOR I` lexically nested loops on one control variable are
; accepted, and **the inner loop wins**: `FOR`
; pushes a second frame and the first `NEXT I`
; closes the inner one, leaving the outer running.
; underflow is silent a value too small to represent becomes zero or a
; denormal without a word, where the standard asks
; for a report.
;
; Stage four went ahead of stage three because it turned out not to depend on
; it, and then took part of it anyway: `A(1)` and `ABS(1)` are the same syntax,
; so arrays could not be built without the machinery that calls a function.
;
; ---------------------------------------------------------------------------
; Why a line-numbered language is the easy case, which is not the obvious way round
;
; Line numbers have a bad name, and for this language they are a gift.
; [3.5](../docs/ROADMAP.md#35-recursion-is-limited-to-about-254-levels) caps
; recursion at about 254 frames, and an interpreter for a modern language would
; meet that cap at once: a tree-walking evaluator spends frames in proportion to
; how deeply the *source* nests, so a long function would run out of machine
; before it ran out of program.
;
; A line-numbered BASIC never nests. Its run loop is a program counter over a
; sorted table of lines, and every construct that looks like nesting -- `GOSUB`,
; `FOR` -- is an explicit stack in an array, which is heap and not frames. So
; the only recursion here is in the expression parser, it runs once at load
; rather than once per execution, and BASIC expressions are shallow.
;
; [evaluator.sol](evaluator.sol) reaches 83 brackets with a three-level grammar.
; This one has four levels and reaches **59**, measured at the bottom of this
; file. No BASIC program written by a person will come near either number.
@include "scan.sol".
@include "control.sol".
; ---------------------------------------------------------------------------
; Characters
;
; BASIC is an uppercase language. The source is *not* uppercased wholesale,
; because that would reach inside string literals and change what a program
; prints -- `PRINT "Hello"` must still say Hello. Only word tokens are folded,
; at the point they are made.
;
; Each of these takes nil without complaining, because the one caller that looks
; ahead (`peekAt`, in `tokenise`) can be looking at the end of the line. A
; character class asked about the absence of a character should answer no.
digits := "0123456789".
letters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ".
isDigit := { c | c:notNil:and({ digits:indexOf(c):notNil }) }.
isLetter := { c | c:notNil:and({ letters:indexOf(c:asUppercase):notNil }) }.
isSpace := { c | c:notNil:and({ c:equals(" "):or({ c:equals("\t") }) }) }.
; ---------------------------------------------------------------------------
; Tokens
token := object:new.
token:kind := 'word. ; 'number 'string 'word 'punct
token:text := "".
makeToken := { kind, text | | t |
t := token:new. t:kind := kind. t:text := text. t }.
; ---------------------------------------------------------------------------
; The tree
;
; The same shape [evaluator.sol](evaluator.sol) uses, for the same reason: a
; node is an object with a kind and the fields that kind needs, and the fields
; it does not need are nil.
node := object:new.
node:kind := 'number. ; 'number 'string 'variable 'binary 'negate 'index 'call
node:value := nil. ; 'number, 'string
node:name := "". ; 'variable, 'index, 'call
node:op := "". ; 'binary
node:left := nil.
node:right := nil.
node:args := nil. ; 'index (the subscripts), 'call (the arguments)
numberNode := { v | | n | n := node:new. n:kind := 'number. n:value := v. n }.
stringNode := { v | | n | n := node:new. n:kind := 'string. n:value := v. n }.
variableNode := { name | | n | n := node:new. n:kind := 'variable. n:name := name. n }.
binaryNode := { op, l, r | | n |
n := node:new. n:kind := 'binary. n:op := op. n:left := l. n:right := r. n }.
negateNode := { x | | n | n := node:new. n:kind := 'negate. n:left := x. n }.
indexNode := { name, args | | n |
n := node:new. n:kind := 'index. n:name := name. n:args := args. n }.
callNode := { name, args | | n |
n := node:new. n:kind := 'call. n:name := name. n:args := args. n }.
; ---------------------------------------------------------------------------
; An array
;
; The cells are one flat array whatever the rank, with the subscripts folded
; into an index -- which is what `dims` is kept for. Two dimensions is the most
; Minimal BASIC allows.
arrayValue := object:new.
arrayValue:dims := nil. ; the upper bound of each subscript
arrayValue:cells := nil.
; ---------------------------------------------------------------------------
; Statements
;
; A statement is parsed once, at load, into one of these. The run loop then
; walks the objects rather than the text, which is not an optimisation so much
; as the difference between an interpreter and a toy: a `FOR` loop in stage two
; will run its body tens of thousands of times, and re-reading the characters
; each pass would make the cost of the loop the cost of parsing it.
statement := object:new.
statement:kind := 'rem. ; 'rem 'let 'print 'end 'goto 'gosub 'return
; 'if 'for 'next 'ongoto 'stop
statement:name := "". ; 'let, 'for, 'next
statement:expr := nil. ; 'let, 'for (the initial value), 'ongoto
statement:items := nil. ; 'print
statement:left := nil. ; 'if
statement:op := "". ; 'if
statement:right := nil. ; 'if
statement:limit := nil. ; 'for
statement:step := nil. ; 'for, nil when none was written
statement:pair := #0. ; 'for and 'next: each other's place in the run order
statement:target := nil. ; 'let: the variable or array element assigned to
; **What was typed, kept so that `LIST` can print it.** The rest of this file
; parses a line and throws the text away, which is right for a program read from
; a file and wrong at a prompt: a listing you are editing has to be shown back
; to you, and re-deriving the text from the tree would print something you did
; not write.
statement:source := "".
; **Line numbers written in a statement, and where they landed.** A `GOTO` says
; a line number and the run loop wants an index into `order`, so the lookup
; happens once at load rather than once per jump -- which matters because the
; jumps in a BASIC program are the loop. `targets` is what the listing said and
; `resolved` is where it points; a target that names no line is caught at load,
; where it can be reported before anything has run.
statement:targets := nil.
statement:resolved := nil.
; A `FOR` in flight: the control variable, where the body starts, and the limit
; and step as they were **when the loop began**. Evaluating them once is the
; standard's rule and not an optimisation -- `FOR I = 1 TO N` where the body
; assigns to `N` runs the number of times `N` named at the start.
loopFrame := object:new.
loopFrame:name := "".
loopFrame:limit := 0.0.
loopFrame:step := 1.0.
loopFrame:body := #1.
; A `PRINT` item is an expression and the separator that came *after* it, which
; is what decides where the next thing goes -- so the separator belongs to the
; item on its left rather than sitting between two of them.
printItem := object:new.
printItem:expr := nil.
printItem:tab := nil. ; the column of a TAB(n), when the item is one
printItem:sep := 'none. ; 'none 'comma 'semi
; ---------------------------------------------------------------------------
; The machine
;
; An object rather than a set of globals, so two listings can be in flight at
; once. That is the shape [lib/scan.sol](../lib/scan.sol) settled on after
; [lib/json.sol](../lib/json.sol) spent four releases unable to parse one
; document while parsing another, and it costs nothing to do it right the first
; time.
basic := object:new.
basic:lines := nil. ; line number -> statement
basic:order := nil. ; line numbers, sorted: the run order
basic:pc := #1. ; an index into `order`, not a line number
basic:vars := nil.
basic:running := false.
basic:atLine := #0. ; the line being parsed or run, for error messages
basic:out := "". ; the print line being built -- see `flush`
basic:tokens := nil. ; the line being parsed, tokenised
basic:cursor := #1. ; the index of the next unread token
basic:index := nil. ; line number -> its place in `order`
basic:jumped := false. ; whether the statement just run moved the counter
basic:calls := nil. ; GOSUB return places, innermost last
basic:loops := nil. ; FOR frames, innermost last
basic:arrays := nil. ; name -> arrayValue
basic:defined := nil. ; FNx -> the DEF statement that defines it
basic:data := nil. ; every DATA value in the listing, in line order
basic:dataAt := #1. ; how far READ has got through it
basic:rng := nil. ; RND's generator; RANDOMIZE replaces it
basic:dirty := true. ; whether the load-time passes need running again
basic:dims := nil. ; the arrays DIM declares, name -> bounds
; How a non-fatal exception is announced. **A block in a slot is a method**, so
; this cannot be held as data and asked for back -- `self:report` *calls* it. The
; default is therefore a block that does nothing rather than a nil to test for,
; which is shorter anyway and has no branch in it.
basic:report := { text | nil }.
basic:base := #0. ; the lowest subscript, which OPTION BASE sets
; Every failure names the line it happened on, because in a language whose
; control flow is line numbers that is the only address a person has.
;
; **Line zero means there is no line**, and then the message says only what went
; wrong. That is a prompt, where `LOAD "nothing.bas"` has no more to do with the
; last line that ran than with any other -- and it is also a file whose first
; line has no number at all. Naming a line that had nothing to do with it is the
; kind of true sentence about the wrong thing this file has already been caught
; by twice.
; ---------------------------------------------------------------------------
; The exceptions that are not failures
;
; ECMA-55 has a class of exception the standard says to **report and carry on
; from**, with a defined value put in place of the bad one -- `TAB(0)` is one:
; the argument becomes 1, execution continues, and a message must say so. Every
; error here was fatal until the conformance suite asked for that, because
; nothing had needed a complaint that was not also a stop.
;
; Where it goes is the caller's business, which is why it is a block in a slot
; rather than a `display` written here: a listing run from a file sends it to
; standard error with the failures, and the prompt puts it on the screen with
; everything else it says.
basic:warn := { message |
self:report(self:atLine:equals(#0):ifElse(
{ message },
{ "line {}: {}":fill([self:atLine, message]) })) }.
basic:fail := { message |
self:atLine:equals(#0):ifElse(
{ error:raise(message) },
{ error:raise("line {}: {}":fill([self:atLine, message])) }) }.
; ---------------------------------------------------------------------------
; Tokenising
;
; One line at a time, not the whole listing, because BASIC is line-oriented and
; because `REM` swallows the rest of its line as raw text -- text that need not
; tokenise at all. `10 REM DON'T` is a legal line and an apostrophe is not a
; token, so `REM` has to be recognised before the tokeniser sees it and not
; after.
;
; The cursor is [lib/scan.sol](../lib/scan.sol)'s, which makes this the sixth
; file to use it and the first that is not a rewrite of a cursor it had already
; written for itself.
;
; The dispatch is [control.sol](../lib/control.sol)'s `ifElseIf`, and this is
; the shape that file's own example is written in: a scanner deciding what a
; character starts. [disasm.sol](disasm.sol) reaches for it in the same place --
; deciding what a constant tag means -- which is two programs arriving at one
; use, and a fair sign the library named the right thing. Flat, five arms, the last one the default because it is
; last -- against a four-deep nest of `ifElse` closing with `}) }) }) })`.
;
; **It is not free and it is affordable here.** Tokenising 2,000 lines takes
; 0.32s as a nest and 0.43s this way, a third more; a listing anybody actually
; types is a few hundred lines, so that is single-digit milliseconds. And it
; costs no depth at all, because this loop is not inside the recursion -- the
; measurement at the bottom of the file is unchanged either way. Both halves of
; control.sol's advice, checked rather than quoted.
basic:tokenise := { text | | s, out, c |
s := scan:on(text).
out := array:new.
{ s:atEnd:not }:whileTrue({
c := s:peek.
[ { isSpace:value(c) }, { s:step },
{ isDigit:value(c):or({ c:equals("."):and({
isDigit:value(s:peekAt(#1)) }) }) },
{ out:add(self:numberToken(s)) },
{ isLetter:value(c) }, { out:add(self:wordToken(s)) },
{ c:equals("\"") }, { out:add(self:quotedToken(s)) },
{ out:add(self:punctToken(s)) } ]:ifElseIf }).
out }.
; A numeric literal: digits, an optional fraction, an optional exponent. The
; text is kept and converted later by `asFloat`, which already reads every one
; of these forms -- so this only has to decide where the number *ends*.
basic:numberToken := { s | | start |
start := s:pos.
s:skipWhile({ c | isDigit:value(c) }).
s:match("."):ifTrue({ s:skipWhile({ c | isDigit:value(c) }) }).
s:peek:notNil:and({ s:peek:asUppercase:equals("E") }):ifTrue({
s:step.
s:match("+"):ifFalse({ s:match("-") }).
s:skipWhile({ c | isDigit:value(c) }) }).
makeToken:value('number, s:since(start)) }.
; Folded to uppercase here and nowhere else, so `print x` and `PRINT X` are one
; program while `PRINT "x"` still prints a small x.
basic:wordToken := { s | | text |
text := s:takeWhile({ c | isLetter:value(c):or({ isDigit:value(c) }) }):asUppercase.
; The `$` is part of the name and not an operator: `A$` is one variable and
; the dollar is how its type is spelt. Minimal BASIC has exactly two types
; and no way to declare either, so the name carries it.
makeToken:value('word, s:match("$"):ifElse({ text:concat("$") }, { text })) }.
; Minimal BASIC has no escapes inside a string and so no way to write a quote in
; one. That is the standard rather than a shortcut taken here: the closing quote
; is simply the next one.
basic:quotedToken := { s | | text |
s:step.
text := s:takeUntil({ c | c:equals("\"") }).
s:match("\""):ifFalse({ self:fail("a string was never closed") }).
makeToken:value('string, text) }.
; The two-character relational operators are read now although nothing until
; stage two can use one, because the alternative is a tokeniser that reports `<`
; and `=` separately and an `IF` that has to put them back together.
basic:punctToken := { s | | c |
c := s:next.
c:equals("<"):ifTrue({
s:match("="):ifElse({ c := "<=" }, { s:match(">"):ifTrue({ c := "<>" }) }) }).
c:equals(">"):ifTrue({ s:match("="):ifTrue({ c := ">=" }) }).
"+-*/^(),;=<>":indexOf(c:at(#1)):isNil:ifTrue({
self:fail("'{}' means nothing here":fill([c])) }).
makeToken:value('punct, c) }.
; ---------------------------------------------------------------------------
; Reading a listing
;
; A line is a number and then a statement. The number is not decoration: it is
; the line's name, the thing `GOTO` takes an argument of in stage two, and the
; order the program runs in -- which is the order of the *numbers* and not the
; order they were typed.
basic:load := { source |
self:lines := dictionary:new.
self:vars := dictionary:new.
self:out := "".
source:split("\n"):do({ line | self:loadLine(line) }).
self:link }.
; ---------------------------------------------------------------------------
; Linking, and why it is separate from reading
;
; The four passes below need the **whole** program: a `GOTO` cannot be resolved
; until every line exists, and a `FOR` cannot find its `NEXT`. Reading a file
; hands over the whole thing at once, so the two used to be one step.
;
; At a prompt they cannot be. `10 GOTO 100` is a perfectly ordinary thing to
; type before line 100 exists, and refusing it would make the prompt unusable.
; So entering a line **parses** it -- which catches a syntax error where it was
; typed, as BASIC does -- and marks the program unlinked; `RUN` links it.
;
; That is the cost of having moved the line lookups to load time, arriving where
; it was always going to: the thing that makes a jump an array index is the
; thing that makes an edit invalidate one.
basic:link := {
self:order := self:lines:keys:sorted.
self:placeLines.
self:resolveTargets.
self:pairLoops.
self:gather.
self:dirty := false }.
basic:linkIfNeeded := { self:dirty:ifTrue({ self:link }) }.
; ---------------------------------------------------------------------------
; Three passes over the loaded listing, all of them at load
;
; A BASIC program is a graph and not a sequence, and the edges are line numbers.
; Every one of them is followed here before anything runs, which buys three
; things: a jump is an array index rather than a search, a `GOTO` to a line that
; does not exist is reported before the program prints anything, and a `FOR`
; with no `NEXT` is a listing error rather than a surprise at run time.
basic:placeLines := {
self:index := dictionary:new.
[#1, self:order:size]:loop({ i |
self:index:atPut(self:order:at(i), i) }) }.
; Without this a jump would be `order:indexOf(line)`, a scan of the whole
; listing -- and the jumps in a BASIC program are its loops, so the scan would
; be per iteration. It is the one place where an interpreter for a language with
; line numbers has to do something a tree-walking one never would.
basic:resolveTargets := {
self:order:do({ line |
self:atLine := line.
self:resolveOne(self:lines:at(line)) }) }.
basic:resolveOne := { st |
st:targets:isNil:ifFalse({
st:resolved := st:targets:collect({ n |
self:index:includes(n):ifFalse({
self:fail("there is no line {}":fill([n])) }).
self:index:at(n) }) }) }.
; `FOR` and `NEXT` find each other here rather than at run time, which needs
; them properly nested -- and the standard requires that, so following it is
; free. The pairing is what lets a `FOR` whose range is empty skip its body: it
; already knows where the body ends.
; `DATA` and `DEF` are both in force for the whole listing regardless of where
; they are written, so both are collected before anything runs. One walk does
; the two of them because they are the same walk, not because they are related.
basic:gather := { | dimmed, based |
self:data := array:new.
self:defined := dictionary:new.
self:dims := dictionary:new.
self:base := #0.
dimmed := false.
based := false.
self:order:do({ line | | st |
st := self:lines:at(line).
self:atLine := line.
st:kind:equals('data):ifTrue({
st:items:do({ v | self:data:add(v) }) }).
st:kind:equals('dim):ifTrue({
dimmed := true.
st:items:do({ each |
self:dims:includes(each:at(#1)):ifTrue({
self:fail("{} is given bounds twice":fill([each:at(#1)])) }).
self:dims:atPut(each:at(#1), each:at(#2)) }) }).
st:kind:equals('option):ifTrue({
based:ifTrue({ self:fail("OPTION BASE is said twice") }).
dimmed:ifTrue({
self:fail("OPTION BASE comes before any DIM, not after") }).
based := true.
self:base := st:pair }).
st:kind:equals('def):ifTrue({
self:defined:includes(st:name):ifTrue({
self:fail("{} is defined twice":fill([st:name])) }).
self:defined:atPut(st:name, st) }) }).
self:dataAt := #1 }.
basic:pairLoops := { | stack, st, opening |
stack := array:new.
[#1, self:order:size]:loop({ i |
st := self:lines:at(self:order:at(i)).
self:atLine := self:order:at(i).
st:kind:equals('for):ifTrue({ stack:add(i) }).
st:kind:equals('next):ifTrue({
stack:size:equals(#0):ifTrue({ self:fail("NEXT without a FOR") }).
opening := stack:at(stack:size).
stack:removeLast.
self:lines:at(self:order:at(opening)):name:equals(st:name):ifFalse({
self:fail("NEXT {} closes FOR {}"
:fill([st:name, self:lines:at(self:order:at(opening)):name])) }).
self:lines:at(self:order:at(opening)):pair := i.
st:pair := opening }) }).
stack:size:equals(#0):ifFalse({
self:atLine := self:order:at(stack:at(stack:size)).
self:fail("FOR without a NEXT") }) }.
basic:loadLine := { line | | s, number, rest |
s := scan:on(line).
s:skipWhile({ c | isSpace:value(c) }).
s:atEnd:ifFalse({
number := s:takeWhile({ c | isDigit:value(c) }).
number:equals(""):ifTrue({
self:atLine := #0.
self:fail("a line must start with a line number: {}":fill([line:trim])) }).
self:atLine := number:asInteger.
; Two lines with one number is a mistake in a listing rather than a
; redefinition: the second would silently win, and which of the two ran
; would depend on nothing the person reading the listing can see. At a
; prompt the same thing is an edit, which is `enterLine` below and the
; one place these two disagree.
self:lines:includes(self:atLine):ifTrue({
self:fail("line {} appears twice":fill([self:atLine])) }).
rest := s:rest:trim.
rest:equals(""):ifTrue({
self:fail("this line has a number and nothing else") }).
self:store(self:atLine, rest) }) }.
basic:store := { number, rest | | st |
st := self:parseStatement(rest).
st:source := "{} {}":fill([number, rest]).
self:lines:atPut(number, st).
self:dirty := true }.
; ---------------------------------------------------------------------------
; Which statement it is
;
; A dictionary of blocks, keyed by the keyword. There are three ways to write a
; dispatch in this language and this is the third of them, so it is worth saying
; why rather than leaving it to look like the first thing that came to mind.
;
; A **staircase of `ifElse`** is out because there are twenty statements to
; recognise and [3.2](../docs/ROADMAP.md#32-no-non-local-return) gives no early
; return to leave a chain from, so it would be twenty levels deep and a wall of
; brackets at the end.
;
; **[ifElseIf](../lib/control.sol)** is the library's answer to exactly that,
; and it is the right shape for a flat dispatch -- the tokeniser below uses it.
; It is the wrong shape *here* because it tests its conditions in order: with
; twenty keywords, recognising `STOP` means twenty block calls and twenty
; string comparisons. A dictionary asks once. The trade turns on whether the
; conditions are arbitrary questions or all the same question asked about
; different constants, and a keyword table is the second.
;
; Each block is handed the machine, since a block in a dictionary is not a
; method and has no `self` of its own. They are bound at the top level, where
; the frame they capture lasts as long as the program does; a dictionary of
; blocks built inside a method would fall foul of
; [3.1](../docs/ROADMAP.md#31-capturing-blocks-cannot-escape-their-frame).
parsers := dictionary:new.
basic:parseStatement := { text | | tokens, keyword |
; `REM` is tested against the raw text before anything is tokenised, because
; what follows it is not a token sequence. A line beginning `REMOVE` is a
; remark too: the standard says the rest of the line is ignored after `REM`,
; and every BASIC ever written reads that as literally as this does.
;
; **`DATA` is the same, and that took the NBS suite to notice.** Its items
; are not tokens either: an unquoted datum runs to the next comma and may
; hold anything but one, so `DATA +. -` is three characters of perfectly
; legal string. Reading DATA with the tokeniser rejected a fifth of the
; conformance programs.
text:size:greaterOrEqual(#4)
:and({ text:copyFrom(#1, #4):asUppercase:equals("DATA") })
:ifElse({ self:parseData(text:copyFrom(#5, text:size)) }, {
text:size:greaterOrEqual(#3)
:and({ text:copyFrom(#1, #3):asUppercase:equals("REM") })
:ifElse({ self:statementOf('rem) }, {
tokens := self:tokenise(text).
tokens:size:equals(#0):ifTrue({ self:fail("there is no statement here") }).
tokens := self:joinGo(tokens).
keyword := tokens:at(#1).
keyword:kind:equals('word):ifFalse({
self:fail("a statement starts with a keyword, not '{}'"
:fill([keyword:text])) }).
parsers:includes(keyword:text):ifFalse({
self:fail("'{}' is not a statement in Minimal BASIC"
:fill([keyword:text])) }).
parsers:at(keyword:text):value(self, tokens) }) }) }.
basic:statementOf := { kind | | st |
st := statement:new. st:kind := kind. st }.
; ECMA-55 requires the word `LET`. Later BASICs made it optional and this one
; does not, because the standard is the whole reason for having chosen a dialect
; and the first convenience is the one that makes the second hard to refuse.
parsers:atPut("LET", { m, tokens | | st, target |
tokens:size:lessThan(#4):ifTrue({
m:fail("LET needs a variable, an = and a value") }).
st := m:statementOf('let).
; The left side is parsed by the ordinary expression machinery and then
; checked, rather than being read by hand -- `A`, `A$` and `A(I,J)` are
; already three shapes and `primary` knows all of them. `=` is not an
; operator in this grammar, so the parse stops exactly where it should.
target := m:parse(tokens, #2).
target:kind:equals('variable):or({ target:kind:equals('index) }):ifFalse({
m:fail("LET assigns to a variable or an array element") }).
st:target := target.
m:expect(tokens, m:cursor, "=", "LET needs an = after the variable").
st:expr := m:parse(tokens, m:cursor:add(#1)).
st }).
parsers:atPut("END", { m, tokens |
tokens:size:equals(#1):ifFalse({ m:fail("END takes nothing after it") }).
m:statementOf('end) }).
; `STOP` and `END` do the same thing here. The standard distinguishes them by
; where they may appear -- `END` is the last line of the program and there is
; exactly one -- and this does not enforce that, because a listing typed to try
; something out is not improved by being told it has no `END`.
parsers:atPut("STOP", { m, tokens |
tokens:size:equals(#1):ifFalse({ m:fail("STOP takes nothing after it") }).
m:statementOf('stop) }).
; ---------------------------------------------------------------------------
; GO TO, which is two words
;
; The standard writes `GO TO` and `GO SUB` with a space, because spaces are not
; significant in Minimal BASIC and the two spellings are the same statement.
; This tokeniser splits on spaces, so it sees either one word or two, and the
; two are put back together here -- before the keyword table is asked, so the
; table has one entry per statement rather than two.
basic:joinGo := { tokens | | rest |
tokens:at(#1):text:equals("GO"):and({ tokens:size:greaterThan(#1) })
:and({ tokens:at(#2):text:equals("TO"):or({
tokens:at(#2):text:equals("SUB") }) })
:ifElse({
rest := array:of(makeToken:value('word,
"GO":concat(tokens:at(#2):text))).
[#3, tokens:size]:loop({ i | rest:add(tokens:at(i)) }).
rest },
{ tokens }) }.
; A line number is a whole number and nothing else. `10.5` tokenises as a
; perfectly good numeric literal, which is why this is checked rather than
; assumed: `GOTO 10.5` should say what is wrong with it and not round.
basic:lineNumber := { t | self:wholeNumber(t, "a line number") }.
basic:tokenAt := { tokens, i |
i:greaterThan(tokens:size):ifElse({ nil }, { tokens:at(i) }) }.
basic:expect := { tokens, i, text, message | | t |
t := self:tokenAt(tokens, i).
t:isNil:or({ t:text:equals(text):not }):ifTrue({ self:fail(message) }).
t }.
; A whole number written out, which `DIM` and every line number need and which
; the tokeniser will happily have read as `10.5` or `1E3`.
basic:wholeNumber := { t, what |
t:isNil:ifTrue({ self:fail("{} is missing":fill([what])) }).
t:kind:equals('number)
:and({ t:text:indexOf("."):isNil })
:and({ t:text:asUppercase:indexOf("E"):isNil })
:ifFalse({ self:fail("'{}' is not {}":fill([t:text, what])) }).
t:text:asInteger }.
parsers:atPut("GOTO", { m, tokens | | st |
tokens:size:equals(#2):ifFalse({ m:fail("GOTO takes one line number") }).
st := m:statementOf('goto).
st:targets := [m:lineNumber(tokens:at(#2))].
st }).
parsers:atPut("GOSUB", { m, tokens | | st |
tokens:size:equals(#2):ifFalse({ m:fail("GOSUB takes one line number") }).
st := m:statementOf('gosub).
st:targets := [m:lineNumber(tokens:at(#2))].
st }).
parsers:atPut("RETURN", { m, tokens |
tokens:size:equals(#1):ifFalse({ m:fail("RETURN takes nothing after it") }).
m:statementOf('return) }).
; ---------------------------------------------------------------------------
; IF, which in this dialect can only jump
;
; IF <expression> <relation> <expression> THEN <line number>
;
; **`THEN` takes a line number and not a statement.** `IF X > 0 THEN PRINT "YES"`
; is not Minimal BASIC; it is `IF X > 0 THEN 100`, with the work on line 100 and
; a `GOTO` round it. Every dialect after this one allowed the statement form,
; which is why the restriction reads like a missing feature rather than the
; standard being kept -- the same shape as the sign rule in the expression
; grammar, and refused for the same reason.
relations := ["=", "<>", "<", "<=", ">", ">="].
parsers:atPut("IF", { m, tokens | | st, t |
st := m:statementOf('if).
st:left := m:parse(tokens, #2).
t := m:tokenAt(tokens, m:cursor).
t:isNil:ifTrue({ m:fail("IF needs a comparison") }).
relations:indexOf(t:text):isNil:ifTrue({
m:fail("'{}' is not a comparison: one of = <> < <= > >=":fill([t:text])) }).
st:op := t:text.
st:right := m:parse(tokens, m:cursor:add(#1)).
t := m:tokenAt(tokens, m:cursor).
t:isNil:or({ t:text:equals("THEN"):not }):ifTrue({
m:fail("IF needs THEN and a line number") }).
t := m:tokenAt(tokens, m:cursor:add(#1)).
t:isNil:ifTrue({ m:fail("THEN needs a line number") }).
; Named rather than left to `lineNumber`, because the mistake here is
; almost never a mistyped number -- it is knowing a later BASIC.
t:kind:equals('word):ifTrue({
m:fail("THEN takes a line number in this dialect, not a statement: "
:concat("put the {} on its own line and jump to it")
:fill([t:text])) }).
st:targets := [m:lineNumber(t)].
m:cursor:add(#2):lessOrEqual(tokens:size):ifTrue({
m:fail("THEN takes a line number and nothing after it") }).
st }).
; ---------------------------------------------------------------------------
; ON ... GOTO -- the computed jump
;
; ON <expression> GOTO <line>, <line>, ...
;
; The value picks a line by its position in the list, counting from one. Out of
; range is an error rather than a fall-through, which is the standard's reading
; and the useful one: a computed jump that quietly does nothing is a bug that
; looks like a working program.
parsers:atPut("ON", { m, tokens | | st, t, i |
st := m:statementOf('ongoto).
st:expr := m:parse(tokens, #2).
t := m:tokenAt(tokens, m:cursor).
t:isNil:or({ t:text:equals("GOTO"):not }):ifTrue({
m:fail("ON needs GOTO and a list of line numbers") }).
st:targets := array:new.
i := m:cursor:add(#1).
{ i:lessOrEqual(tokens:size) }:whileTrue({
st:targets:add(m:lineNumber(tokens:at(i))).
i := i:add(#1).
i:lessOrEqual(tokens:size):ifTrue({
tokens:at(i):text:equals(","):ifFalse({
m:fail("line numbers after GOTO are separated by commas") }).
i := i:add(#1).
i:greaterThan(tokens:size):ifTrue({
m:fail("a comma with no line number after it") }) }) }).
st:targets:size:equals(#0):ifTrue({ m:fail("ON GOTO needs a line number") }).
st }).
; ---------------------------------------------------------------------------
; FOR and NEXT
;
; FOR <variable> = <expression> TO <expression> [STEP <expression>]
; NEXT <variable>
;
; The limit and the step are evaluated **once, when the loop starts**, which is
; the standard's rule rather than an optimisation: `FOR I = 1 TO N` where the
; body assigns to `N` runs the number of times `N` named at the start. The test
; happens before the body, so a loop whose range is already empty runs no times.
parsers:atPut("FOR", { m, tokens | | st, t |
st := m:statementOf('for).
st:name := m:numericName(m:tokenAt(tokens, #2)).
t := m:tokenAt(tokens, #3).
t:isNil:or({ t:text:equals("=") :not }):ifTrue({
m:fail("FOR needs an = after the variable") }).
st:expr := m:parse(tokens, #4).
t := m:tokenAt(tokens, m:cursor).
t:isNil:or({ t:text:equals("TO"):not }):ifTrue({ m:fail("FOR needs TO") }).
st:limit := m:parse(tokens, m:cursor:add(#1)).
t := m:tokenAt(tokens, m:cursor).
t:isNil:ifElse({ st:step := nil }, {
t:text:equals("STEP"):ifFalse({
m:fail("FOR takes STEP and nothing else after the limit") }).
st:step := m:parse(tokens, m:cursor:add(#1)).
m:cursor:lessOrEqual(tokens:size):ifTrue({
m:fail("STEP takes one value") }) }).
st }).
parsers:atPut("NEXT", { m, tokens | | st |
tokens:size:equals(#2):ifFalse({ m:fail("NEXT takes one variable") }).
st := m:statementOf('next).
st:name := m:numericName(tokens:at(#2)).
st }).
; ---------------------------------------------------------------------------
; DIM
;
; DIM A(10), B(3,3)
;
; The bounds are written-out whole numbers and not expressions, which is the
; standard: an array's size is a property of the listing rather than of the run,
; so it can be read off the page.
parsers:atPut("DIM", { m, tokens | | st, i, name, dims |
st := m:statementOf('dim).
st:items := array:new.
i := #2.
{ i:lessOrEqual(tokens:size) }:whileTrue({
name := m:arrayName(m:variableName(m:tokenAt(tokens, i))).
i := i:add(#1).
m:expect(tokens, i, "(",
"DIM needs a bound in brackets after {}":fill([name])).
i := i:add(#1).
dims := array:new.
dims:add(m:wholeNumber(m:tokenAt(tokens, i), "a bound")).
i := i:add(#1).
{ m:tokenAt(tokens, i):notNil
:and({ m:tokenAt(tokens, i):text:equals(",") }) }:whileTrue({
i := i:add(#1).
dims:add(m:wholeNumber(m:tokenAt(tokens, i), "a bound")).
i := i:add(#1) }).
dims:size:greaterThan(#2):ifTrue({
m:fail("{} has {} subscripts, and this dialect allows two"
:fill([name, dims:size])) }).
m:expect(tokens, i, ")", "DIM: a ( was never closed").
i := i:add(#1).
st:items:add([name, dims]).
m:tokenAt(tokens, i):notNil:ifTrue({
m:expect(tokens, i, ",", "DIM separates arrays with commas").
i := i:add(#1).
m:tokenAt(tokens, i):isNil:ifTrue({
m:fail("DIM: a comma with nothing after it") }) }) }).
st:items:size:equals(#0):ifTrue({ m:fail("DIM needs an array") }).
st }).
; ---------------------------------------------------------------------------
; DEF, which defines one function of one number
;
; DEF FNS(X) = X * X
;
; Not a statement that runs: the definition is in force for the whole listing
; however far down it is written, so these are collected at load with the DATA
; and the run loop steps over them.
parsers:atPut("DEF", { m, tokens | | st, name |
st := m:statementOf('def).
name := m:tokenAt(tokens, #2).
name:isNil:or({ name:kind:equals('word):not }):ifTrue({
m:fail("DEF needs a name like FNA") }).
name:text:size:equals(#3)
:and({ name:text:copyFrom(#1, #2):equals("FN") })
:and({ isLetter:value(name:text:at(#3)) })
:ifFalse({ m:fail("'{}' is not a function name: FN and one letter"
:fill([name:text])) }).
st:name := name:text.
; **The parameter is optional**, which the NBS suite found: `DEF FNM=123` is
; a function of nothing, referenced as a bare `FNM`. An empty list rather
; than a nil, so that everything downstream counts rather than asks.
m:tokenAt(tokens, #3):isNil:ifTrue({
m:fail("DEF needs an = and then the expression") }).
m:tokenAt(tokens, #3):text:equals("("):ifElse({
st:items := [m:numericName(m:tokenAt(tokens, #4))].
m:expect(tokens, #5, ")", "DEF: a ( was never closed").
m:expect(tokens, #6, "=", "DEF needs an = and then the expression").
st:expr := m:parse(tokens, #7) }, {
st:items := array:new.
m:expect(tokens, #3, "=", "DEF needs an = and then the expression").
st:expr := m:parse(tokens, #4) }).
m:cursor:lessOrEqual(tokens:size):ifTrue({
m:fail("DEF takes one expression") }).
st }).
; ---------------------------------------------------------------------------
; DATA, READ and RESTORE -- a listing's own input
;
; Every `DATA` in the program is one list, in line order, however the lines are
; scattered. `READ` walks it and `RESTORE` goes back to the beginning. It is the
; oldest way a program carried its own input, and it is why so much BASIC has a
; wall of numbers at the bottom.
;
; A `DATA` item is a constant and never an expression, so these are read here
; and not parsed into a tree. An unquoted word is text -- `DATA JANUARY` is the
; string, not a variable -- which is the standard and catches everybody once.
; **A datum is kept as the text that was written**, and turned into a number or
; a string by the `READ` that takes it -- because in this language the *variable*
; decides which it is. `DATA F,6` read into `D$` is the string "6", and the same
; `6` read into `A` is the number. Deciding at DATA time, which is what this did
; first, gets that exactly backwards.
basic:parseData := { text | | st, s, item |
st := self:statementOf('data).
st:items := array:new.
s := scan:on(text).
{ true }:doUntil({
s:skipWhile({ c | isSpace:value(c) }).
item := s:peek:notNil:and({ s:peek:equals("\"") })
:ifElse({ self:quotedDatum(s) }, { self:plainDatum(s) }).
st:items:add(item).
s:skipWhile({ c | isSpace:value(c) }).
s:atEnd:ifElse({ true }, {
s:match(","):ifFalse({
self:fail("DATA separates values with commas") }).
false }) }).
st:items:size:equals(#0):ifTrue({ self:fail("DATA needs a value") }).
st }.