forked from scotws/TaliForth
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathTali-Forth.asm
7799 lines (6718 loc) · 258 KB
/
Tali-Forth.asm
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
; TALI FORTH FOR THE 65C02
; Scot W. Stevenson <scot.stevenson@gmail.com>
;
; First version 19. Jan 2014
; This version 31. Dec 2016 (BETA)
; -----------------------------------------------------------------------------
; This program is placed in the public domain.
; Note that it is distributed in the hope that it will be useful, but WITHOUT
; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
; FITNESS FOR A PARTICULAR PURPOSE. Use at your own risk. See COPYING.txt
; for details.
; -----------------------------------------------------------------------------
; Developed with the Ophis assembler and py65mon simulator
; -----------------------------------------------------------------------------
.org FORTH
; =============================================================================
; HELPER DEFINITIONS
; =============================================================================
; Character set (ASCII)
.alias AscCC $03 ; break (Control-C) ASCII character
.alias AscBS $08 ; backspace ASCII character
.alias AscLF $0A ; line feed ASCII character
.alias AscCR $0D ; carriage return ASCII character
.alias AscESC $1B ; Escape ASCII character
.alias AscSP $20 ; space ASCII character
.alias AscDEL $7F ; DEL ASCII character
; Dictionary flags
.alias IM %10000000 ; Immediate
.alias NC %01000000 ; Native Compile
.alias CO %00100000 ; Compile Only
; =============================================================================
; HARDWARE MAP
; =============================================================================
; -----------------------------------------------------------------------------
; RAM
; Assumes continuous RAM memory present from $0000 on. Minimum amount of
; RAM required is TODO kb
.alias RamStart $0000 ; must include Zero Page and Stack
.alias RamSize $9FFF ; default $A000 for 40 kb x 8 bit RAM
.alias RamEnd [RamStart + RamSize]
.alias PadOffset $FF ; Distance between Pad area and CP
;==============================================================================
; MEMORY MAP
;==============================================================================
; -----------------------------------------------------------------------------
; ZERO PAGE
; The zero page acts as the Parameter (Data) Stack. It starts at $7F and grows
; downward towards $00 (128 bytes --> 64 cells) which makes over- and underlow
; easier to detect (highest bit must be 0 for a valid entry). Growing downward
; makes stack manipulations easier to understand, as entries are accessed
; by adding to the stack pointer (X register) instead of subtracting.
.alias SPMAX $00 ; top of parameter (data) stack
.alias SP0 $7F ; bottom of parameter (data) stack
; The top 16 bytes of the zero page ($F0 to $FF) and the 16 bytes above
; the stack ($80 to $8F) are left unused as a "flood plain" in case of
; stack over- or underflow as these conditions are only tested for after
; the fact.
; -----------------------------------------------------------------------------
; FORTH REGISTERS, POINTERS, AND FLAGS
; Forth registers and pointers start on the zero page at $90. IP and WRDWRK
; could probably be replaced by clever use of TMPADR, but this way we make
; sure that they won't be overwritten
.alias DP $90 ; Dictionary Pointer (last entry; 2 bytes)
.alias CP $92 ; Compiler Pointer (next free RAM byte; 2 bytes)
; $94 ; UNUSED TODO close this gap
.alias STATE $95 ; Compile state flag, TRUE is compile (2 bytes)
.alias BASE $97 ; Number base, default decimal ($0A) (1 byte)
.alias CIBA $98 ; Address of the Current Input Buffer (CIB) (2 bytes)
.alias CIBN $9A ; Number of chars in the Current Input Buffer (2 bytes)
.alias INP $9C ; Input Buffer Pointer (>IN; 2 bytes)
.alias OUTPORT $9E ; Output port (default 0, 2 bytes)
.alias INPORT $A0 ; Input port (default 0, 2 bytes)
.alias TBLLOC $A2 ; Location of table currently being used (2 bytes)
.alias IP $A4 ; Instruction Pointer, current xt (2 bytes)
.alias WRKWRD $A6 ; WORKWORD: xt (link) of word being compiled (2 bytes)
.alias TMPADR $A8 ; Temporary storage for addresses (2 bytes)
.alias TMPADR1 $AA ; Temporary storage for more addresses (2 bytes)
.alias TMPADR2 $AC ; Temporary storage for even more addresses (2 bytes)
.alias TMPCNT $AE ; Temporary storage for counters (2 bytes)
.alias TMPX $B0 ; Temporary storage for X Register (1 byte)
.alias FLAG $B1 ; Generic flag (1 byte)
.alias FLAG2 $B2 ; Generic flag (1 byte)
.alias OUTP $B3 ; Output pointer for formated output (2 bytes)
; The zero page entries $D0 to $EF are reserved for the kernel and are
; defined in Tali-Kernel.asm
; -----------------------------------------------------------------------------
; RETURN STACK
; Tali Forth uses the 65c02 stack as the Return Stack, from $100 to $1ff. The
; CPU stack pointer is used as the Return Stack Pointer
.alias RP0 $FF ; bottom of return stack (pointer value)
; -----------------------------------------------------------------------------
; OTHER RAM AREAS
; The Dictionary Pointer (DP) should start off pointing to WORDS: If there is
; a break in the dictionary's link structure, this makes it easier to find it.
; Note that the last entry in the dictionary should always be BYE for the
; same reason
.alias TIB $2600 ; Terminal Input Buffer, $400 to $4FF
.alias SYSPAD $2700 ; System scratch pad
.alias CP0 $2800 ; Start of free RAM (Compiler Pointer)
.alias DP0 l_words ; First entry in the dictionary at start
; -----------------------------------------------------------------------------
; CONSTANTS
.alias PADSIZE $FF ; Size of the PAD area
.alias TIBSIZE $00FE ; Size of the Terminal Input Buffer
; =============================================================================
; INITILIZE SYSTEM (COLD BOOT)
; =============================================================================
; IRQ, BRK and Cold Boot commands end up here
k_irqv:
COLD: ; Load default values to registers and pointers
; TODO move this to a table once we know what we are doing
; Initialize Dictionary Pointer (DP)
lda #<DP0
sta DP
lda #>DP0
sta DP+1
; Compile Pointer (CP): First available space in RAM
lda #<CP0
sta CP
lda #>CP0
sta CP+1
; Initialize number base to ten (DECIMAL)
lda #$0A
sta BASE
; Set input and output ports to zero. We don't have an error port in this
; version
stz OUTPORT
stz OUTPORT+1
stz INPORT
stz INPORT+1
; We start out with the Terminal Input Buffer (TIB) as the
; Current Input Buffer (CIB) ...
; TODO see if we really need this, since it is repeated below
lda #<TIB ; LSB
sta CIBA
lda #>TIB ; MSB
sta CIBA+1
; ... and declare that buffer to be empty
stz CIBN
stz CIBN+1
; Reset stack pointer
ldx #SP0
; -----------------------------------------------------------------------------
; COMPILE HIGH-LEVEL COMMANDS
; We use Tali Forth to compile even more Tali Forth. Putting these high-level
; commands in strings and compiling them during system boot is a quick way to
; get a working system, and makes it easier to later include new commands
; in the dictionary.
.scope
ldy #$00
_loop: ; make room on the Data Stack
dex
dex
; put address on Zero Page for easy handling. We end up pointing to the next
; entry in the table. All entries are counted strings
lda fhltbl,y
sta 1,x ; LSB
iny
lda fhltbl,y
sta 2,x ; MSB
iny
; a value of 0000 means we've completed the table and can quit
ora 1,x ; MSB is still in A
beq _done
; we store the high-level instructions as counted strings for ease
; of handling. Use COUNT to convert them to the modern (addr u)
; format
jsr l_count
; Top of stack now points to string of command, so EVALUATE. Note
; we currently don't support the ANS Forth word SOURCE-ID; if we
; ever do, it will need to be handled here
phy
jsr l_eval
ply
bra _loop
_done:
.scend
; -----------------------------------------------------------------------------
; We're ready to go. Print intro strings. If this gets any longer, we might
; want to convert this to a loop and table structure
jsr l_cr
lda #$00 ; title
jsr f_prtzerostr
lda #$01 ; version
jsr f_prtzerostr
lda #$02 ; disclaimer
jsr f_prtzerostr
lda #$03 ; mini-instructions
jsr f_prtzerostr
jsr l_cr
; =============================================================================
; START MAIN LOOP
; =============================================================================
; Note we can't call a subroutine here because ABORT goes directly to QUIT
; and QUIT resets the Return Stack
jmp l_abort
; Whimsical style note: Indentation of main code increased past this level
; to show we're passed the boot sequence
; =============================================================================
; HELPER ROUTINES
; =============================================================================
; Internal helper routines, not to be accessed by outside. These start with
; "f_"
; -----------------------------------------------------------------------------
; CONVERT LOWER CASE ASCII LETTER TO UPPER CASE
; Takes char in A and converts any letters to upper case. If not a letter or
; already upper case, leave original value. Called by TOUPPER
.scope
f_toupper:
cmp #'a
bcc _done ; not lower case, too low
cmp #'z+1
bcs _done ; not lower case, too high
adc #$e0 ; offset to upper case (wraps)
_done: rts ; we're good (finally)
.scend
; -----------------------------------------------------------------------------
; CONVERT STRING FROM LOWER CASE TO UPPER
; Assumes that string is on stack as ( addr u ) and converts it in place.
; Calls f_toupper, destroys A, Y and changes TMPCNT, TMPADR and TMPADR1
.scope
f_strtoupper: lda 3,x ; LSB of addr
sta TMPADR
lda 4,x ; MSB of addr
sta TMPADR+1
ldy 1,x ; LSB of u, we ignore MSB
dey ; adjust length
_loop: lda (TMPADR),y
jsr f_toupper
sta (TMPADR),y
dey
bpl _loop
rts
.scend
; -----------------------------------------------------------------------------
; COMPILE SUBROUTINE JUMP / JUMP / WORD
; Routines to compile instructions such as "jsr l_words" or "jmp l_words" into a
; word that is created by another Forth word. Use either for subroutine jumps
;
; jsr f_cmpljsr
; .word <addr>
;
; which includes the opcode for JSR, or
;
; jsr f_cmpljmp
; .word <addr>
;
; which includes the opcode for JMP, or
;
; jsr f_cmplword
; .word <word>
;
; which simply adds the word in little-endian format. Used in various places. Note
; this trades memory savings for a 12 clock cycle overhead because of JSR/RTS.
; Note this uses FLAG. Note this may not be used for words that require native
; compile
.scope
f_cmplword: lda #$00 ; just compile word in little-endian
bra _common
f_cmpljsr: lda #$20 ; compile "JSR" opcode
bra _common
f_cmpljmp: lda #$4C ; compile "JMP" opcode; falls through to _common
; opcode doubles as a non-zero flag
_common: sta FLAG
; pull address/word off of stack, increase by one because of
; the way the 65c02 handles subroutines
ply ; LSB of address
pla ; MSB of address
iny
bne +
inc
* sty TMPADR1 ; LSB
sta TMPADR1+1 ; MSB
ldy #$00
; see if we're just adding a word
lda FLAG
beq _wordonly
; this is either f_cmpljsr or f_cmpljmp, so we use the opcode
; that doubled as the flag and save that first
sta (CP),y
iny
; continue with common part: compile word
_wordonly: lda (TMPADR1) ; LSB
sta (CP),y
iny
inc TMPADR1
bne +
inc TMPADR+1
* lda (TMPADR1) ; MSB
sta (CP),y
iny
; save new CP
tya
clc
adc CP
sta CP
bcc +
inc CP+1
; restore the correct return address. We have already added
; two to the return address, so we just need to push it on
; the stack
* lda TMPADR1+1 ; MSB
pha
lda TMPADR1 ; LSB
pha
rts
.scend
; -----------------------------------------------------------------------------
; COMPILE/EXECUTE MAIN ROUTINE
; This is the core routine called by EVALUTE and QUIT. We process one line
; only. Uses Y and TMPCNT
.scope
f_compexe:
_parseword: ; PARSE-NAME ("text" -- addr u)
jsr l_prsnm
; If PARSE-NAME returns zero on top of the stack, it means
; that no characters were left in the line and we need a
; new line
lda 1,x
sta TMPCNT+1 ; save a copy of u for number check
ora 2,x
bne +
jmp _doneline
; Though we let the user input words in upper or lower case,
; we only work with upper case internally, so we convert
; the string now. We have (addr u) on the stack.
* jsr f_strtoupper
; PARSE-NAME returned the word it found as (addr n), while
; FIND wants a counted string (cs-addr). We jump to a
; special entry point in the FIND routine to avoid having
; to convert things ("FIND internal", l_findint)
; see if the word the stack points to is in the dictionary,
; returns zero if not
jsr l_findint
lda 1,x ; we only need to check LSB for flag
bne _found
; Attempt to convert number. FIND has returned (addr 0),
; we need to send (addr u) to NUMBER. Good thing we saved
; n to TMPCNT+1
lda TMPCNT+1
sta 1,x
stz 2,x ; paranoid
jsr l_number ; returns (n -1 | d -1 | addr 0 )
; if there was a failure, it isn't a number either, so
; complain and abort
lda 1,x
beq _parseerror
; it's a legit number, so drop flag off stack
inx
inx ; now (n | d)
; did we get the number in compile or interpret mode?
lda STATE
ora STATE+1
beq _parseword ; interpret, so we're done. Get next word
; TODO We only handle single-cell numbers correctly here
; Impletement 2LITERAL and (2LITERAL) and change the
; flags from NUMBER so we can distinguish between single
; and double numbers
; Compile the number as a literal
jsr f_cmpljsr
.word l_plit
; compile our number
jsr l_comma
; we're done, get next word
bra _parseword
_parseerror: ; Word not found and it isn't a number, so complain
; and abort. We land here with (addr 0) and u in TMPCNT
; print offending word for easier diagnostics
lda TMPCNT
sta 1,x
lda TMPCNT+1
sta 2,x ; paranoid, should be zero anyway
jsr l_type
lda #$0b ; code for syntax error string
jmp error
_found: ; Found word, stack is now (xt f). Save the xt that was
; returned before we do anything else.
lda 3,x ; LSB
sta IP
lda 4,x ; MSB
sta IP+1
; Compile or interpret?
lda STATE
ora STATE+1
bne _compile
; Interpret. But make sure this is not a compile-only word
; by checking the flag in bit 5 of the Length Byte.
ldy #$02
lda (IP),y
and #%00100000
beq _execute
lda #$0c ; code for "compile-only word" error string
jmp error
_execute: ; We already have saved the xt and we don't care about the
; flag in interpret mode, so we dump both
inx
inx
inx
inx
; Only JMP has the addressing mode we need, and all our
; Forth commands end with a RTS instruction. We fake the
; return address by pushing the correct address to the
; 65c02's stack and then doing a normal JMP. When we return,
; we land on a NOP so we don't have to DEC the return address
lda #>_doneexec ; push MSB first
pha
lda #<_doneexec
pha
jmp (IP)
_doneexec: ; Keep the NOP here as the landing site for the indirect
; subroutine jump (easier and quicker than adjusting the
; return address on the 65c02's stack)
nop
; Check for stack over- or underflow. Note that this check
; happens after the fact so we keep "floodplain"
; bytes on both sides of the stack (see memory map)
txa
bpl +
lda #$07 ; code for stack error
jmp error
; We're good, get next word
* jmp _parseword
_compile: ; Compile. First, see if the Precedence bit is set. If yes,
; it's an immediate word and we execute it even though
; we're in compile mode.
lda 1,x
dec ; is A = $01?
beq _execute
; Call COMPILE, and let it do the hard work. First though
; drop the flag, leaving just the xt on the stack
inx
inx
jsr l_cmpc
; That's quite enough of this word, let's get the next one
jmp _parseword
; we're all done on this line
_doneline: rts
.scend
; -----------------------------------------------------------------------------
; Convert byte in A to two ASCII hex digits and print them via f_putchr, usually
; to the screen. Calls f_nib2asc, which does all the real work
.scope
f_byte2hexasc:
pha ; save copy of A
; convert hi nibble
lsr
lsr
lsr
lsr
jsr f_nib2asc
; convert lo nibble
pla
pha ; we want to return original A
jsr f_nib2asc
pla
rts
.scend
; -----------------------------------------------------------------------------
; Convert lower nibble of A to ASCII hex number equivalent and print it via
; f_putchr. Called by f_byte2hexasc
.scope
f_nib2asc:
and #$0F
ora #'0
cmp #'9+1
bcc +
adc #$06
* jmp f_putchr ; JSR/RTS
.scend
; -----------------------------------------------------------------------------
; OUTPUT CHARACTER TO CURRENT PORT. This is a general routine used by
; EMIT, TYPE, SPACE and others. Assumes that character to print is in A.
; If this gets any larger than three channels, consider making this a table.
; TODO test VIA routines
.scope
f_putchr:
jmp k_wrtchr ; JSR/RTS
.scend
; -----------------------------------------------------------------------------
; PRINT ZERO-TERMINATED STRING TO CURRENT PORT. Used internally to print
; strings, as zero-terminated is easier to work with on the 65c02 than
; counted strings. Accepts number of string in A.
.scope
f_wrtzerostr: ; version without a final linefeed
ldy #$00
bra _common
f_prtzerostr: ; version with a final linefeed
ldy #$FF
_common: phy
asl
tay
lda strtbl,y
sta TBLLOC
iny
lda strtbl,y
sta TBLLOC+1
ldy #$00
* lda (TBLLOC),y
beq _linefeed
jsr f_putchr
iny
bra -
_linefeed: ; get flag to see if we print a final linefeed or not
ply
beq _done
lda #AscCR
jsr f_putchr
lda #AscLF
jsr f_putchr
_done: rts
.scend
; -----------------------------------------------------------------------------
; INPUT CHARACTER FROM CURRENT PORT. This is a general routine used by
; KEY, ACCEPT and others. Returns the character in A.
; If this gets any larger than three ports , consider making this a table.
; TODO test VIA routines
.scope
f_getchr:
jmp k_getchr ; JSR/RTS
.scend
; -----------------------------------------------------------------------------
; COMPARE TOS/NOS and return results in form of the 65c02 flags
; Adapted from Lance A. Leventhal "6502 Assembly Language Subroutines".
; For signed numbers, Z signals equality and N which number is larger:
; if TOS = NOS: Z=1 and N=0
; if TOS > NOS: Z=0 and N=0
; if TOS < NOS: Z=0 and N=1
; For unsigned numbers, Z signals equality and C which number is larger:
; if TOS = NOS: Z=1 and N=0
; if TOS > NOS: Z=0 and C=1
; if TOS < NOS: Z=0 and C=0
; Compared to the book routine, WORD1 (MINUED) is TOS
; WORD2 (SUBTRAHEND) is NOS
.scope
f_cmp16: ; compare LSB. We do this first to set the Carry Flag
lda 1,x ; LSB of TOS
cmp 3,x ; LSB of NOS
beq _equal
; low bytes are not equal, compare MSB
lda 2,x ; MSB of TOS
sbc 4,x ; MSB of NOS
ora #$01 ; Make Zero Flag 0 because we're not equal
bvs _overflow
bra _notequal
_equal: ; low bytes are equal, so we compare high bytes
lda 2,x ; MSB of TOS
sbc 4,x ; MSB of NOS
bvc _done
_overflow: ; handle overflow because we use signed numbers
eor #$80 ; complement negative flag
_notequal: ora #$01 ; if overflow, we can't be equal
_done: rts
.scend
; =============================================================================
; CODE FIELD ROUTINES
; =============================================================================
; Code field routines start with fc_
; -----------------------------------------------------------------------------
; DOCONST Execute a constant: Push the data in the first two bytes of the
; Data Field onto the stack.
.scope
fc_docon: ; value is stored in the two bytes after the JSR
; return address
pla ; LSB of return address
sta TMPADR2
pla ; MSB of return address
sta TMPADR2+1
; make room on stack and save the value there
dex
dex
; start LDY off with one instead of zero because of how JSR
; stores the address for RTS
ldy #$01
lda (TMPADR2),y ; LSB
sta 1,x
iny
lda (TMPADR2),y ; MSB
sta 2,x
; the RTS takes us back to the original caller
rts
.scend
; -----------------------------------------------------------------------------
; DOVAR Execute a variable: Push the address of the first bytes of the
; Data Field on the stack. This is called with JSR instead of JMP, so we can
; pick the address of the calling variable off the 65c02's stack. The final
; RTS takes us to the original caller.
.scope
fc_dovar: ; pull return address off of the machine's stack, adding one
; because of the way the 65c02 handles subroutines
ply ; LSB
pla ; MSB
iny
bne +
inc
* sty TMPADR2 ; LSB
sta TMPADR2+1 ; MSB
; get variable and push it on the stack
dex
dex
lda TMPADR2 ; LSB
sta 1,x
lda TMPADR2+1 ; MSB
sta 2,x
rts
.scend
; -----------------------------------------------------------------------------
; DODOES Runtime part for DOES>, installed by DOES> and used in combination
; with (DOES). See http://www.bradrodriguez.com/papers/moving3.htm
; for details on how this works.
.scope
fc_dodoes: ; Assumes the address of the CFA of the original defining word
; is on the top of the stack (for instance, CONSTANT). Save
; it for a jump later. We have to add one byte because of the
; way that the 65c02 handles subroutines
ply ; LSB
pla ; MSB
iny
bne +
inc
* sty TMPADR2 ; LSB
sta TMPADR2+1 ; MSB
; Next on the stack should be the address of the PFA of
; the calling defined word, say the name of the constant
; we just defined. Push this on the Data Stack.
dex
dex
pla
clc
adc #$01 ; add one because of JSR convention
sta 1,x
pla
adc #$00 ; we only care about the carry
sta 2,x
; Left on the stack is the return address from the original
; "main" routine. We leave that untouched, and JMP to the
; code fragment of the defining word
jmp (TMPADR2)
.scend
; =============================================================================
; ERROR ROUTINE
; =============================================================================
; PRINT ERROR STRING. Expect the error code (actually just the string number)
; in A. The jump to ABORT takes us back to the interpreter loop.
.scope
error: ; print a space
pha
lda #AscSP
jsr f_putchr
; print generic error string
lda #$06 ; code for ">>>Error<<<" string
jsr f_prtzerostr
; print specific error string, terminated by a line feed
pla
jsr f_prtzerostr
jmp l_abort
.scend
; =============================================================================
; DICTIONARY
; =============================================================================
; -----------------------------------------------------------------------------
; BYE ( -- )
; BRK is for systems that have a monitor; use WAI or STP for standalone
; systems. With an emulator, you can check if everything went well because
; X should be $7F after this instruction closes the program.
; ** THIS IS ALWAYS THE FINAL ENTRY IN THE DICTIONARY **
l_bye: bra a_bye
.byte NC+$03
.word $0000 ; no more links, end of dictionary
.word z_bye
.byte "BYE"
a_bye: brk
z_bye: rts ; never reached, require for native compile
; ----------------------------------------------------------------------------
; COLD ( -- )
; Reboot the Forth system
l_cold: bra a_cold
.byte $04
.word l_bye ; link to BYE
.word z_cold
.byte "COLD"
.scope
a_cold: jmp COLD
z_cold: rts
.scend
; -----------------------------------------------------------------------------
; (LITERAL) ( -- x )
; Run-time routine for LITERAL: Push value in the two bytes immediately
; following this word on the stack. This is a compile-only word and must
; be called by JSR (no native compiling). Note we can't replace this by
; f_cmplword because we put things on the stack, not compile them
l_plit: bra a_plit
.byte CO+$09
.word l_cold ; link to COLD
.word z_plit
.byte "(LITERAL)"
.scope
a_plit: ; make room on stack
dex
dex
; get the value after the command
ply ; LSB
pla ; MSB
iny
bne +
inc
* sty TMPADR ; LSB
sta TMPADR+1
; get bytes after JSR address
lda (TMPADR) ; LSB
sta 1,x
inc TMPADR
bne +
inc TMPADR+1
* lda (TMPADR) ; LSB
sta 2,x
; replace the new address on the stack
lda TMPADR+1
pha
lda TMPADR
pha
z_plit: rts
.scend
; -----------------------------------------------------------------------------
; LITERAL ( n -- )
; During compilation, add number to compiled word so that (LITERAL) is called
; during execution. This is an immediate, compile-only word
l_lit: bra a_lit
.byte IM+CO+$07
.word l_plit ; link to (LITERAL)
.word z_lit
.byte "LITERAL"
.scope
a_lit: ldy #$00
; compile the call to (LITERAL)
jsr f_cmpljsr
.word l_plit
; now store the number provided on the stack
jsr l_comma
z_lit: rts
.scend
; -----------------------------------------------------------------------------
; ABORT ( -- )
; Reset the parameter (data) stack pointer and continue as QUIT
; Note we can jump here via subroutine because we reset the stack pointer anyway
l_abort: bra a_abort
.byte $05
.word l_lit ; link to LITERAL
.word z_abort
.byte "ABORT"
a_abort: ldx #SP0 ; Reset stack pointer
; Set output and input to default (zero)
stz OUTPORT
stz OUTPORT+1
stz INPORT
stz INPORT+1
z_abort: bra l_quit ; ABORT always flows into quit
; -----------------------------------------------------------------------------
; QUIT ( -- )
; Endless interpreter loop. Resets the return stack so it can't be accessed by
; subroutine calls without trickery.
; TODO see if this will compile okay
l_quit: bra a_quit
.byte $04
.word l_abort ; link to ABORT
.word z_quit
.byte "QUIT"
.scope
a_quit: ; Reset the return stack (65c02 stack) pointer
txa
ldx #RP0
txs
tax
; Default input buffer is the Terminal Input Buffer
lda #<TIB ; LSB
sta CIBA
lda #>TIB ; MSB
sta CIBA+1
; Set number of chars in the Current Input Buffer to zero
stz CIBN
stz CIBN+1 ; paranoid, always zero
; Reset interpreter state to interpret (not: compile). Note
; that state is defined as part of the ANS Forth Core standard
; to be one cell large. Anything non-zero is compiling
stz STATE
stz STATE+1 ; paranoid, always zero
_getline: ; Get one line of input from the user. Keelah se'lai!
; Get address of Terminal Input Buffer (TIB)
jsr l_source
; max number of characters to get. Overwrites the number
; of chars in buffer that SOURCE just returned
lda #<TIBSIZE
sta 1,x
lda #>TIBSIZE
sta 2,x
; Get input line, print a space at the end as an offset
; to what the user gave us
jsr l_accept
; ACCEPT returns the number of characters given, which we
; put in CIBN. Note we only accept up to $FF chars, so
; CIBN+1 is a dummy value, included in case we later want to
; expand this to 16 bit
lda 1,x
sta CIBN
stz CIBN+1 ; paranoid, always zero
inx ; drop return value
inx
; Reset pointer (>IN)
stz INP
stz INP+1 ; paranoid, always zero
; compile or execute
jsr f_compexe
; Completed one line. If we're still compiling, print
; "compiled", else print "ok"
lda STATE
ora STATE+1
beq _prtok
; Tell the user we've added the word to the definition
; (replaces the "ok" prompt)
lda #$05
jsr f_prtzerostr
bra _clrstack
_prtok: ; If we're done with the line, print "ok" (lower case)
lda #$04
jsr f_prtzerostr ; drops through
_clrstack: ; drop the address and number of characters that PARSE-NAME
; always returns
inx
inx
inx
inx
; Get the next line. This is an endless loop. We have to use
; JMP instead of BRA because the distance is too great
jmp _getline
z_quit: rts ; never reached; required for native compile
.scend
; ----------------------------------------------------------------------------
; ABORTQ ( "message" -- ) ("ABORT"")
; If TOP is TRUE, print a message and abort. This is a compile-only word
; One way to create this in Forth is CamelForth's
; : ?ABORT ( f addr u ) ROT IF TYPE ABORT THEN 2DROP ;
; : ABORT" ( "msg" -- ) [COMPILE] S" ['] ?ABORT COMPILE, ;
l_abortq: bra a_abortq
.byte CO+IM+$06
.word l_quit ; link to QUIT
.word z_abortq