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