538039b5ab29b0cd402d26105e3df89d3c5f2424
[rrq/jonasforth.git] / main.asm
1 ;; vim: syntax=fasm
2
3 format ELF64 executable
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   mov rax, 1
26   mov rdi, 1
27   mov rsi, rcx
28   syscall
29 }
30
31 ;; }}}
32
33 ;; The code in this macro is placed at the end of each Forth word. When we are
34 ;; executing a definition, this code is what causes execution to resume at the
35 ;; next word in that definition.
36 macro next {
37   ;; RSI points to the address of the definition of the next word to execute.
38   lodsq                   ; Load value at RSI into RAX and increment RSI
39   ;; Now RAX contains the location of the next word to execute. The first 8
40   ;; bytes of this word is the address of the codeword, which is what we want
41   ;; to execute.
42   jmp qword [rax]         ; Jump to the codeword of the current word
43 }
44
45 ;; pushr and popr work on the return stack, whose location is stored in the
46 ;; register RBP.
47 macro pushr x {
48   sub rbp, 8
49   mov qword [rbp], x
50 }
51 macro popr x {
52   mov x, [rbp]
53   add rbp, 8
54 }
55
56 ;; The following macro generates the dictionary header. It updates the
57 ;; initial_latest_entry variable, which is used as the initial value of the
58 ;; latest_entry variable that is made available at runtime.
59 ;;
60 ;; The header contains a link to the previous entry, the length of the name of
61 ;; the word and the word itself as a string literal.
62 ;;
63 ;; This macro also defines a label LABEL_entry.
64 initial_latest_entry = 0
65 macro header label, name, immediate {
66   local .string_end
67
68 label#_entry:
69   dq initial_latest_entry
70   if immediate eq
71     db 0
72   else
73     db 1
74   end if
75   db .string_end - ($ + 1)
76   db name
77   .string_end:
78 label:
79
80 initial_latest_entry = label#_entry
81 }
82
83 ;; Define a Forth word that is implemented in assembly. See 'header' for details.
84 macro forth_asm label, name, immediate {
85   header label, name, immediate
86   dq .start
87 .start:
88 }
89
90 segment readable executable
91
92 entry main
93
94 include "impl.asm"      ; Misc. subroutines
95 include "bootstrap.asm" ; Forth words encoded in Assembly
96
97 main:
98   cld                        ; Clear direction flag so LODSQ does the right thing.
99   mov rbp, return_stack_top  ; Initialize return stack
100
101   mov rax, MAIN
102   jmp qword [rax]
103
104 program: dq MAIN
105
106 ;; The codeword is the code that will be executed at the beginning of a forth
107 ;; word. It needs to save the old RSI and update it to point to the next word to
108 ;; execute.
109 header DOCOL, 'DOCOL'
110   pushr rsi            ; Save old value of RSI on return stack; we will continue execution there after we are done executing this word
111   lea rsi, [rax + 8]   ; RAX currently points to the address of the codeword, so we want to continue at RAX+8
112   next                 ; Execute word pointed to by RSI
113
114 ;; This word is called at the end of a Forth definition. It just needs to
115 ;; restore the old value of RSI (saved by 'DOCOL') and resume execution.
116 forth_asm EXIT, 'EXIT'
117   popr rsi
118   next
119
120 ;; LIT is a special word that reads the next "word pointer" and causes it to be
121 ;; placed on the stack rather than executed.
122 forth_asm LIT, 'LIT'
123   lodsq
124   push rax
125   next
126
127 ;; Given a string (a pointer following by a size), return the location of the
128 ;; dictionary entry for that word. If no such word exists, return 0.
129 forth_asm FIND, 'FIND'
130   mov [.rsi], rsi
131
132   pop [find.search_length]
133   pop [find.search_buffer]
134   mov rsi, [latest_entry]       ; Start with the last added word
135   call find
136   push rsi
137
138   mov rsi, [.rsi]
139   next
140   push rsi
141
142   mov rsi, [.rsi]
143   next
144
145 ;; Given an entry in the dictionary, return a pointer to the codeword of that
146 ;; entry.
147 forth_asm TCFA, '>CFA'
148   pop rax
149   add rax, 8 + 1                ; [rax] = length of name
150   movzx rbx, byte [rax]
151   inc rax
152   add rax, rbx                  ; [rax] = codeword
153   push rax
154   next
155
156 ;; BRANCH is the fundamental mechanism for branching. BRANCH reads the next word
157 ;; as a signed integer literal and jumps by that offset.
158 forth_asm BRANCH, 'BRANCH'
159   add rsi, [rsi] ; [RSI], which is the next word, contains the offset; we add this to the instruction pointer.
160   next           ; Then, we can just continue execution as normal
161
162 ;; 0BRANCH is like BRANCH, but it jumps only if the top of the stack is zero.
163 forth_asm ZBRANCH, '0BRANCH'
164   ;; Compare top of stack to see if we should branch
165   pop rax
166   cmp rax, 0
167   jnz .dont_branch
168 .do_branch:
169   jmp BRANCH.start
170 .dont_branch:
171   add rsi, 8     ; We need to skip over the next word, which contains the offset.
172   next
173
174 ;; Duplicate the top of the stack.
175 forth_asm DUP_, 'DUP'
176   push qword [rsp]
177   next
178
179 ;; Execute the codeword at the given address.
180 forth_asm EXEC, 'EXEC'
181   pop rax
182   jmp qword [rax]
183
184 ;; Expects a character on the stack and prints it to standard output.
185 forth_asm EMIT, 'EMIT'
186   pushr rsi
187   pushr rax
188
189   lea rcx, [rsp]
190   mov rdx, 1
191   sys_print_string
192
193   add rsp, 8
194   popr rax
195   popr rsi
196   next
197
198 ;; Read a word from standard input and push it onto the stack as a pointer and a
199 ;; size. The pointer is valid until the next call to READ_WORD.
200 forth_asm READ_WORD, 'READ-WORD'
201   mov [.rsi], rsi
202
203   call read_word
204   push rdi                      ; Buffer
205   push rdx                      ; Length
206
207   mov rsi, [.rsi]
208   next
209
210 ;; Takes a string on the stack and replaces it with the decimal number that the
211 ;; string represents.
212 forth_asm PARSE_NUMBER, 'PARSE-NUMBER'
213   pop rcx     ; Length
214   pop rdi     ; String pointer
215
216   push rsi
217   call parse_number
218   pop rsi
219
220   push rax                      ; Result
221   next
222
223 ;; Takes a string (in the form of a pointer and a length on the stack) and
224 ;; prints it to standard output.
225 forth_asm TELL, 'TELL'
226   pushr rax
227   pushr rsi
228
229   pop rdx ; Length
230   pop rcx ; Buffer
231   sys_print_string
232
233   popr rsi
234   popr rax
235   next
236
237 ;; Exit the program cleanly.
238 forth_asm TERMINATE, 'TERMINATE'
239   mov rax, $3C
240   mov rdi, 0
241   syscall
242
243 ;; Duplicate a pair of elements.
244 forth_asm PAIRDUP, '2DUP'
245   pop rbx
246   pop rax
247   push rax
248   push rbx
249   push rax
250   push rbx
251   next
252
253 ;; Swap the top two elements on the stack.
254 forth_asm SWAP, 'SWAP'
255   pop rax
256   pop rbx
257   push rax
258   push rbx
259   next
260
261 ;; Remove the top element from the stack.
262 forth_asm DROP, 'DROP'
263   add rsp, 8
264   next
265
266 forth_asm NOT_, 'NOT'
267   pop rax
268   cmp rax, 0
269   jz .false
270 .true:
271   push 0
272   next
273 .false:
274   push 1
275   next
276
277 ;; .U prints the value on the stack as an unsigned integer in hexadecimal.
278 forth_asm DOTU, '.U'
279   mov [.length], 0
280   mov [.printed_length], 1
281   pop rax                       ; RAX = value to print
282   push rsi                      ; Save value of RSI
283
284   ;; We start by constructing the buffer to print in reverse
285
286 .loop:
287   mov rdx, 0
288   mov rbx, $10
289   div rbx                       ; Put remainer in RDX and quotient in RAX
290
291   ;; Place the appropriate character in the buffer
292   mov rsi, .chars
293   add rsi, rdx
294   mov bl, [rsi]
295   mov rdi, .rbuffer
296   add rdi, [.length]
297   mov [rdi], bl
298   inc [.length]
299
300   ;; .printed_length is the number of characters that we ulitmately want to
301   ;; print. If we have printed a non-zero character, then we should update
302   ;; .printed_length.
303   cmp bl, '0'
304   je .skip_updating_real_length
305   mov rbx, [.length]
306   mov [.printed_length], rbx
307 .skip_updating_real_length:
308
309   cmp [.length], 16
310   jle .loop
311
312   ;; Flip buffer around, since it is currently reversed
313   mov rcx, [.printed_length]
314 .flip:
315   mov rsi, .rbuffer
316   add rsi, rcx
317   dec rsi
318   mov al, [rsi]
319
320   mov rdi, .buffer
321   add rdi, [.printed_length]
322   sub rdi, rcx
323   mov [rdi], al
324
325   loop .flip
326
327   ;; Print the buffer
328   mov rcx, .buffer
329   mov rdx, [.printed_length]
330   sys_print_string
331
332   ;; Restore RSI and continue execution
333   pop rsi
334   next
335
336 ;; Takes a value and an address, and stores the value at the given address.
337 forth_asm PUT, '!'
338   pop rbx                       ; Address
339   pop rax                       ; Value
340   mov [rbx], rax
341   next
342
343 ;; Takes an address and returns the value at the given address.
344 forth_asm GET, '@'
345   pop rax
346   mov rax, [rax]
347   push rax
348   next
349
350 forth_asm PUT_BYTE, 'C!'
351   pop rbx
352   pop rax                       ; Value
353   mov [rbx], al
354   next
355
356 forth_asm GET_BYTE, 'C@'
357   pop rax
358   movzx rax, byte [rax]
359   push rax
360   next
361
362 ;; Add two integers on the stack.
363 forth_asm PLUS, '+'
364   pop rax
365   pop rbx
366   add rax, rbx
367   push rax
368   next
369
370 ;; Calculate difference between two integers on the stack. The second number is
371 ;; subtracted from the first.
372 forth_asm MINUS, '-'
373   pop rax
374   pop rbx
375   sub rbx, rax
376   push rbx
377   next
378
379 ;; Given two integers a and b on the stack, pushes the quotient and remainder of
380 ;; division of a by b.
381 forth_asm TIMESMOD, '/MOD'
382   pop rbx                       ; b
383   pop rax                       ; a
384   mov rdx, 0
385   div rbx
386   push rax                      ; a / b
387   push rdx                      ; a % b
388   next
389
390 ;; Read user input until next " character is found. Push a string containing the
391 ;; input on the stack as (buffer length). Note that the buffer is only valid
392 ;; until the next call to S" and that no more than 255 character can be read.
393 forth_asm READ_STRING, 'S"'
394   push rsi
395
396   mov [.length], 0
397
398 .read_char:
399   mov rax, 0
400   mov rdi, 0
401   mov rsi, .char_buffer
402   mov rdx, 1
403   syscall
404
405   mov al, [.char_buffer]
406   cmp al, '"'
407   je .done
408
409   mov rdx, .buffer
410   add rdx, [.length]
411   mov [rdx], al
412   inc [.length]
413   jmp .read_char
414
415 .done:
416   pop rsi
417
418   push .buffer
419   push [.length]
420
421   next
422
423 ;; CREATE inserts a new header in the dictionary, and updates LATEST so that it
424 ;; points to the header. To compile a word, the user can then call ',' to
425 ;; continue to append data after the header.
426 ;;
427 ;; It takes the name of the word as a string (address length) on the stack.
428 forth_asm CREATE, 'CREATE'
429   pop rcx                       ; Word string length
430   pop rdx                       ; Word string pointer
431
432   mov rdi, [here]               ; rdi = Address at which to insert this entry
433   mov rax, [latest_entry]       ; rax = Address of the previous entry
434   mov [rdi], rax                ; Insert link to previous entry
435   mov [latest_entry], rdi       ; Update LATEST to point to this word
436
437   add rdi, 8
438   mov [rdi], byte 0             ; Insert immediate flag
439
440   add rdi, 1
441   mov [rdi], byte cl            ; Insert length
442
443   ;; Insert word string
444   add rdi, 1
445
446   push rsi
447   mov rsi, rdx                  ; rsi = Word string pointer
448   rep movsb
449   pop rsi
450
451   ;; Update HERE
452   mov [here], rdi
453
454   next
455
456 forth_asm TICK, "'"
457   lodsq
458   push rax
459   next
460
461 forth_asm ROT, 'ROT'
462   pop rax
463   pop rbx
464   pop rdx
465   push rax
466   push rdx
467   push rbx
468   next
469
470 forth_asm PICK, 'PICK'
471   pop rax
472   lea rax, [rsp + 8 * rax]
473   mov rax, [rax]
474   push rax
475   next
476
477 forth_asm EQL, '='
478   pop rax
479   pop rbx
480   cmp rax, rbx
481   je .eq
482 .noteq:
483   push 0
484   next
485 .eq:
486   push 1
487   next
488
489 forth MAIN, 'MAIN'
490   dq INTERPRET
491   dq BRANCH, -8 * 2
492   dq TERMINATE
493
494 ;; Built-in variables:
495
496 forth STATE, 'STATE'
497   dq LIT, var_STATE
498   dq EXIT
499
500 forth LATEST, 'LATEST'
501   dq LIT, latest_entry
502   dq EXIT
503
504 forth HERE, 'HERE'
505   dq LIT, here
506   dq EXIT
507
508 segment readable writable
509
510 ;; The LATEST variable holds a pointer to the word that was last added to the
511 ;; dictionary. This pointer is updated as new words are added, and its value is
512 ;; used by FIND to look up words.
513 latest_entry dq initial_latest_entry
514
515 ;; The STATE variable is 0 when the interpreter is executing, and non-zero when
516 ;; it is compiling.
517 var_STATE dq 0
518
519 FIND.rsi dq ?
520
521 READ_WORD.rsi dq ?
522 READ_WORD.rbp dq ?
523
524 READ_STRING.char_buffer db ?
525 READ_STRING.buffer rb $FF
526 READ_STRING.length dq ?
527
528 DOTU.chars db '0123456789ABCDEF'
529 DOTU.buffer rq 16               ; 64-bit number has no more than 16 digits in hex
530 DOTU.rbuffer rq 16
531 DOTU.length dq ?
532 DOTU.printed_length dq ?
533
534 ;; Reserve space for compiled words, accessed through HERE.
535 here dq here_top
536 here_top rq $4000
537
538 ;; Return stack
539 rq $2000
540 return_stack_top: