7443c48aac209a0d46f08d1cddeb710766655ee9
[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 ;; Read a word from a buffer. Expects (buffer buffer-length) on the stack.
211 ;; Updates buffer and buffer-length, such that the word has been removed from
212 ;; the buffer. Appends (word-buffer word-buffer-length) to the stack.
213 forth_asm POP_WORD, 'POP-WORD'
214   pushr rsi
215
216   pop rcx ; Length
217   pop rsi ; Buffer
218
219   call pop_word
220
221   push rsi ; Updated buffer
222   push rcx ; Length of updated buffer
223   push rdi ; Word buffer
224   push rdx ; Length of word buffer
225
226   popr rsi
227   next
228
229 ;; Takes a string on the stack and replaces it with the decimal number that the
230 ;; string represents.
231 forth_asm PARSE_NUMBER, 'PARSE-NUMBER'
232   pop rcx     ; Length
233   pop rdi     ; String pointer
234
235   push rsi
236   call parse_number
237   pop rsi
238
239   push rax                      ; Result
240   next
241
242 ;; Takes a string (in the form of a pointer and a length on the stack) and
243 ;; prints it to standard output.
244 forth_asm TELL, 'TELL'
245   pushr rax
246   pushr rsi
247
248   pop rdx ; Length
249   pop rcx ; Buffer
250   sys_print_string
251
252   popr rsi
253   popr rax
254   next
255
256 ;; Exit the program cleanly.
257 forth_asm TERMINATE, 'TERMINATE'
258   mov rax, $3C
259   mov rdi, 0
260   syscall
261
262 ;; Duplicate a pair of elements.
263 forth_asm PAIRDUP, '2DUP'
264   pop rbx
265   pop rax
266   push rax
267   push rbx
268   push rax
269   push rbx
270   next
271
272 ;; Swap the top two elements on the stack.
273 forth_asm SWAP, 'SWAP'
274   pop rax
275   pop rbx
276   push rax
277   push rbx
278   next
279
280 ;; Remove the top element from the stack.
281 forth_asm DROP, 'DROP'
282   add rsp, 8
283   next
284
285 forth_asm NOT_, 'NOT'
286   pop rax
287   cmp rax, 0
288   jz .false
289 .true:
290   push 0
291   next
292 .false:
293   push 1
294   next
295
296 ;; .U prints the value on the stack as an unsigned integer in hexadecimal.
297 forth_asm DOTU, '.U'
298   mov [.length], 0
299   mov [.printed_length], 1
300   pop rax                       ; RAX = value to print
301   push rsi                      ; Save value of RSI
302
303   ;; We start by constructing the buffer to print in reverse
304
305 .loop:
306   mov rdx, 0
307   mov rbx, $10
308   div rbx                       ; Put remainer in RDX and quotient in RAX
309
310   ;; Place the appropriate character in the buffer
311   mov rsi, .chars
312   add rsi, rdx
313   mov bl, [rsi]
314   mov rdi, .rbuffer
315   add rdi, [.length]
316   mov [rdi], bl
317   inc [.length]
318
319   ;; .printed_length is the number of characters that we ulitmately want to
320   ;; print. If we have printed a non-zero character, then we should update
321   ;; .printed_length.
322   cmp bl, '0'
323   je .skip_updating_real_length
324   mov rbx, [.length]
325   mov [.printed_length], rbx
326 .skip_updating_real_length:
327
328   cmp [.length], 16
329   jle .loop
330
331   ;; Flip buffer around, since it is currently reversed
332   mov rcx, [.printed_length]
333 .flip:
334   mov rsi, .rbuffer
335   add rsi, rcx
336   dec rsi
337   mov al, [rsi]
338
339   mov rdi, .buffer
340   add rdi, [.printed_length]
341   sub rdi, rcx
342   mov [rdi], al
343
344   loop .flip
345
346   ;; Print the buffer
347   mov rcx, .buffer
348   mov rdx, [.printed_length]
349   sys_print_string
350
351   ;; Restore RSI and continue execution
352   pop rsi
353   next
354
355 ;; Takes a value and an address, and stores the value at the given address.
356 forth_asm PUT, '!'
357   pop rbx                       ; Address
358   pop rax                       ; Value
359   mov [rbx], rax
360   next
361
362 ;; Takes an address and returns the value at the given address.
363 forth_asm GET, '@'
364   pop rax
365   mov rax, [rax]
366   push rax
367   next
368
369 forth_asm PUT_BYTE, 'C!'
370   pop rbx
371   pop rax                       ; Value
372   mov [rbx], al
373   next
374
375 forth_asm GET_BYTE, 'C@'
376   pop rax
377   movzx rax, byte [rax]
378   push rax
379   next
380
381 ;; Add two integers on the stack.
382 forth_asm PLUS, '+'
383   pop rax
384   pop rbx
385   add rax, rbx
386   push rax
387   next
388
389 ;; Calculate difference between two integers on the stack. The second number is
390 ;; subtracted from the first.
391 forth_asm MINUS, '-'
392   pop rax
393   pop rbx
394   sub rbx, rax
395   push rbx
396   next
397
398 ;; Given two integers a and b on the stack, pushes the quotient and remainder of
399 ;; division of a by b.
400 forth_asm TIMESMOD, '/MOD'
401   pop rbx                       ; b
402   pop rax                       ; a
403   mov rdx, 0
404   div rbx
405   push rax                      ; a / b
406   push rdx                      ; a % b
407   next
408
409 ;; Read user input until next " character is found. Push a string containing the
410 ;; input on the stack as (buffer length). Note that the buffer is only valid
411 ;; until the next call to S" and that no more than 255 characters can be read.
412 ;;
413 ;; [TODO] We want to be able to use this when reading from buffers (e.g. in
414 ;; INTERPRET-STRING) too!
415 forth_asm READ_STRING, 'S"'
416   push rsi
417
418   mov [.length], 0
419
420 .read_char:
421   mov rax, 0
422   mov rdi, 0
423   mov rsi, .char_buffer
424   mov rdx, 1
425   syscall
426
427   mov al, [.char_buffer]
428   cmp al, '"'
429   je .done
430
431   mov rdx, .buffer
432   add rdx, [.length]
433   mov [rdx], al
434   inc [.length]
435   jmp .read_char
436
437 .done:
438   pop rsi
439
440   push .buffer
441   push [.length]
442
443   next
444
445 ;; CREATE inserts a new header in the dictionary, and updates LATEST so that it
446 ;; points to the header. To compile a word, the user can then call ',' to
447 ;; continue to append data after the header.
448 ;;
449 ;; It takes the name of the word as a string (address length) on the stack.
450 forth_asm CREATE, 'CREATE'
451   pop rcx                       ; Word string length
452   pop rdx                       ; Word string pointer
453
454   mov rdi, [here]               ; rdi = Address at which to insert this entry
455   mov rax, [latest_entry]       ; rax = Address of the previous entry
456   mov [rdi], rax                ; Insert link to previous entry
457   mov [latest_entry], rdi       ; Update LATEST to point to this word
458
459   add rdi, 8
460   mov [rdi], byte 0             ; Insert immediate flag
461
462   add rdi, 1
463   mov [rdi], byte cl            ; Insert length
464
465   ;; Insert word string
466   add rdi, 1
467
468   push rsi
469   mov rsi, rdx                  ; rsi = Word string pointer
470   rep movsb
471   pop rsi
472
473   ;; Update HERE
474   mov [here], rdi
475
476   next
477
478 forth_asm TICK, "'"
479   lodsq
480   push rax
481   next
482
483 forth_asm ROT, 'ROT'
484   pop rax
485   pop rbx
486   pop rdx
487   push rax
488   push rdx
489   push rbx
490   next
491
492 forth_asm PICK, 'PICK'
493   pop rax
494   lea rax, [rsp + 8 * rax]
495   mov rax, [rax]
496   push rax
497   next
498
499 forth_asm EQL, '='
500   pop rax
501   pop rbx
502   cmp rax, rbx
503   je .eq
504 .noteq:
505   push 0
506   next
507 .eq:
508   push 1
509   next
510
511 forth MAIN, 'MAIN'
512   dq SYSCODE
513   dq INTERPRET_STRING
514   dq INTERPRET
515   dq BRANCH, -8 * 2
516   dq TERMINATE
517
518 ;; Built-in variables:
519
520 forth STATE, 'STATE'
521   dq LIT, var_STATE
522   dq EXIT
523
524 forth LATEST, 'LATEST'
525   dq LIT, latest_entry
526   dq EXIT
527
528 forth HERE, 'HERE'
529   dq LIT, here
530   dq EXIT
531
532 forth SYSCODE, 'SYSCODE'
533   dq LIT, sysf
534   dq LIT, sysf.len
535   dq EXIT
536
537 forth INPUT_BUFFER, 'INPUT-BUFFER'
538   dq LIT, input_buffer
539   dq EXIT
540
541 forth INPUT_LENGTH, 'INPUT-LENGTH'
542   dq LIT, input_buffer_length
543   dq EXIT
544
545 segment readable writable
546
547 ;; The LATEST variable holds a pointer to the word that was last added to the
548 ;; dictionary. This pointer is updated as new words are added, and its value is
549 ;; used by FIND to look up words.
550 latest_entry dq initial_latest_entry
551
552 ;; The STATE variable is 0 when the interpreter is executing, and non-zero when
553 ;; it is compiling.
554 var_STATE dq 0
555
556 FIND.rsi dq ?
557
558 READ_WORD.rsi dq ?
559 READ_WORD.rbp dq ?
560
561 READ_STRING.char_buffer db ?
562 READ_STRING.buffer rb $FF
563 READ_STRING.length dq ?
564
565 DOTU.chars db '0123456789ABCDEF'
566 DOTU.buffer rq 16               ; 64-bit number has no more than 16 digits in hex
567 DOTU.rbuffer rq 16
568 DOTU.length dq ?
569 DOTU.printed_length dq ?
570
571 ;; Reserve space for compiled words, accessed through HERE.
572 here dq here_top
573 here_top rq $4000
574
575 ;; Pointer to input buffer and its length. Used as local variable in
576 ;; INTERPRET-STRING (see bootstrap.asm). [TODO] The code organization is a bit
577 ;; awkward here.
578 input_buffer dq ?
579 input_buffer_length dq ?
580
581 ;; Return stack
582 rq $2000
583 return_stack_top:
584
585 segment readable
586
587 ;; We store some Forth code in sys.f that defined common words that the user
588 ;; would expect to have available at startup. To execute these words, we just
589 ;; include the file directly in the binary, and then interpret it at startup.
590 sysf file 'sys.f'
591 sysf.len = $ - sysf
592