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