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