4ed478d0743f7f75d584f2632cc377e454fa98f8
[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 word and push it onto the stack as a pointer and a size. The pointer
235 ;; is valid until the next call to READ_WORD.
236 forth_asm READ_WORD, 'READ-WORD'
237   ;; Are we reading from user input or from the input buffer?
238   cmp [input_buffer], 0
239   jne .from_buffer
240
241   ;; Reading user input
242   mov [.rsi], rsi
243
244   call read_word
245   push rdi                      ; Buffer
246   push rdx                      ; Length
247
248   mov rsi, [.rsi]
249   next
250
251 .from_buffer:
252   ;; Reading from buffer
253   mov [.rsi], rsi
254
255   mov rsi, [input_buffer]
256   mov rcx, [input_buffer_length]
257
258   call pop_word
259
260   mov [input_buffer], rsi        ; Updated buffer
261   mov [input_buffer_length], rcx ; Length of updated buffer
262   push rdi                       ; Word buffer
263   push rdx                       ; Length of word buffer
264
265   mov rsi, [.rsi]
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   sys_print_string
290
291   popr rsi
292   popr rax
293   next
294
295 ;; Exit the program cleanly.
296 forth_asm TERMINATE, 'TERMINATE'
297   sys_terminate 0
298
299 ;; Duplicate a pair of elements.
300 forth_asm PAIRDUP, '2DUP'
301   pop rbx
302   pop rax
303   push rax
304   push rbx
305   push rax
306   push rbx
307   next
308
309 ;; Swap the top two elements on the stack.
310 forth_asm SWAP, 'SWAP'
311   pop rax
312   pop rbx
313   push rax
314   push rbx
315   next
316
317 ;; Remove the top element from the stack.
318 forth_asm DROP, 'DROP'
319   add rsp, 8
320   next
321
322 forth_asm NOT_, 'NOT'
323   pop rax
324   cmp rax, 0
325   jz .false
326 .true:
327   push 0
328   next
329 .false:
330   push 1
331   next
332
333 ;; .U prints the value on the stack as an unsigned integer in hexadecimal.
334 forth_asm DOTU, '.U'
335   mov [.length], 0
336   mov [.printed_length], 1
337   pop rax                       ; RAX = value to print
338   push rsi                      ; Save value of RSI
339
340   ;; We start by constructing the buffer to print in reverse
341
342 .loop:
343   mov rdx, 0
344   mov rbx, $10
345   div rbx                       ; Put remainer in RDX and quotient in RAX
346
347   ;; Place the appropriate character in the buffer
348   mov rsi, .chars
349   add rsi, rdx
350   mov bl, [rsi]
351   mov rdi, .rbuffer
352   add rdi, [.length]
353   mov [rdi], bl
354   inc [.length]
355
356   ;; .printed_length is the number of characters that we ulitmately want to
357   ;; print. If we have printed a non-zero character, then we should update
358   ;; .printed_length.
359   cmp bl, '0'
360   je .skip_updating_real_length
361   mov rbx, [.length]
362   mov [.printed_length], rbx
363 .skip_updating_real_length:
364
365   cmp [.length], 16
366   jle .loop
367
368   ;; Flip buffer around, since it is currently reversed
369   mov rcx, [.printed_length]
370 .flip:
371   mov rsi, .rbuffer
372   add rsi, rcx
373   dec rsi
374   mov al, [rsi]
375
376   mov rdi, .buffer
377   add rdi, [.printed_length]
378   sub rdi, rcx
379   mov [rdi], al
380
381   loop .flip
382
383   ;; Print the buffer
384   mov rcx, .buffer
385   mov rdx, [.printed_length]
386   sys_print_string
387
388   ;; Restore RSI and continue execution
389   pop rsi
390   next
391
392 ;; Takes a value and an address, and stores the value at the given address.
393 forth_asm PUT, '!'
394   pop rbx                       ; Address
395   pop rax                       ; Value
396   mov [rbx], rax
397   next
398
399 ;; Takes an address and returns the value at the given address.
400 forth_asm GET, '@'
401   pop rax
402   mov rax, [rax]
403   push rax
404   next
405
406 forth_asm PUT_BYTE, 'C!'
407   pop rbx
408   pop rax                       ; Value
409   mov [rbx], al
410   next
411
412 forth_asm GET_BYTE, 'C@'
413   pop rax
414   movzx rax, byte [rax]
415   push rax
416   next
417
418 ;; Add two integers on the stack.
419 forth_asm PLUS, '+'
420   pop rax
421   pop rbx
422   add rax, rbx
423   push rax
424   next
425
426 ;; Calculate difference between two integers on the stack. The second number is
427 ;; subtracted from the first.
428 forth_asm MINUS, '-'
429   pop rax
430   pop rbx
431   sub rbx, rax
432   push rbx
433   next
434
435 ;; Given two integers a and b on the stack, pushes the quotient and remainder of
436 ;; division of a by b.
437 forth_asm TIMESMOD, '/MOD'
438   pop rbx                       ; b
439   pop rax                       ; a
440   mov rdx, 0
441   div rbx
442   push rax                      ; a / b
443   push rdx                      ; a % b
444   next
445
446 ;; Read input until next " character is found. Push a string containing the
447 ;; input on the stack as (buffer length). Note that the buffer is only valid
448 ;; until the next call to S" and that no more than 255 characters can be read.
449 forth_asm READ_STRING, 'S"'
450   ;; If the input buffer is set, we should read from there instead.
451   cmp [input_buffer], 0
452   jne read_string_buffer
453
454   push rsi
455
456   mov [.length], 0
457
458 .read_char:
459   mov rsi, .char_buffer
460   sys_read_char
461
462   mov al, [.char_buffer]
463   cmp al, '"'
464   je .done
465
466   mov rdx, .buffer
467   add rdx, [.length]
468   mov [rdx], al
469   inc [.length]
470   jmp .read_char
471
472 .done:
473   pop rsi
474
475   push .buffer
476   push [.length]
477
478   next
479
480 read_string_buffer:
481   push rsi
482
483   ;; We borrow READ_STRING's buffer. They won't mind.
484   mov [READ_STRING.length], 0
485
486 .read_char:
487   mov rbx, [input_buffer]
488   mov al, [rbx]
489   cmp al, '"'
490   je .done
491
492   mov rdx, READ_STRING.buffer
493   add rdx, [READ_STRING.length]
494   mov [rdx], al
495   inc [READ_STRING.length]
496
497   inc [input_buffer]
498   dec [input_buffer_length]
499
500   jmp .read_char
501
502 .done:
503   pop rsi
504
505   ;; Skip closing "
506   inc [input_buffer]
507   dec [input_buffer_length]
508
509   push READ_STRING.buffer
510   push [READ_STRING.length]
511
512   next
513
514 ;; CREATE inserts a new header in the dictionary, and updates LATEST so that it
515 ;; points to the header. To compile a word, the user can then call ',' to
516 ;; continue to append data after the header.
517 ;;
518 ;; It takes the name of the word as a string (address length) on the stack.
519 forth_asm CREATE, 'CREATE'
520   pop rcx                       ; Word string length
521   pop rdx                       ; Word string pointer
522
523   mov rdi, [here]               ; rdi = Address at which to insert this entry
524   mov rax, [latest_entry]       ; rax = Address of the previous entry
525   mov [rdi], rax                ; Insert link to previous entry
526   mov [latest_entry], rdi       ; Update LATEST to point to this word
527
528   add rdi, 8
529   mov [rdi], byte 0             ; Insert immediate flag
530
531   add rdi, 1
532   mov [rdi], byte cl            ; Insert length
533
534   ;; Insert word string
535   add rdi, 1
536
537   push rsi
538   mov rsi, rdx                  ; rsi = Word string pointer
539   rep movsb
540   pop rsi
541
542   ;; Update HERE
543   mov [here], rdi
544
545   next
546
547 forth_asm TICK, "'"
548   lodsq
549   push rax
550   next
551
552 forth_asm ROT, 'ROT'
553   pop rax
554   pop rbx
555   pop rdx
556   push rax
557   push rdx
558   push rbx
559   next
560
561 forth_asm PICK, 'PICK'
562   pop rax
563   lea rax, [rsp + 8 * rax]
564   mov rax, [rax]
565   push rax
566   next
567
568 forth_asm EQL, '='
569   pop rax
570   pop rbx
571   cmp rax, rbx
572   je .eq
573 .noteq:
574   push 0
575   next
576 .eq:
577   push 1
578   next
579
580 forth MAIN, 'MAIN'
581   dq SYSCODE
582   dq INTERPRET_STRING
583   dq INTERPRET
584   dq BRANCH, -8 * 2
585   dq TERMINATE
586
587 ;; Built-in variables:
588
589 forth STATE, 'STATE'
590   dq LIT, var_STATE
591   dq EXIT
592
593 forth LATEST, 'LATEST'
594   dq LIT, latest_entry
595   dq EXIT
596
597 forth HERE, 'HERE'
598   dq LIT, here
599   dq EXIT
600
601 forth SYSCODE, 'SYSCODE'
602   dq LIT, sysf
603   dq LIT, sysf.len
604   dq EXIT
605
606 forth INPUT_BUFFER, 'INPUT-BUFFER'
607   dq LIT, input_buffer
608   dq EXIT
609
610 forth INPUT_LENGTH, 'INPUT-LENGTH'
611   dq LIT, input_buffer_length
612   dq EXIT
613
614 section '.data' readable writable
615
616 ;; The LATEST variable holds a pointer to the word that was last added to the
617 ;; dictionary. This pointer is updated as new words are added, and its value is
618 ;; used by FIND to look up words.
619 latest_entry dq initial_latest_entry
620
621 ;; The STATE variable is 0 when the interpreter is executing, and non-zero when
622 ;; it is compiling.
623 var_STATE dq 0
624
625 ;; The interpreter can read either from standard input or from a buffer. When
626 ;; input-buffer is set (non-null), words like READ-WORD and S" will use this
627 ;; buffer instead of reading user input.
628 input_buffer dq 0
629 input_buffer_length dq 0
630
631 FIND.rsi dq ?
632
633 READ_WORD.rsi dq ?
634 READ_WORD.rbp dq ?
635
636 READ_STRING.char_buffer db ?
637 READ_STRING.buffer rb $FF
638 READ_STRING.length dq ?
639
640 DOTU.chars db '0123456789ABCDEF'
641 DOTU.buffer rq 16               ; 64-bit number has no more than 16 digits in hex
642 DOTU.rbuffer rq 16
643 DOTU.length dq ?
644 DOTU.printed_length dq ?
645
646 ;; Reserve space for compiled words, accessed through HERE.
647 here dq here_top
648 here_top rq $4000
649
650 ;; Return stack
651 rq $2000
652 return_stack_top:
653
654 ;; We store some Forth code in sys.f that defined common words that the user
655 ;; would expect to have available at startup. To execute these words, we just
656 ;; include the file directly in the binary, and then interpret it at startup.
657 sysf file 'sys.f'
658 sysf.len = $ - sysf
659