54b2a5dfd8c21cf4e7de8ef30c7470ab5c3ecac0
[rrq/jonasforth.git] / main.asm
1 ;; vim: syntax=fasm
2
3 ;; At compile-time we load the module given by the environment variable
4 ;; OS_INCLUDE. All of the following these procedures should preserve the value
5 ;; of RSI and RSP. They may use other registers as they like.
6 ;;
7 ;; The module should provide the following:
8 ;;
9 ;; os_code_section
10 ;;   Macro to start the text segment.
11 ;;
12 ;; os_data_section
13 ;;   Macro to start the data segment.
14 ;;
15 ;; os_initialize
16 ;;   Called at initialization.
17 ;;
18 ;; os_print_string
19 ;;   Takes a string buffer in RCX and the length in RDX, and prints the string
20 ;;   to the console.
21 ;;
22 ;; os_read_char
23 ;;   Wait for the user to type a key, and then put the corresponding ASCII byte
24 ;;   into RAX.
25 ;;
26 ;; os_terminate
27 ;;   Shut down the system, returning the error code given in RAX.
28 include '%OS_INCLUDE%'
29
30 ;; The code in this macro is placed at the end of each Forth word. When we are
31 ;; executing a definition, this code is what causes execution to resume at the
32 ;; next word in that definition.
33 macro next {
34   ;; RSI points to the address of the definition of the next word to execute.
35   lodsq                   ; Load value at RSI into RAX and increment RSI
36   ;; Now RAX contains the location of the next word to execute. The first 8
37   ;; bytes of this word is the address of the codeword, which is what we want
38   ;; to execute.
39   jmp qword [rax]         ; Jump to the codeword of the current word
40 }
41
42 ;; pushr and popr work on the return stack, whose location is stored in the
43 ;; register RBP.
44 macro pushr x {
45   sub rbp, 8
46   mov qword [rbp], x
47 }
48 macro popr x {
49   mov x, [rbp]
50   add rbp, 8
51 }
52
53 ;; The following macro generates the dictionary header. It updates the
54 ;; initial_latest_entry variable, which is used as the initial value of the
55 ;; latest_entry variable that is made available at runtime.
56 ;;
57 ;; The header contains a link to the previous entry, the length of the name of
58 ;; the word and the word itself as a string literal.
59 ;;
60 ;; This macro also defines a label LABEL_entry.
61 initial_latest_entry = 0
62 macro header label, name, immediate {
63   local .string_end
64
65 label#_entry:
66   dq initial_latest_entry
67   if immediate eq
68     db 0
69   else
70     db 1
71   end if
72   db .string_end - ($ + 1)
73   db name
74   .string_end:
75 label:
76
77 initial_latest_entry = label#_entry
78 }
79
80 ;; Define a Forth word that is implemented in assembly. See 'header' for details.
81 macro forth_asm label, name, immediate {
82   header label, name, immediate
83   dq .start
84 .start:
85 }
86
87 include "impl.asm"      ; Misc. subroutines
88 include "bootstrap.asm" ; Forth words encoded in Assembly
89
90 os_code_section
91
92 main:
93   cld                        ; Clear direction flag so LODSQ does the right thing.
94   mov rbp, return_stack_top  ; Initialize return stack
95
96   call os_initialize
97
98   mov rax, MAIN
99   jmp qword [rax]
100
101 program: dq MAIN
102
103 ;; The codeword is the code that will be executed at the beginning of a forth
104 ;; word. It needs to save the old RSI and update it to point to the next word to
105 ;; execute.
106 header DOCOL, 'DOCOL'
107   pushr rsi            ; Save old value of RSI on return stack; we will continue execution there after we are done executing this word
108   lea rsi, [rax + 8]   ; RAX currently points to the address of the codeword, so we want to continue at RAX+8
109   next                 ; Execute word pointed to by RSI
110
111 ;; This word is called at the end of a Forth definition. It just needs to
112 ;; restore the old value of RSI (saved by 'DOCOL') and resume execution.
113 forth_asm EXIT, 'EXIT'
114   popr rsi
115   next
116
117 ;; LIT is a special word that reads the next "word pointer" and causes it to be
118 ;; placed on the stack rather than executed.
119 forth_asm LIT, 'LIT'
120   lodsq
121   push rax
122   next
123
124 ;; When LITSTRING is encountered while executing a word, it instead reads a
125 ;; string from the definition of that word, and places that string on the stack
126 ;; as (buffer, length).
127 forth_asm LITSTRING, 'LITSTRING'
128   lodsb
129   push rsi ; Buffer
130   movzx rax, al
131   push rax ; Length
132   add rsi, rax ; Skip over string before resuming execution
133   next
134
135 ;; Given a string (a pointer following by a size), return the location of the
136 ;; dictionary entry for that word. If no such word exists, return 0.
137 forth_asm FIND, 'FIND'
138   mov [.rsi], rsi
139
140   pop [find.search_length]
141   pop [find.search_buffer]
142   mov rsi, [latest_entry]       ; Start with the last added word
143   call find
144   push rsi
145
146   mov rsi, [.rsi]
147   next
148   push rsi
149
150   mov rsi, [.rsi]
151   next
152
153 ;; Given an entry in the dictionary, return a pointer to the codeword of that
154 ;; entry.
155 forth_asm TCFA, '>CFA'
156   pop rax
157   add rax, 8 + 1                ; [rax] = length of name
158   movzx rbx, byte [rax]
159   inc rax
160   add rax, rbx                  ; [rax] = codeword
161   push rax
162   next
163
164 ;; BRANCH is the fundamental mechanism for branching. BRANCH reads the next word
165 ;; as a signed integer literal and jumps by that offset.
166 forth_asm BRANCH, 'BRANCH'
167   add rsi, [rsi] ; [RSI], which is the next word, contains the offset; we add this to the instruction pointer.
168   next           ; Then, we can just continue execution as normal
169
170 ;; 0BRANCH is like BRANCH, but it jumps only if the top of the stack is zero.
171 forth_asm ZBRANCH, '0BRANCH'
172   ;; Compare top of stack to see if we should branch
173   pop rax
174   cmp rax, 0
175   jnz .dont_branch
176 .do_branch:
177   jmp BRANCH.start
178 .dont_branch:
179   add rsi, 8     ; We need to skip over the next word, which contains the offset.
180   next
181
182 ;; Duplicate the top of the stack.
183 forth_asm DUP_, 'DUP'
184   push qword [rsp]
185   next
186
187 ;; Execute the codeword at the given address.
188 forth_asm EXEC, 'EXEC'
189   pop rax
190   jmp qword [rax]
191
192 ;; Expects a character on the stack and prints it to standard output.
193 forth_asm EMIT, 'EMIT'
194   pushr rsi
195   pushr rax
196
197   lea rcx, [rsp]
198   mov rdx, 1
199   call os_print_string
200
201   add rsp, 8
202   popr rax
203   popr rsi
204   next
205
206 ;; Read a single character from the current input stream. Usually, this will wait
207 ;; for the user to press a key, and then return the corresponding character. When
208 ;; reading from a special buffer, it will instead return the next characater from
209 ;; that buffer.
210 ;;
211 ;; The ASCII character code is placed on the stack.
212 forth_asm KEY, 'KEY'
213   call .impl
214   push rax
215   next
216
217 ;; Result in RAX
218 .impl:
219   ;; Are we reading from user input or from the input buffer?
220   cmp [input_buffer], 0
221   jne .from_buffer
222
223   ;; Reading user input
224   call os_read_char
225   ret
226
227 .from_buffer:
228   ;; Reading from buffer
229   mov rax, [input_buffer]
230   movzx rax, byte [rax]
231
232   inc [input_buffer]
233   dec [input_buffer_length]
234   ret
235
236 ;; Read a word and push it onto the stack as a pointer and a size. The pointer
237 ;; is valid until the next call to READ_WORD.
238 forth_asm READ_WORD, 'READ-WORD'
239   push rsi
240 .skip_whitespace:
241   ;; Read characters until one of them is not whitespace.
242   call KEY.impl
243   ;; We consider newlines and spaces to be whitespace.
244   cmp al, ' '
245   je .skip_whitespace
246   cmp al, $A
247   je .skip_whitespace
248
249   ;; We got a character that wasn't whitespace. Now read the actual word.
250   mov [.length], 0
251
252 .read_alpha:
253   movzx rbx, [.length]
254   mov rsi, .buffer
255   add rsi, rbx
256   mov [rsi], al
257   inc [.length]
258
259   call KEY.impl
260
261   cmp al, ' '
262   je .end
263   cmp al, $A
264   jne .read_alpha
265
266 .end:
267   pop rsi
268   push .buffer
269   movzx rax, [.length]
270   push rax
271
272   next
273
274 ;; Takes a string on the stack and replaces it with the decimal number that the
275 ;; string represents.
276 forth_asm PARSE_NUMBER, 'PARSE-NUMBER'
277   pop rcx     ; Length
278   pop rdi     ; String pointer
279
280   push rsi
281   call parse_number
282   pop rsi
283
284   push rax                      ; Result
285   next
286
287 ;; Takes a string (in the form of a pointer and a length on the stack) and
288 ;; prints it to standard output.
289 forth_asm TELL, 'TELL'
290   pushr rax
291   pushr rsi
292
293   pop rdx ; Length
294   pop rcx ; Buffer
295   call os_print_string
296
297   popr rsi
298   popr rax
299   next
300
301 ;; Exit the program cleanly.
302 forth_asm TERMINATE, 'TERMINATE'
303   mov rax, 0
304   call os_terminate
305
306 ;; Duplicate a pair of elements.
307 forth_asm PAIRDUP, '2DUP'
308   pop rbx
309   pop rax
310   push rax
311   push rbx
312   push rax
313   push rbx
314   next
315
316 ;; Swap the top two elements on the stack.
317 forth_asm SWAP, 'SWAP'
318   pop rax
319   pop rbx
320   push rax
321   push rbx
322   next
323
324 ;; Remove the top element from the stack.
325 forth_asm DROP, 'DROP'
326   add rsp, 8
327   next
328
329 forth_asm NOT_, 'NOT'
330   pop rax
331   cmp rax, 0
332   jz .false
333 .true:
334   push 0
335   next
336 .false:
337   push 1
338   next
339
340 ;; .U prints the value on the stack as an unsigned integer in hexadecimal.
341 forth_asm DOTU, '.U'
342   mov [.length], 0
343   mov [.printed_length], 1
344   pop rax                       ; RAX = value to print
345   push rsi                      ; Save value of RSI
346
347   ;; We start by constructing the buffer to print in reverse
348
349 .loop:
350   mov rdx, 0
351   mov rbx, $10
352   div rbx                       ; Put remainer in RDX and quotient in RAX
353
354   ;; Place the appropriate character in the buffer
355   mov rsi, .chars
356   add rsi, rdx
357   mov bl, [rsi]
358   mov rdi, .rbuffer
359   add rdi, [.length]
360   mov [rdi], bl
361   inc [.length]
362
363   ;; .printed_length is the number of characters that we ulitmately want to
364   ;; print. If we have printed a non-zero character, then we should update
365   ;; .printed_length.
366   cmp bl, '0'
367   je .skip_updating_real_length
368   mov rbx, [.length]
369   mov [.printed_length], rbx
370 .skip_updating_real_length:
371
372   cmp [.length], 16
373   jle .loop
374
375   ;; Flip buffer around, since it is currently reversed
376   mov rcx, [.printed_length]
377 .flip:
378   mov rsi, .rbuffer
379   add rsi, rcx
380   dec rsi
381   mov al, [rsi]
382
383   mov rdi, .buffer
384   add rdi, [.printed_length]
385   sub rdi, rcx
386   mov [rdi], al
387
388   loop .flip
389
390   ;; Print the buffer
391   mov rcx, .buffer
392   mov rdx, [.printed_length]
393   call os_print_string
394
395   ;; Restore RSI and continue execution
396   pop rsi
397   next
398
399 ;; Takes a value and an address, and stores the value at the given address.
400 forth_asm PUT, '!'
401   pop rbx                       ; Address
402   pop rax                       ; Value
403   mov [rbx], rax
404   next
405
406 ;; Takes an address and returns the value at the given address.
407 forth_asm GET, '@'
408   pop rax
409   mov rax, [rax]
410   push rax
411   next
412
413 forth_asm PUT_BYTE, 'C!'
414   pop rbx
415   pop rax                       ; Value
416   mov [rbx], al
417   next
418
419 forth_asm GET_BYTE, 'C@'
420   pop rax
421   movzx rax, byte [rax]
422   push rax
423   next
424
425 ;; Add two integers on the stack.
426 forth_asm PLUS, '+'
427   pop rax
428   pop rbx
429   add rax, rbx
430   push rax
431   next
432
433 ;; Calculate difference between two integers on the stack. The second number is
434 ;; subtracted from the first.
435 forth_asm MINUS, '-'
436   pop rax
437   pop rbx
438   sub rbx, rax
439   push rbx
440   next
441
442 ;; Given two integers a and b on the stack, pushes the quotient and remainder of
443 ;; division of a by b.
444 forth_asm TIMESMOD, '/MOD'
445   pop rbx                       ; b
446   pop rax                       ; a
447   mov rdx, 0
448   div rbx
449   push rax                      ; a / b
450   push rdx                      ; a % b
451   next
452
453 ;; Read input until next " character is found. Push a string containing the
454 ;; input on the stack as (buffer length). Note that the buffer is only valid
455 ;; until the next call to S" and that no more than 255 characters can be read.
456 forth_asm READ_STRING, 'S"'
457   ;; If the input buffer is set, we should read from there instead.
458   cmp [input_buffer], 0
459   jne read_string_buffer
460
461   push rsi
462
463   mov [.length], 0
464
465 .read_char:
466   call os_read_char
467   cmp al, '"'
468   je .done
469
470   mov rdx, .buffer
471   add rdx, [.length]
472   mov [rdx], al
473   inc [.length]
474   jmp .read_char
475
476 .done:
477   pop rsi
478
479   push .buffer
480   push [.length]
481
482   next
483
484 read_string_buffer:
485   push rsi
486
487   ;; We borrow READ_STRING's buffer. They won't mind.
488   mov [READ_STRING.length], 0
489
490 .read_char:
491   mov rbx, [input_buffer]
492   mov al, [rbx]
493   cmp al, '"'
494   je .done
495
496   mov rdx, READ_STRING.buffer
497   add rdx, [READ_STRING.length]
498   mov [rdx], al
499   inc [READ_STRING.length]
500
501   inc [input_buffer]
502   dec [input_buffer_length]
503
504   jmp .read_char
505
506 .done:
507   pop rsi
508
509   ;; Skip closing "
510   inc [input_buffer]
511   dec [input_buffer_length]
512
513   push READ_STRING.buffer
514   push [READ_STRING.length]
515
516   next
517
518 ;; CREATE inserts a new header in the dictionary, and updates LATEST so that it
519 ;; points to the header. To compile a word, the user can then call ',' to
520 ;; continue to append data after the header.
521 ;;
522 ;; It takes the name of the word as a string (address length) on the stack.
523 forth_asm CREATE, 'CREATE'
524   pop rcx                       ; Word string length
525   pop rdx                       ; Word string pointer
526
527   mov rdi, [here]               ; rdi = Address at which to insert this entry
528   mov rax, [latest_entry]       ; rax = Address of the previous entry
529   mov [rdi], rax                ; Insert link to previous entry
530   mov [latest_entry], rdi       ; Update LATEST to point to this word
531
532   add rdi, 8
533   mov [rdi], byte 0             ; Insert immediate flag
534
535   add rdi, 1
536   mov [rdi], byte cl            ; Insert length
537
538   ;; Insert word string
539   add rdi, 1
540
541   push rsi
542   mov rsi, rdx                  ; rsi = Word string pointer
543   rep movsb
544   pop rsi
545
546   ;; Update HERE
547   mov [here], rdi
548
549   next
550
551 forth_asm TICK, "'"
552   lodsq
553   push rax
554   next
555
556 forth_asm ROT, 'ROT'
557   pop rax
558   pop rbx
559   pop rdx
560   push rax
561   push rdx
562   push rbx
563   next
564
565 forth_asm PICK, 'PICK'
566   pop rax
567   lea rax, [rsp + 8 * rax]
568   mov rax, [rax]
569   push rax
570   next
571
572 forth_asm EQL, '='
573   pop rax
574   pop rbx
575   cmp rax, rbx
576   je .eq
577 .noteq:
578   push 0
579   next
580 .eq:
581   push 1
582   next
583
584 forth MAIN, 'MAIN'
585   dq SYSCODE
586   dq INTERPRET_STRING
587   dq INTERPRET
588   dq BRANCH, -8 * 2
589   dq TERMINATE
590
591 ;; EFI:
592
593 forth EFI_SYSTEM_TABLE_CONSTANT, 'SystemTable'
594   dq LIT, system_table, GET
595   dq EXIT
596
597 forth_asm EFICALL2, 'EFICALL2'
598   pop rax ; function pointer
599   pop rdx ; 2nd argument
600   pop rcx ; 1st argument
601
602   push rsi
603
604   sub rsp, 32
605   call rax
606   add rsp, 32
607
608   pop rsi
609
610   next
611
612 ;; Built-in variables:
613
614 forth STATE, 'STATE'
615   dq LIT, var_STATE
616   dq EXIT
617
618 forth LATEST, 'LATEST'
619   dq LIT, latest_entry
620   dq EXIT
621
622 forth HERE, 'HERE'
623   dq LIT, here
624   dq EXIT
625
626 forth SYSCODE, 'SYSCODE'
627   dq LIT, sysf
628   dq LIT, sysf.len
629   dq EXIT
630
631 forth INPUT_BUFFER, 'INPUT-BUFFER'
632   dq LIT, input_buffer
633   dq EXIT
634
635 forth INPUT_LENGTH, 'INPUT-LENGTH'
636   dq LIT, input_buffer_length
637   dq EXIT
638
639 os_data_section
640
641 ;; The LATEST variable holds a pointer to the word that was last added to the
642 ;; dictionary. This pointer is updated as new words are added, and its value is
643 ;; used by FIND to look up words.
644 latest_entry dq initial_latest_entry
645
646 ;; The STATE variable is 0 when the interpreter is executing, and non-zero when
647 ;; it is compiling.
648 var_STATE dq 0
649
650 ;; The interpreter can read either from standard input or from a buffer. When
651 ;; input-buffer is set (non-null), words like READ-WORD and S" will use this
652 ;; buffer instead of reading user input.
653 input_buffer dq 0
654 input_buffer_length dq 0
655
656 FIND.rsi dq ?
657
658 READ_WORD.rsi dq ?
659 READ_WORD.rbp dq ?
660
661 READ_STRING.char_buffer db ?
662 READ_STRING.buffer rb $FF
663 READ_STRING.length dq ?
664
665 DOTU.chars db '0123456789ABCDEF'
666 DOTU.buffer rq 16               ; 64-bit number has no more than 16 digits in hex
667 DOTU.rbuffer rq 16
668 DOTU.length dq ?
669 DOTU.printed_length dq ?
670
671 KEY.buffer dq ?
672
673 READ_WORD.buffer rb $FF
674 READ_WORD.length db ?
675
676 ;; Reserve space for compiled words, accessed through HERE.
677 here dq here_top
678 here_top rq $4000
679
680 ;; Return stack
681 rq $2000
682 return_stack_top:
683
684 ;; We store some Forth code in sys.f that defined common words that the user
685 ;; would expect to have available at startup. To execute these words, we just
686 ;; include the file directly in the binary, and then interpret it at startup.
687 sysf:
688 file 'sys.f'
689 file 'uefi.f'
690 sysf.len = $ - sysf
691