; ==================================================================
; MIBASIC.ASM - MIKES INTEGER BASIC INTERPRETER
; based on MIKEOS BASIC by Mike Saunders (http://mikeos.berlios.de)
; "OS-independent" standalone version, Neville Watkin, April 2011
; ==================================================================
; ------------------------------------------------------------------
; Token types
define VARIABLE 1
define STRING_VAR 2
define NUMBER 3
define STRING 4
define QUOTE 5
define CHAR 6
define UNKNOWN 7
define LABEL 8
; ------------------------------------------------------------------
; The BASIC intepreter execution starts here...

        ORG     0100H           ;just for a quick DOS test

os_run_basic:
  mov dword [orig_stack], esp           ; Save stack pointer -- we might jump to the
                                        ; error printing code and quit in the middle
                                        ; some nested loops, and we want to preserve
                                        ; the stack
        MOV     EAX,basic_prog ;embedded test program for a quick DOS test
        MOV     EBX,8192       ;default size for test program (not critical)
  mov dword [load_point], eax           ; EAX was passed as starting location of code
  mov dword [prog], eax                 ; prog = pointer to current execution point in code
  add ebx, eax                          ; We were passed the .BAS byte size in BX
  dec ebx
  dec ebx
  mov dword [prog_end], ebx             ; Make note of program end point
  call clear_ram                        ; Clear variables etc. from previous run
                                        ; of a BASIC program
mainloop:
  call get_token                        ; Get a token from the start of the line
  cmp eax, STRING                       ; Is the type a string of characters?
  je .keyword                           ; If so, let's see if it's a keyword to process
  cmp eax, VARIABLE                     ; If it's a variable at the start of the line,
  je near assign                        ; this is an assign (eg "X = Y + 5")
  cmp eax, STRING_VAR                   ; Same for a string variable (eg $1)
  je near assign
  cmp eax, LABEL                        ; Don't need to do anything here - skip
  je mainloop
  mov esi, err_syntax                   ; Otherwise show an error and quit
  jmp error
.keyword:
  mov esi, token                        ; Start trying to match commands
  mov edi, alert_cmd
  call os_string_compare
  jc near do_alert
  mov edi, call_cmd
  call os_string_compare
  jc near do_call
  mov edi, cls_cmd
  call os_string_compare
  jc near do_cls
  mov edi, cursor_cmd
  call os_string_compare
  jc near do_cursor
  mov edi, curschar_cmd
  call os_string_compare
  jc near do_curschar
  mov edi, end_cmd
  call os_string_compare
  jc near do_end
  mov edi, for_cmd
  call os_string_compare
  jc near do_for
  mov edi, getkey_cmd
  call os_string_compare
  jc near do_getkey
  mov edi, gosub_cmd
  call os_string_compare
  jc near do_gosub
  mov edi, goto_cmd
  call os_string_compare
  jc near do_goto
  mov edi, input_cmd
  call os_string_compare
  jc near do_input
  mov edi, if_cmd
  call os_string_compare
  jc near do_if
;  mov edi, load_cmd
;  call os_string_compare
;  jc near do_load
  mov edi, move_cmd
  call os_string_compare
  jc near do_move
  mov edi, next_cmd
  call os_string_compare
  jc near do_next
  mov edi, pause_cmd
  call os_string_compare
  jc near do_pause
  mov edi, peek_cmd
  call os_string_compare
  jc near do_peek
  mov edi, poke_cmd
  call os_string_compare
  jc near do_poke
  mov edi, port_cmd
  call os_string_compare
  jc near do_port
  mov edi, print_cmd
  call os_string_compare
  jc near do_print
  mov edi, rand_cmd
  call os_string_compare
  jc near do_rand
  mov edi, rem_cmd
  call os_string_compare
  jc near do_rem
  mov edi, return_cmd
  call os_string_compare
  jc near do_return
;  mov edi, save_cmd
;  call os_string_compare
;  jc near do_save
  mov edi, serial_cmd
  call os_string_compare
  jc near do_serial
  mov edi, sound_cmd
  call os_string_compare
  jc near do_sound
  mov edi, waitkey_cmd
  call os_string_compare
  jc near do_waitkey
  mov esi, err_cmd_unknown              ; Command not found?
  jmp error
; ------------------------------------------------------------------
; CLEAR RAM
clear_ram:
  mov al, 0
  mov edi, variables
  mov ecx, 52
  rep stosb
  mov edi, for_variables
  mov ecx, 52
  rep stosb
  mov edi, for_code_points
  mov ecx, 52
  rep stosb
  mov byte [gosub_depth], 0
  mov edi, gosub_points
  mov ecx, 20
  rep stosb
  mov edi, string_vars
  mov ecx, 1024
  rep stosb
  ret
; ------------------------------------------------------------------
; ASSIGNMENT
assign:
  cmp eax, VARIABLE                     ; Are we starting with a number var?
  je .do_num_var
  mov edi, string_vars                  ; Otherwise it's a string var
  mov eax, 128
  mul ebx                               ; (EBX = string number, passed back from get_token)
  add edi, eax
  push edi
  call get_token
  mov byte al, [token]
  cmp al, '='
  jne near .error
  call get_token
  cmp eax, QUOTE
  je .second_is_quote
  cmp eax, STRING_VAR
  jne near .error
  mov esi, string_vars                  ; Otherwise it's a string var
  mov eax, 128
  mul ebx                               ; (EBX = string number, passed back from get_token)
  add esi, eax
  pop edi
  call os_string_copy
  jmp mainloop
.second_is_quote:
  mov esi, token
  pop edi
  call os_string_copy
  jmp mainloop
.do_num_var:
  mov eax, 0
  mov byte al, [token]
  mov byte [.tmp], al
  call get_token
  mov byte al, [token]
  cmp al, '='
  jne near .error
  call get_token
  cmp eax, NUMBER
  je .second_is_num
  cmp eax, VARIABLE
  je .second_is_variable
  cmp eax, STRING
  je near .second_is_string
  cmp eax, UNKNOWN
  jne near .error
  mov byte al, [token]                  ; Address of string var?
  cmp al, '&'
  jne near .error
  call get_token                        ; Let's see if there's a string var
  cmp eax, STRING_VAR
  jne near .error
  mov edi, string_vars
  mov eax, 128
  mul ebx
  add edi, eax
  mov ebx, edi
  mov byte al, [.tmp]
  call set_var
  jmp mainloop
.second_is_variable:
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov ebx, eax
  mov byte al, [.tmp]
  call set_var
  jmp .check_for_more
.second_is_num:
  mov esi, token
  call os_string_to_int
  mov ebx, eax                          ; Number to insert in variable table
  mov eax, 0
  mov byte al, [.tmp]
  call set_var
                                        ; The assignment could be simply "X = 5" etc. Or it could be
                                        ; "X = Y + 5" -- ie more complicated. So here we check to see if
                                        ; there's a delimiter...
.check_for_more:
  mov dword eax, [prog]                 ; Save code location in case there's no delimiter
  mov dword [.tmp_loc], eax
  call get_token                        ; Any more to deal with in this assignment?
  mov byte al, [token]
  cmp al, '+'
  je .theres_more
  cmp al, '-'
  je .theres_more
  cmp al, '*'
  je .theres_more
  cmp al, '/'
  je .theres_more
  cmp al, '%'
  je .theres_more
  mov dword eax, [.tmp_loc]             ; Not a delimiter, so step back before the token
  mov dword [prog], eax                 ; that we just grabbed
  jmp mainloop                          ; And go back to the code interpreter!
.theres_more:
  mov byte [.delim], al
  call get_token
  cmp eax, VARIABLE
  je .handle_variable
  mov esi, token
  call os_string_to_int
  mov ebx, eax
  mov eax, 0
  mov byte al, [.tmp]
  call get_var                          ; This also points ESI at right place in variable table
  cmp byte [.delim], '+'
  jne .not_plus
  add eax, ebx
  jmp .finish
.not_plus:
  cmp byte [.delim], '-'
  jne .not_minus
  sub eax, ebx
  jmp .finish
.not_minus:
  cmp byte [.delim], '*'
  jne .not_times
  mul ebx
  jmp .finish
.not_times:
  cmp byte [.delim], '/'
  jne .not_divide
  mov edx, 0
  div ebx
  jmp .finish
.not_divide:
  mov edx, 0
  div ebx
  mov eax, edx                          ; Get remainder
.finish:
  mov ebx, eax
  mov byte al, [.tmp]
  call set_var
  jmp .check_for_more
.handle_variable:
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov ebx, eax
  mov eax, 0
  mov byte al, [.tmp]
  call get_var
  cmp byte [.delim], '+'
  jne .vnot_plus
  add eax, ebx
  jmp .vfinish
.vnot_plus:
  cmp byte [.delim], '-'
  jne .vnot_minus
  sub eax, ebx
  jmp .vfinish
.vnot_minus:
  cmp byte [.delim], '*'
  jne .vnot_times
  mul ebx
  jmp .vfinish
.vnot_times:
  cmp byte [.delim], '/'
  jne .vnot_divide
  mov edx, 0
  div ebx
  jmp .finish
.vnot_divide:
  mov edx, 0
  div ebx
  mov eax, edx                          ; Get remainder
.vfinish:
  mov ebx, eax
  mov byte al, [.tmp]
  call set_var
  jmp .check_for_more
.second_is_string:
  mov edi, token
  mov esi, progstart_keyword
  call os_string_compare
  je .is_progstart
  mov esi, ramstart_keyword
  call os_string_compare
  je .is_ramstart
  jmp .error
.is_progstart:
  mov eax, 0
  mov byte al, [.tmp]
  mov dword ebx, [load_point]
  call set_var
  jmp mainloop
.is_ramstart:
  mov eax, 0
  mov byte al, [.tmp]
  mov dword ebx, [prog_end]
  inc ebx
  inc ebx
  inc ebx
  call set_var
  jmp mainloop
.error:
  mov esi, err_syntax
  jmp error
.tmp db 0
.tmp_loc dd 0
.delim db 0
; ==================================================================
; SPECIFIC COMMAND CODE STARTS HERE
; ------------------------------------------------------------------
; ALERT
do_alert:
  call get_token
  cmp eax, QUOTE
  je .is_quote
  mov esi, err_syntax
  jmp error
.is_quote:
  mov eax, token                        ; First string for alert box
  mov ebx, 0                            ; Others are blank
  mov ecx, 0
  mov edx, 0                            ; One-choice box
  call os_dialog_box
  jmp mainloop
; ------------------------------------------------------------------
; CALL
do_call:
  call get_token
  cmp eax, NUMBER
  je .is_number
  mov eax, 0
  mov byte al, [token]
  call get_var
  jmp .execute_call
.is_number:
  mov esi, token
  call os_string_to_int
.execute_call:
  mov ebx, 0
  mov ecx, 0
  mov edx, 0
  mov edi, 0
  mov esi, 0
  call eax
  jmp mainloop
; ------------------------------------------------------------------
; CLS
do_cls:
  call os_clear_screen
  jmp mainloop
; ------------------------------------------------------------------
; CURSOR
do_cursor:
  call get_token
  mov esi, token
  mov edi, .on_str
  call os_string_compare
  jc .turn_on
  mov esi, token
  mov edi, .off_str
  call os_string_compare
  jc .turn_off
  mov esi, err_syntax
  jmp error
.turn_on:
  call os_show_cursor
  jmp mainloop
.turn_off:
  call os_hide_cursor
  jmp mainloop
.on_str db "ON", 0
.off_str db "OFF", 0
; ------------------------------------------------------------------
; CURSCHAR
do_curschar:
  call get_token
  cmp eax, VARIABLE
  je .is_ok
  mov esi, err_syntax
  jmp error
.is_ok:
  mov eax, 0
  mov byte al, [token]
  push eax                              ; Store variable we're going to use
  mov ah, 08h
  mov ebx, 0
  int 10h                               ; Get char at current cursor location
  mov ebx, 0                            ; We only want the lower byte (the char, not attribute)
  mov bl, al
  pop eax                               ; Get the variable back
  call set_var                          ; And store the value
  jmp mainloop
; ------------------------------------------------------------------
; END
do_end:
  mov dword esp, [orig_stack]
  ret
; ------------------------------------------------------------------
; FOR
do_for:
  call get_token                        ; Get the variable we're using in this loop
  cmp eax, VARIABLE
  jne near .error
  mov eax, 0
  mov byte al, [token]
  mov byte [.tmp_var], al               ; Store it in a temporary location for now
  call get_token
  mov eax, 0                            ; Check it's followed up with '='
  mov byte al, [token]
  cmp al, '='
  jne .error
  call get_token                        ; Next we want a number
  cmp eax, NUMBER
  jne .error
  mov esi, token                        ; Convert it
  call os_string_to_int
                                        ; At this stage, we've read something like "FOR X = 1"
                                        ; so let's store that 1 in the variable table
  mov ebx, eax
  mov eax, 0
  mov byte al, [.tmp_var]
  call set_var
  call get_token                        ; Next we're looking for "TO"
  cmp eax, STRING
  jne .error
  mov eax, token
  call os_string_uppercase
  mov esi, token
  mov edi, .to_string
  call os_string_compare
  jnc .error
                                        ; So now we're at "FOR X = 1 TO"
  call get_token
  cmp eax, NUMBER
  jne .error
  mov esi, token                        ; Get target number
  call os_string_to_int
  mov ebx, eax
  mov eax, 0
  mov byte al, [.tmp_var]
  sub al, 65                            ; Store target number in table
  mov edi, for_variables
  add edi, eax
  add edi, eax
  mov eax, ebx
  stosw
                                        ; So we've got the variable, assigned it the starting number, and put into
                                        ; our table the limit it should reach. But we also need to store the point in
                                        ; code after the FOR line we should return to if NEXT X doesn't complete the loop...
  mov eax, 0
  mov byte al, [.tmp_var]
  sub al, 65                            ; Store code position to return to in table
  mov edi, for_code_points
  add edi, eax
  add edi, eax
  mov dword eax, [prog]
  stosw
  jmp mainloop
.error:
  mov esi, err_syntax
  jmp error
.tmp_var db 0
.to_string db 'TO', 0
; ------------------------------------------------------------------
; GETKEY
do_getkey:
  call get_token
  cmp eax, VARIABLE
  je .is_variable
  mov esi, err_syntax
  jmp error
.is_variable:
  mov eax, 0
  mov byte al, [token]
  push eax
  call os_check_for_key
  mov ebx, 0
  mov bl, al
  pop eax
  call set_var
  jmp mainloop
; ------------------------------------------------------------------
; GOSUB
do_gosub:
  call get_token                        ; Get the number (label)
  cmp eax, STRING
  je .is_ok
  mov esi, err_goto_notlabel
  jmp error
.is_ok:
  mov esi, token                        ; Back up this label
  mov edi, .tmp_token
  call os_string_copy
  mov eax, .tmp_token
  call os_string_length
  mov edi, .tmp_token                   ; Add ':' char to end for searching
  add edi, eax
  mov al, ':'
  stosb
  mov al, 0
  stosb
  inc byte [gosub_depth]
  mov eax, 0
  mov byte al, [gosub_depth]            ; Get current GOSUB nest level
  cmp al, 9
  jle .within_limit
  mov esi, err_nest_limit
  jmp error
.within_limit:
  mov edi, gosub_points                 ; Move into our table of pointers
  add edi, eax                          ; Table is words (not bytes)
  add edi, eax
  mov dword eax, [prog]
  stosw                                 ; Store current location before jump
  mov dword eax, [load_point]
  mov dword [prog], eax                 ; Return to start of program to find label
.loop:
  call get_token
  cmp eax, LABEL
  jne .line_loop
  mov esi, token
  mov edi, .tmp_token
  call os_string_compare
  jc mainloop
.line_loop:                             ; Go to end of line
  mov dword esi, [prog]
  mov byte al, [esi]
  inc dword [prog]
  cmp al, 10
  jne .line_loop
  mov dword eax, [prog]
  mov dword ebx, [prog_end]
  cmp eax, ebx
  jg .past_end
  jmp .loop
.past_end:
  mov esi, err_label_notfound
  jmp error
.tmp_token: times 30 db 0
; ------------------------------------------------------------------
; GOTO
do_goto:
  call get_token                        ; Get the next token
  cmp eax, STRING
  je .is_ok
  mov esi, err_goto_notlabel
  jmp error
.is_ok:
  mov esi, token                        ; Back up this label
  mov edi, .tmp_token
  call os_string_copy
  mov eax, .tmp_token
  call os_string_length
  mov edi, .tmp_token                   ; Add ':' char to end for searching
  add edi, eax
  mov al, ':'
  stosb
  mov al, 0
  stosb
  mov dword eax, [load_point]
  mov dword [prog], eax                 ; Return to start of program to find label
.loop:
  call get_token
  cmp eax, LABEL
  jne .line_loop
  mov esi, token
  mov edi, .tmp_token
  call os_string_compare
  jc mainloop
.line_loop:                             ; Go to end of line
  mov dword esi, [prog]
  mov byte al, [esi]
  inc dword [prog]
  cmp al, 10
  jne .line_loop
  mov dword eax, [prog]
  mov dword ebx, [prog_end]
  cmp eax, ebx
  jg .past_end
  jmp .loop
.past_end:
  mov esi, err_label_notfound
  jmp error
.tmp_token: times 30 db 0
; ------------------------------------------------------------------
; IF
do_if:
  call get_token
  cmp eax, VARIABLE                     ; If can only be followed by a variable
  je .num_var
  cmp eax, STRING_VAR
  je near .string_var
  mov esi, err_syntax
  jmp error
.num_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov edx, eax                          ; Store value of first part of comparison
  call get_token                        ; Get the delimiter
  mov byte al, [token]
  cmp al, '='
  je .equals
  cmp al, '>'
  je .greater
  cmp al, '<'
  je .less
  mov esi, err_syntax                   ; If not one of the above, error out
  jmp error
.equals:
  call get_token                        ; Is this 'X = Y' (equals another variable?)
  cmp eax, CHAR
  je .equals_char
  mov byte al, [token]
  call is_letter
  jc .equals_var
  mov esi, token                        ; Otherwise it's, eg 'X = 1' (a number)
  call os_string_to_int
  cmp eax, edx                          ; On to the THEN bit if 'X = num' matches
  je near .on_to_then
  jmp .finish_line                      ; Otherwise skip the rest of the line
.equals_char:
  mov eax, 0
  mov byte al, [token]
  cmp eax, edx
  je near .on_to_then
  jmp .finish_line
.equals_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  cmp eax, edx                          ; Do the variables match?
  je near .on_to_then                   ; On to the THEN bit if so
  jmp .finish_line                      ; Otherwise skip the rest of the line
.greater:
  call get_token                        ; Greater than a variable or number?
  mov byte al, [token]
  call is_letter
  jc .greater_var
  mov esi, token                        ; Must be a number here...
  call os_string_to_int
  cmp eax, edx
  jl near .on_to_then
  jmp .finish_line
.greater_var:                           ; Variable in this case
  mov eax, 0
  mov byte al, [token]
  call get_var
  cmp eax, edx                          ; Make the comparison!
  jl .on_to_then
  jmp .finish_line
.less:
  call get_token
  mov byte al, [token]
  call is_letter
  jc .less_var
  mov esi, token
  call os_string_to_int
  cmp eax, edx
  jg .on_to_then
  jmp .finish_line
.less_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  cmp eax, edx
  jg .on_to_then
  jmp .finish_line
.string_var:
  mov byte [.tmp_string_var], bl
  call get_token
  mov byte al, [token]
  cmp al, '='
  jne .error
  call get_token
  cmp eax, STRING_VAR
  je .second_is_string_var
  cmp eax, QUOTE
  jne .error
  mov esi, string_vars
  mov eax, 128
  mul ebx
  add esi, eax
  mov edi, token
  call os_string_compare
  je .on_to_then
  jmp .finish_line
.second_is_string_var:
  mov esi, string_vars
  mov eax, 128
  mul ebx
  add esi, eax
  mov edi, string_vars
  mov ebx, 0
  mov byte bl, [.tmp_string_var]
  mov eax, 128
  mul ebx
  add edi, eax
  call os_string_compare
  jc .on_to_then
  jmp .finish_line
.on_to_then:
  call get_token
  mov esi, token
  mov edi, then_keyword
  call os_string_compare
  jc .then_present
  mov esi, err_syntax
  jmp error
.then_present:                          ; Continue rest of line like any other command!
  jmp mainloop
.finish_line:                           ; IF wasn't fulfilled, so skip rest of line
  mov dword esi, [prog]
  mov byte al, [esi]
  inc dword [prog]
  cmp al, 10
  jne .finish_line
  jmp mainloop
.error:
  mov esi, err_syntax
  jmp error
.tmp_string_var db 0
; ------------------------------------------------------------------
; INPUT
do_input:
  mov al, 0                             ; Clear string from previous usage
  mov edi, .tmpstring
  mov ecx, 128
  rep stosb
  call get_token
  cmp eax, VARIABLE                     ; We can only INPUT to variables!
  je .number_var
  cmp eax, STRING_VAR
  je .string_var
  mov esi, err_syntax
  jmp error
.number_var:
  mov eax, .tmpstring                   ; Get input from the user
  call os_input_string
  mov eax, .tmpstring
  call os_string_length
  cmp eax, 0
  jne .char_entered
  mov byte [.tmpstring], '0'            ; If enter hit, fill variable with zero
  mov byte [.tmpstring + 1], 0
.char_entered:
  mov esi, .tmpstring                   ; Convert to integer format
  call os_string_to_int
  mov ebx, eax
  mov eax, 0
  mov byte al, [token]                  ; Get the variable where we're storing it...
  call set_var                          ; ...and store it!
  call os_print_newline
  jmp mainloop
.string_var:
  push ebx
  mov eax, .tmpstring
  call os_input_string
  mov esi, .tmpstring
  mov edi, string_vars
  pop ebx
  mov eax, 128
  mul ebx
  add edi, eax
  call os_string_copy
  call os_print_newline
  jmp mainloop
.tmpstring: times 128 db 0
; ------------------------------------------------------------------
;; LOAD
;do_load:
;  call get_token
;  cmp eax, QUOTE
;  je .is_quote
;  cmp eax, STRING_VAR
;  jne .error
;  mov esi, string_vars
;  mov eax, 128
;  mul ebx
;  add esi, eax
;  jmp .get_position
;.is_quote:
;  mov esi, token
;.get_position:
;  mov eax, esi
;  call os_file_exists
;  jc .file_not_exists
;  mov edx, eax                          ; Store for now
;  call get_token
;  cmp eax, VARIABLE
;  je .second_is_var
;  cmp eax, NUMBER
;  jne .error
;  mov esi, token
;  call os_string_to_int
;.load_part:
;  mov ecx, eax
;  mov eax, edx
;  call os_load_file
;  mov eax, 0
;  mov byte al, 'S'
;  call set_var
;  mov eax, 0
;  mov byte al, 'R'
;  mov ebx, 0
;  call set_var
;  jmp mainloop
;.second_is_var:
;  mov eax, 0
;  mov byte al, [token]
;  call get_var
;  jmp .load_part
;.file_not_exists:
;  mov eax, 0
;  mov byte al, 'R'
;  mov ebx, 1
;  call set_var
;  call get_token                        ; Skip past the loading point -- unnecessary now
;  jmp mainloop
;.error:
;  mov esi, err_syntax
;  jmp error
; ------------------------------------------------------------------
; MOVE
do_move:
  call get_token
  cmp eax, VARIABLE
  je .first_is_var
  mov esi, token
  call os_string_to_int
  mov dl, al
  jmp .onto_second
.first_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov dl, al
.onto_second:
  call get_token
  cmp eax, VARIABLE
  je .second_is_var
  mov esi, token
  call os_string_to_int
  mov dh, al
  jmp .finish
.second_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov dh, al
.finish:
  call os_move_cursor
  jmp mainloop
; ------------------------------------------------------------------
; NEXT
do_next:
  call get_token
  cmp eax, VARIABLE                     ; NEXT must be followed by a variable
  jne .error
  mov eax, 0
  mov byte al, [token]
  call get_var
  inc eax                               ; NEXT increments the variable, of course!
  mov ebx, eax
  mov eax, 0
  mov byte al, [token]
  sub al, 65
  mov esi, for_variables
  add esi, eax
  add esi, eax
  lodsw                                 ; Get the target number from the table
  inc eax                               ; (Make the loop inclusive of target number)
  cmp eax, ebx                          ; Do the variable and target match?
  je .loop_finished
  mov eax, 0                            ; If not, store the updated variable
  mov byte al, [token]
  call set_var
  mov eax, 0                            ; Find the code point and go back
  mov byte al, [token]
  sub al, 65
  mov esi, for_code_points
  add esi, eax
  add esi, eax
  lodsw
  mov dword [prog], eax
  jmp mainloop
.loop_finished:
  jmp mainloop
.error:
  mov esi, err_syntax
  jmp error
; ------------------------------------------------------------------
; PAUSE
do_pause:
  call get_token
  cmp eax, VARIABLE
  je .is_var
  mov esi, token
  call os_string_to_int
  jmp .finish
.is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
.finish:
  call os_pause
  jmp mainloop
; ------------------------------------------------------------------
; PEEK
do_peek:
  call get_token
  cmp eax, VARIABLE
  jne .error
  mov eax, 0
  mov byte al, [token]
  mov byte [.tmp_var], al
  call get_token
  cmp eax, VARIABLE
  je .dereference
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int
.store:
  mov esi, eax
  mov ebx, 0
  mov byte bl, [esi]
  mov eax, 0
  mov byte al, [.tmp_var]
  call set_var
  jmp mainloop
.dereference:
  mov byte al, [token]
  call get_var
  jmp .store
.error:
  mov esi, err_syntax
  jmp error
.tmp_var db 0
; ------------------------------------------------------------------
; POKE
do_poke:
  call get_token
  cmp eax, VARIABLE
  je .first_is_var
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int
  cmp eax, 255
  jg .error
  mov byte [.first_value], al
  jmp .onto_second
.first_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov byte [.first_value], al
.onto_second:
  call get_token
  cmp eax, VARIABLE
  je .second_is_var
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int
.got_value:
  mov edi, eax
  mov eax, 0
  mov byte al, [.first_value]
  mov byte [edi], al
  jmp mainloop
.second_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  jmp .got_value
.error:
  mov esi, err_syntax
  jmp error
.first_value db 0
; ------------------------------------------------------------------
; PORT
do_port:
  call get_token
  mov esi, token
  mov edi, .out_cmd
  call os_string_compare
  jc .do_out_cmd
  mov edi, .in_cmd
  call os_string_compare
  jc .do_in_cmd
  jmp .error
.do_out_cmd:
  call get_token
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int                 ; Now EAX = port number
  mov edx, eax
  call get_token
  cmp eax, NUMBER
  je .out_is_num
  cmp eax, VARIABLE
  je .out_is_var
  jmp .error
.out_is_num:
  mov esi, token
  call os_string_to_int
  call os_port_byte_out
  jmp mainloop
.out_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
  call os_port_byte_out
  jmp mainloop
.do_in_cmd:
  call get_token
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int
  mov edx, eax
  call get_token
  cmp eax, VARIABLE
  jne .error
  mov byte cl, [token]
  call os_port_byte_in
  mov ebx, 0
  mov bl, al
  mov al, cl
  call set_var
  jmp mainloop
.error:
  mov esi, err_syntax
  jmp error
.out_cmd db "OUT", 0
.in_cmd db "IN", 0
; ------------------------------------------------------------------
; PRINT
do_print:
  call get_token                        ; Get part after PRINT
  cmp eax, QUOTE                        ; What type is it?
  je .print_quote
  cmp eax, VARIABLE                     ; Numerical variable (eg X)
  je .print_var
  cmp eax, STRING_VAR                   ; String variable (eg $1)
  je .print_string_var
  cmp eax, STRING                       ; Special keyword (eg CHR or HEX)
  je .print_keyword
  mov esi, err_print_type               ; We only print quoted strings and vars!
  jmp error
.print_var:
  mov eax, 0
  mov byte al, [token]
  call get_var                          ; Get its value
  call os_int_to_string                 ; Convert to string
  mov esi, eax
  call os_print_string
  jmp .newline_or_not
.print_quote:                           ; If it's quoted text, print it
  mov esi, token
  call os_print_string
  jmp .newline_or_not
.print_string_var:
  mov esi, string_vars
  mov eax, 128
  mul ebx
  add esi, eax
  call os_print_string
  jmp .newline_or_not
.print_keyword:
  mov esi, token
  mov edi, chr_keyword
  call os_string_compare
  jc .is_chr
  mov edi, hex_keyword
  call os_string_compare
  jc .is_hex
  mov esi, err_syntax
  jmp error
.is_chr:
  call get_token
  cmp eax, VARIABLE
  jne .error
  mov eax, 0
  mov byte al, [token]
  call get_var
  mov ah, 0Eh
  int 10h
  jmp .newline_or_not
.is_hex:
  call get_token
  cmp eax, VARIABLE
  jne .error
  mov eax, 0
  mov byte al, [token]
  call get_var
  call os_print_2hex
  jmp .newline_or_not
.error:
  mov esi, err_syntax
  jmp error
.newline_or_not:
                                        ; We want to see if the command ends with ';' -- which means that
                                        ; we shouldn't print a newline after it finishes. So we store the
                                        ; current program location to pop ahead and see if there's the ';'
                                        ; character -- otherwise we put the program location back and resume
                                        ; the main loop
  mov dword eax, [prog]
  mov dword [.tmp_loc], eax
  call get_token
  cmp eax, UNKNOWN
  jne .ignore
  mov eax, 0
  mov al, [token]
  cmp al, ';'
  jne .ignore
  jmp mainloop                          ; And go back to interpreting the code!
.ignore:
  call os_print_newline
  mov dword eax, [.tmp_loc]
  mov dword [prog], eax
  jmp mainloop
.tmp_loc dd 0
; ------------------------------------------------------------------
; RAND
do_rand:
  call get_token
  cmp eax, VARIABLE
  jne .error
  mov byte al, [token]
  mov byte [.tmp], al
  call get_token
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int
  mov dword [.num1], eax
  call get_token
  cmp eax, NUMBER
  jne .error
  mov esi, token
  call os_string_to_int
  mov dword [.num2], eax
  mov dword eax, [.num1]
  mov dword ebx, [.num2]
  call os_get_random
  mov ebx, ecx
  mov eax, 0
  mov byte al, [.tmp]
  call set_var
  jmp mainloop
.tmp db 0
.num1 dd 0
.num2 dd 0
.error:
  mov esi, err_syntax
  jmp error
; ------------------------------------------------------------------
; REM
do_rem:
  mov dword esi, [prog]
  mov byte al, [esi]
  inc dword [prog]
  cmp al, 10                            ; Find end of line after REM
  jne do_rem
  jmp mainloop
; ------------------------------------------------------------------
; RETURN
do_return:
  mov eax, 0
  mov byte al, [gosub_depth]
  cmp al, 0
  jne .is_ok
  mov esi, err_return
  jmp error
.is_ok:
  mov esi, gosub_points
  add esi, eax                          ; Table is words (not bytes)
  add esi, eax
  lodsw
  mov dword [prog], eax
  dec byte [gosub_depth]
  jmp mainloop
; ------------------------------------------------------------------
;; SAVE
;do_save:
;  call get_token
;  cmp eax, QUOTE
;  je .is_quote
;  cmp eax, STRING_VAR
;  jne near .error
;  mov esi, string_vars
;  mov eax, 128
;  mul ebx
;  add esi, eax
;  jmp .get_position
;.is_quote:
;  mov esi, token
;.get_position:
;  mov edi, .tmp_filename
;  call os_string_copy
;  call get_token
;  cmp eax, VARIABLE
;  je .second_is_var
;  cmp eax, NUMBER
;  jne .error
;  mov esi, token
;  call os_string_to_int
;.set_data_loc:
;  mov dword [.data_loc], eax
;  call get_token
;  cmp eax, VARIABLE
;  je .third_is_var
;  cmp eax, NUMBER
;  jne .error
;  mov esi, token
;  call os_string_to_int
;.set_data_size:
;  mov dword [.data_size], eax
;  mov dword eax, .tmp_filename
;  mov dword ebx, [.data_loc]
;  mov dword ecx, [.data_size]
;  call os_write_file
;  jc .save_failure
;  mov eax, 0
;  mov byte al, 'R'
;  mov ebx, 0
;  call set_var
;  jmp mainloop
;.second_is_var:
;  mov eax, 0
;  mov byte al, [token]
;  call get_var
;  jmp .set_data_loc
;.third_is_var:
;  mov eax, 0
;  mov byte al, [token]
;  call get_var
;  jmp .set_data_size
;.save_failure:
;  mov eax, 0
;  mov byte al, 'R'
;  mov ebx, 1
;  call set_var
;  jmp mainloop
;.error:
;  mov esi, err_syntax
;  jmp error
;.filename_loc dd 0
;.data_loc dd 0
;.data_size dd 0
;.tmp_filename: times 15 db 0
; ------------------------------------------------------------------
; SERIAL
do_serial:
  call get_token
  mov esi, token
  mov edi, .on_cmd
  call os_string_compare
  jc .do_on_cmd
  mov edi, .send_cmd
  call os_string_compare
  jc .do_send_cmd
  mov edi, .rec_cmd
  call os_string_compare
  jc .do_rec_cmd
  jmp .error
.do_on_cmd:
  call get_token
  cmp eax, NUMBER
  je .do_on_cmd_ok
  jmp .error
.do_on_cmd_ok:
  mov esi, token
  call os_string_to_int
  cmp eax, 1200
  je .on_cmd_slow_mode
  cmp eax, 9600
  je .on_cmd_fast_mode
  jmp .error
.on_cmd_fast_mode:
  mov eax, 0
  call os_serial_port_enable
  jmp mainloop
.on_cmd_slow_mode:
  mov eax, 1
  call os_serial_port_enable
  jmp mainloop
.do_send_cmd:
  call get_token
  cmp eax, NUMBER
  je .send_number
  cmp eax, VARIABLE
  je .send_variable
  jmp .error
.send_number:
  mov esi, token
  call os_string_to_int
  call os_send_via_serial
  jmp mainloop
.send_variable:
  mov eax, 0
  mov byte al, [token]
  call get_var
  call os_send_via_serial
  jmp mainloop
.do_rec_cmd:
  call get_token
  cmp eax, VARIABLE
  jne .error
  mov byte al, [token]
  mov ecx, 0
  mov cl, al
  call os_get_via_serial
  mov ebx, 0
  mov bl, al
  mov al, cl
  call set_var
  jmp mainloop
.error:
  mov esi, err_syntax
  jmp error
.on_cmd db "ON", 0
.send_cmd db "SEND", 0
.rec_cmd db "REC", 0
; ------------------------------------------------------------------
; SOUND
do_sound:
  call get_token
  cmp eax, VARIABLE
  je .first_is_var
  mov esi, token
  call os_string_to_int
  jmp .done_first
.first_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
.done_first:
  call os_speaker_tone
  call get_token
  cmp eax, VARIABLE
  je .second_is_var
  mov esi, token
  call os_string_to_int
  jmp .finish
.second_is_var:
  mov eax, 0
  mov byte al, [token]
  call get_var
.finish:
  call os_pause
  call os_speaker_off
  jmp mainloop
; ------------------------------------------------------------------
; WAITKEY
do_waitkey:
  call get_token
  cmp eax, VARIABLE
  je .is_variable
  mov esi, err_syntax
  jmp error
.is_variable:
  mov eax, 0
  mov byte al, [token]
  push eax
  call os_wait_for_key
  cmp eax, 48E0h
  je .up_pressed
  cmp eax, 50E0h
  je .down_pressed
  cmp eax, 4BE0h
  je .left_pressed
  cmp eax, 4DE0h
  je .right_pressed
.store:
  mov ebx, 0
  mov bl, al
  pop eax
  call set_var
  jmp mainloop
.up_pressed:
  mov eax, 1
  jmp .store
.down_pressed:
  mov eax, 2
  jmp .store
.left_pressed:
  mov eax, 3
  jmp .store
.right_pressed:
  mov eax, 4
  jmp .store
; ==================================================================
; INTERNAL ROUTINES FOR INTERPRETER
; ------------------------------------------------------------------
; Get value of variable character specified in AL (eg 'A')
get_var:
  sub al, 65
  mov esi, variables
  add esi, eax
  add esi, eax
  lodsw
  ret
; ------------------------------------------------------------------
; Set value of variable character specified in AL (eg 'A')
; with number specified in EBX
set_var:
  mov ah, 0
  sub al, 65                            ; Remove ASCII codes before 'A'
  mov edi, variables                    ; Find position in table (of words)
  add edi, eax
  add edi, eax
  mov eax, ebx
  stosw
  ret
; ------------------------------------------------------------------
; Get token from current position in prog
get_token:
  mov dword esi, [prog]
  lodsb
  cmp al, 10
  je .newline
        CMP     AL, 13          ;allow for CRLF
        JE      .newline
  cmp al, ' '
  je .newline
  call is_number
  jc get_number_token
  cmp al, '"'
  je get_quote_token
  cmp al, 39                            ; Quote mark (')
  je get_char_token
  cmp al, '$'
  je near get_string_var_token
  jmp get_string_token
.newline:
  inc dword [prog]
  jmp get_token
get_number_token:
  mov dword esi, [prog]
  mov edi, token
.loop:
  lodsb
  cmp al, 10
  je .done
        CMP     AL, 13          ;allow for CRLF
        JE      .done
  cmp al, ' '
  je .done
  call is_number
  jc .fine
  mov esi, err_char_in_num
  jmp error
.fine:
  stosb
  inc dword [prog]
  jmp .loop
.done:
  mov al, 0                             ; Zero-terminate the token
  stosb
  mov eax, NUMBER                       ; Pass back the token type
  ret
get_char_token:
  inc dword [prog]                      ; Move past first quote (')
  mov dword esi, [prog]
  lodsb
  mov byte [token], al
  lodsb
  cmp al, 39                            ; Needs to finish with another quote
  je .is_ok
  mov esi, err_quote_term
  jmp error
.is_ok:
  inc dword [prog]
  inc dword [prog]
  mov eax, CHAR
  ret
get_quote_token:
  inc dword [prog]                      ; Move past first quote (") char
  mov dword esi, [prog]
  mov edi, token
.loop:
  lodsb
  cmp al, '"'
  je .done
  cmp al, 10
  je .error
  stosb
  inc dword [prog]
  jmp .loop
.done:
  mov al, 0                             ; Zero-terminate the token
  stosb
  inc dword [prog]                      ; Move past final quote
  mov eax, QUOTE                        ; Pass back token type
  ret
.error:
  mov esi, err_quote_term
  jmp error
get_string_var_token:
  lodsb
  mov ebx, 0                            ; If it's a string var, pass number of string in EBX
  mov bl, al
  sub bl, 49
  inc dword [prog]
  inc dword [prog]
  mov eax, STRING_VAR
  ret

get_string_token:
  mov dword esi, [prog]
  mov edi, token
.loop:
  lodsb
  cmp al, 10
  je .done
        CMP     AL, 13          ;allow for CRLF
        JE      .done
  cmp al, ' '
  je .done
  stosb
  inc dword [prog]
  jmp .loop
.done:
  mov al, 0                             ; Zero-terminate the token
  stosb
  mov eax, token
  call os_string_uppercase
  mov eax, token
  call os_string_length                 ; How long was the token?
  cmp eax, 1                            ; If 1 char, it's a variable or delimiter
  je .is_not_string
  mov esi, token                        ; If the token ends with ':', it's a label
  add esi, eax
  dec esi
  lodsb
  cmp al, ':'
  je .is_label
  mov eax, STRING                       ; Otherwise it's a general string of characters
  ret
.is_label:
  mov eax, LABEL
  ret
.is_not_string:
  mov byte al, [token]
  call is_letter
  jc .is_var
  mov eax, UNKNOWN
  ret
.is_var:
  mov eax, VARIABLE                     ; Otherwise probably a variable
  ret
; ------------------------------------------------------------------
; Set carry flag if AL contains ASCII number
is_number:
  cmp al, 48
  jl .not_number
  cmp al, 57
  jg .not_number
  stc
  ret
.not_number:
  clc
  ret
; ------------------------------------------------------------------
; Set carry flag if AL contains ASCII letter
is_letter:
  cmp al, 65
  jl .not_letter
  cmp al, 90
  jg .not_letter
  stc
  ret
.not_letter:
  clc
  ret
; ------------------------------------------------------------------
; Print error message and quit out
error:
  call os_print_newline
  call os_print_string                  ; Print error message
  call os_print_newline
  mov dword esp, [orig_stack]           ; Restore the stack to as it was when BASIC started
  ret                                   ; And finish
; ------------------------------------------------------------------
; os_string_length -- Return length of a string
; IN: EAX = string location
; OUT EAX = length (other regs preserved)
os_string_length:
  pusha
  mov ebx, eax                          ; Move location of string to EBX
  mov ecx, 0                            ; Counter
.more:
  cmp byte [ebx], 0                     ; Zero (end of string) yet?
  je .done
  inc ebx                               ; If not, keep adding
  inc ecx
  jmp .more
.done:
  mov dword [.tmp_counter], ecx         ; Store count before restoring other registers
  popa
  mov eax, [.tmp_counter]               ; Put count back into EAX before returning
  ret
.tmp_counter dd 0
; ------------------------------------------------------------------
; os_string_uppercase -- Convert zero-terminated string to upper case
; IN/OUT: EAX = string location
os_string_uppercase:
  pusha
  mov esi, eax                          ; Use ESI to access string
.more:
  cmp byte [esi], 0                     ; Zero-termination of string?
  je .done                              ; If so, quit
  cmp byte [esi], 'a'                   ; In the lower case A to Z range?
  jb .noatoz
  cmp byte [esi], 'z'
  ja .noatoz
  sub byte [esi], 20h                   ; If so, convert input char to upper case
  inc esi
  jmp .more
.noatoz:
  inc esi
  jmp .more
.done:
  popa
  ret
; ------------------------------------------------------------------
; os_string_copy -- Copy one string into another
; IN/OUT: ESI = source, EDI = destination (programmer ensure sufficient room)
os_string_copy:
  pusha
.more:
  mov al, [esi]                         ; Transfer contents (at least one byte terminator)
  mov [edi], al
  inc esi
  inc edi
  cmp byte al, 0                        ; If source string is empty, quit out
  jne .more
.done:
  popa
  ret
; ------------------------------------------------------------------
; os_string_compare -- See if two strings match
; IN: ESI = string one, EDI = string two
; OUT: carry set if same, clear if different
os_string_compare:
  pusha
.more:
  mov al, [esi]                         ; Retrieve string contents
  mov bl, [edi]
  cmp al, bl                            ; Compare characters at current location
  jne .not_same
  cmp al, 0                             ; End of first string? Must also be end of second
  je .terminated
  inc esi
  inc edi
  jmp .more
.not_same:                              ; If unequal lengths with same beginning, the byte
  popa                                  ; comparison fails at shortest string terminator
  clc                                   ; Clear carry flag
  ret
.terminated:                            ; Both strings terminated at the same position
  popa
  stc                                   ; Set carry flag
  ret
; ------------------------------------------------------------------
; os_string_to_int -- Convert decimal string to integer value
; IN: ESI = string location (max 10 chars, up to '4294967295')
; OUT: EAX = number
os_string_to_int:
  pusha
  mov eax, esi                          ; First, get length of string
  call os_string_length
  add esi, eax                          ; Work from rightmost char in string
  dec esi
  mov ecx, eax                          ; Use string length as counter
  mov ebx, 0                            ; EBX will be the final number
  mov eax, 0
                                        ; As we move left in the string, each char is a bigger multiple. The
                                        ; right-most character is a multiple of 1, then next (a char to the
                                        ; left) a multiple of 10, then 100, then 1,000, and the final (and
                                        ; leftmost char) in a five-char number would be a multiple of 10,000
  mov dword [.multiplier], 1            ; Start with multiples of 1
.loop:
  mov eax, 0
  mov byte al, [esi]                    ; Get character
  sub al, 48                            ; Convert from ASCII to real number
  mul dword [.multiplier]               ; Multiply by our multiplier
  add ebx, eax                          ; Add it to EBX
  push eax                              ; Multiply our multiplier by 10 for next char
  mov dword eax, [.multiplier]
  mov edx, 10
  mul edx
  mov dword [.multiplier], eax
  pop eax
  dec ecx                               ; Any more chars?
  cmp ecx, 0
  je .finish
  dec esi                               ; Move back a char in the string
  jmp .loop
.finish:
  mov dword [.tmp], ebx
  popa
  mov dword eax, [.tmp]
  ret
.multiplier dd 0
.tmp dd 0
; ------------------------------------------------------------------
; os_int_to_string -- Convert unsigned integer to string
; IN: EAX = signed int
; OUT: EAX = string location
os_int_to_string:
  pusha
  mov ecx, 0
  mov ebx, 10                           ; Set EBX 10, for division and mod
  mov edi, .t                           ; Get our pointer ready
.push:
  mov edx, 0
  div ebx                               ; Remainder in EDX, quotient in EAX
  inc ecx                               ; Increase pop loop counter
  push edx                              ; Push remainder, so as to reverse order when popping
  test eax, eax                         ; Is quotient zero?
  jnz .push                             ; If not, loop again
.pop:
  pop edx                               ; Pop off values in reverse order, and add 48 to make them digits
  add dl, '0'                           ; And save them in the string, increasing the pointer each time
  mov [edi], dl
  inc edi
  dec ecx
  jnz .pop
  mov byte [edi], 0                     ; Zero-terminate string
  popa
  mov eax, .t                           ; Return location of string
  ret
.t: times 7 db 0
; ------------------------------------------------------------------
; os_print_string -- Displays text
; IN: ESI = message location (zero-terminated string)
; OUT: Nothing (registers preserved)
os_print_string:
  pusha
  mov ah, 0Eh                           ; int 10h teletype function
.repeat1:
  lodsb                                 ; Get char from string
  cmp al, 0
  je .done                              ; If char is zero, end of string
  int 10h                               ; Otherwise, print it
  jmp .repeat1                           ; And move on to next char
.done:
  popa
  ret
; ------------------------------------------------------------------
; os_clear_screen -- Clears the screen to background
; IN/OUT: Nothing (registers preserved)
os_clear_screen:
  pusha
  mov edx, 0                            ; Position cursor at top-left
  call os_move_cursor
  mov ah, 6                             ; Scroll full-screen
  mov al, 0                             ; Normal white on black
  mov bh, 7                             ;
  mov ecx, 0                            ; Top-left
  mov dh, 24                            ; Bottom-right
  mov dl, 79
  int 10h
  popa
  ret
; ------------------------------------------------------------------
; os_move_cursor -- Moves cursor in text mode
; IN: DH, DL = row, column; OUT: Nothing (registers preserved)
os_move_cursor:
  pusha
  mov bh, 0
  mov ah, 2
  int 10h                               ; BIOS interrupt to move cursor
  popa
  ret
; ------------------------------------------------------------------
; os_get_cursor_pos -- Return position of text cursor
; OUT: DH, DL = row, column
os_get_cursor_pos:
  pusha
  mov bh, 0
  mov ah, 3
  int 10h                               ; BIOS interrupt to get cursor position
  mov [.tmp], edx
  popa
  mov edx, [.tmp]
  ret
.tmp dd 0
; ------------------------------------------------------------------
; os_show_cursor -- Turns on cursor in text mode
; IN/OUT: Nothing
os_show_cursor:
  pusha
  mov ch, 6
  mov cl, 7
  mov ah, 1
  mov al, 3
  int 10h
  popa
  ret
; ------------------------------------------------------------------
; os_hide_cursor -- Turns off cursor in text mode
; IN/OUT: Nothing
os_hide_cursor:
  pusha
  mov ch, 32
  mov ah, 1
  mov al, 3                             ; Must be video mode for buggy BIOSes!
  int 10h
  popa
  ret
; ------------------------------------------------------------------
; os_draw_block -- Render block of specified colour
; IN: BL/DL/DH/ESI/EDI = colour/start X pos/start Y pos/width/finish Y pos
os_draw_block:
  pusha
.more:
  call os_move_cursor                   ; Move to block starting position
  mov ah, 09h                           ; Draw colour section
  mov bh, 0
  mov ecx, esi
  mov al, ' '
  int 10h
  inc dh                                ; Get ready for next line
  mov eax, 0
  mov al, dh                            ; Get current Y position into DL
  cmp eax, edi                          ; Reached finishing point (DI)?
  jne .more                             ; If not, keep drawing
  popa
  ret
; ------------------------------------------------------------------
; os_print_newline -- Reset cursor to start of next line
; IN/OUT: Nothing (registers preserved)
os_print_newline:
  pusha
  mov ah, 0Eh                           ; BIOS output char code
  mov al, 13
  int 10h
  mov al, 10
  int 10h
  popa
  ret
; ------------------------------------------------------------------
; os_dialog_box -- Print dialog box in middle of screen, with button(s)
; IN: EAX, EBX, ECX = string locations (set registers to 0 for no display)
; IN: EDX = 0 for single 'OK' dialog, 1 for two-button 'OK' and 'Cancel'
; OUT: If two-button mode, EAX = 0 for OK and 1 for cancel
; NOTE: Each string is limited to 40 characters
os_dialog_box:
  pusha
  mov [.tmp], edx
  call os_hide_cursor
  mov dh, 9                             ; First, draw red background box
  mov dl, 19
.redbox:                                ; Loop to draw all lines of box
  call os_move_cursor
  pusha
  mov ah, 09h
  mov bh, 0
  mov ecx, 42
  mov bl, 01001111b                     ; White on red
  mov al, ' '
  int 10h
  popa
  inc dh
  cmp dh, 16
  je .boxdone
  jmp .redbox
.boxdone:
  cmp eax, 0                            ; Skip string params if zero
  je .no_first_string
  mov dl, 20
  mov dh, 10
  call os_move_cursor
  mov esi, eax                          ; First string
  call os_print_string
.no_first_string:
  cmp ebx, 0
  je .no_second_string
  mov dl, 20
  mov dh, 11
  call os_move_cursor
  mov esi, ebx                          ; Second string
  call os_print_string
.no_second_string:
  cmp ecx, 0
  je .no_third_string
  mov dl, 20
  mov dh, 12
  call os_move_cursor
  mov esi, ecx                          ; Third string
  call os_print_string
.no_third_string:
  mov edx, [.tmp]
  cmp edx, 0
  je .one_button
  cmp edx, 1
  je .two_button
.one_button:
  mov bl, 11110000b                     ; Black on white
  mov dh, 14
  mov dl, 35
  mov esi, 8
  mov edi, 15
  call os_draw_block
  mov dl, 38                            ; OK button, centred at bottom of box
  mov dh, 14
  call os_move_cursor
  mov esi, .ok_button_string
  call os_print_string
  jmp .one_button_wait
.two_button:
  mov bl, 11110000b                     ; Black on white
  mov dh, 14
  mov dl, 27
  mov esi, 8
  mov edi, 15
  call os_draw_block
  mov dl, 30                            ; OK button
  mov dh, 14
  call os_move_cursor
  mov esi, .ok_button_string
  call os_print_string
  mov dl, 44                            ; Cancel button
  mov dh, 14
  call os_move_cursor
  mov esi, .cancel_button_string
  call os_print_string
  mov ecx, 0                            ; Default button = 0
  jmp .two_button_wait
.one_button_wait:
  call os_wait_for_key
  cmp al, 13                            ; Wait for enter key (13) to be pressed
  jne .one_button_wait
  call os_show_cursor
  popa
  ret
.two_button_wait:
  call os_wait_for_key
  cmp ah, 75                            ; Left cursor key pressed?
  jne .noleft
  mov bl, 11110000b                     ; Black on white
  mov dh, 14
  mov dl, 27
  mov esi, 8
  mov edi, 15
  call os_draw_block
  mov dl, 30                            ; OK button
  mov dh, 14
  call os_move_cursor
  mov esi, .ok_button_string
  call os_print_string
  mov bl, 01001111b                     ; White on red for cancel button
  mov dh, 14
  mov dl, 42
  mov esi, 9
  mov edi, 15
  call os_draw_block
  mov dl, 44                            ; Cancel button
  mov dh, 14
  call os_move_cursor
  mov esi, .cancel_button_string
  call os_print_string
  mov ecx, 0                            ; And update result we'll return
  jmp .two_button_wait
.noleft:
  cmp ah, 77                            ; Right cursor key pressed?
  jne .noright
  mov bl, 01001111b                     ; Black on white
  mov dh, 14
  mov dl, 27
  mov esi, 8
  mov edi, 15
  call os_draw_block
  mov dl, 30                            ; OK button
  mov dh, 14
  call os_move_cursor
  mov esi, .ok_button_string
  call os_print_string
  mov bl, 11110000b                     ; White on red for cancel button
  mov dh, 14
  mov dl, 43
  mov esi, 8
  mov edi, 15
  call os_draw_block
  mov dl, 44                            ; Cancel button
  mov dh, 14
  call os_move_cursor
  mov esi, .cancel_button_string
  call os_print_string
  mov ecx, 1                            ; And update result we'll return
  jmp .two_button_wait
.noright:
  cmp al, 13                            ; Wait for enter key (13) to be pressed
  jne .two_button_wait
  call os_show_cursor
  mov [.tmp], ecx                       ; Keep result after restoring all regs
  popa
  mov eax, [.tmp]
  ret
.ok_button_string db 'OK', 0
.cancel_button_string db 'Cancel', 0
.ok_button_noselect db ' OK ', 0
.cancel_button_noselect db ' Cancel ', 0
.tmp dd 0
; ------------------------------------------------------------------
; os_print_digit -- Displays contents of AX as a single digit
; Works up to base 37, ie digits 0-Z
; IN: EAX = "digit" to format and print
os_print_digit:
  pusha
  cmp eax, 9                            ; There is a break in ASCII table between 9 and A
  jle .digit_format
  add eax, 'A'-'9'-1                    ; Correct for the skipped punctuation
.digit_format:
  add eax, '0'                          ; 0 will display as '0', etc.
  mov ah, 0Eh                           ; May modify other registers
  int 10h
  popa
  ret
; ------------------------------------------------------------------
; os_print_1hex -- Displays low nibble of AL in hex format
; IN: AL = number to format and print
os_print_1hex:
  pusha
  and eax, 0Fh                          ; Mask off data to display
  call os_print_digit
  popa
  ret
; ------------------------------------------------------------------
; os_print_2hex -- Displays AL in hex format
; IN: AL = number to format and print
os_print_2hex:
  pusha
  push eax                              ; Output high nibble
  shr eax, 4
  call os_print_1hex
  pop eax                               ; Output low nibble
  call os_print_1hex
  popa
  ret
; ------------------------------------------------------------------
; os_input_string -- Take string from keyboard entry
; IN/OUT: EAX = location of string, other regs preserved
; (Location will contain up to 255 characters, zero-terminated)
os_input_string:
  pusha
  mov edi, eax                          ; EDI is where we'll store input (buffer)
  mov ecx, 0                            ; Character received counter for backspace
.more:                                  ; Now onto string getting
  call os_wait_for_key
  cmp al, 13                            ; If Enter key pressed, finish
  je .done
  cmp al, 8                             ; Backspace pressed?
  je .backspace                         ; If not, skip following checks
  cmp al, ' '                           ; In ASCII range (32 - 126)?
  jb .more                              ; Ignore most non-printing characters
  cmp al, '~'
  ja .more
  jmp .nobackspace
.backspace:
  cmp ecx, 0                            ; Backspace at start of string?
  je .more                              ; Ignore it if so
  call os_get_cursor_pos                ; Backspace at start of screen line?
  cmp dl, 0
  je .backspace_linestart
  pusha
  mov ah, 0Eh                           ; If not, write space and move cursor back
  mov al, 8
  int 10h                               ; Backspace twice, to clear space
  mov al, 32
  int 10h
  mov al, 8
  int 10h
  popa
  dec edi                               ; Character position will be overwritten by new
                                        ; character or terminator at end
  dec ecx                               ; Step back counter
  jmp .more
.backspace_linestart:
  dec dh                                ; Jump back to end of previous line
  mov dl, 79
  call os_move_cursor
  mov al, ' '                           ; Print space there
  mov ah, 0Eh
  int 10h
  mov dl, 79                            ; And jump back before the space
  call os_move_cursor
  dec edi                               ; Step back position in string
  dec ecx                               ; Step back counter
  jmp .more
.nobackspace:
  pusha
  mov ah, 0Eh                           ; Output entered, printable character
  int 10h
  popa
  stosb                                 ; Store character in designated buffer
  inc ecx                               ; Characters processed += 1
  cmp ecx, 254                          ; Make sure we don't exhaust buffer
  jae near .done
  jmp near .more                        ; Still room for more
.done:
  mov eax, 0
  stosb
  popa
  ret
; ------------------------------------------------------------------
; os_wait_for_key -- Waits for keypress and returns key
; IN: Nothing; OUT: EAX = key pressed, other regs preserved
os_wait_for_key:
  pusha
  mov eax, 0
  mov ah, 10h                           ; BIOS call to wait for key
  int 16h
  mov [.tmp_buf], eax                   ; Store resulting keypress
  popa                                  ; But restore all other regs
  mov eax, [.tmp_buf]
  ret
.tmp_buf dd 0
; ------------------------------------------------------------------
; os_check_for_key -- Scans keyboard for input, but doesn't wait
; IN: Nothing; OUT: EAX = 0 if no key pressed, otherwise scan code
os_check_for_key:
  pusha
  mov eax, 0
  mov ah, 1                             ; BIOS call to check for key
  int 16h
  jz .nokey                             ; If no key, skip to end
  mov eax, 0                            ; Otherwise get it from buffer
  int 16h
  mov [.tmp_buf], eax                   ; Store resulting keypress
  popa                                  ; But restore all other regs
  mov eax, [.tmp_buf]
  ret
.nokey:
  popa
  mov eax, 0                            ; Zero result if no key pressed
  ret
.tmp_buf dd 0
; ------------------------------------------------------------------
; os_port_byte_out -- Send byte to a port
; IN: EDX = port address, AL = byte to send
os_port_byte_out:
  pusha
  out dx, al
  popa
  ret
; ------------------------------------------------------------------
; os_port_byte_in -- Receive byte from a port
; IN: EDX = port address
; OUT: AL = byte from port
os_port_byte_in:
  pusha
  in al, dx
  mov dword [.tmp], eax
  popa
  mov eax, [.tmp]
  ret
.tmp dd 0
; ------------------------------------------------------------------
; os_serial_port_enable -- Set up the serial port for transmitting data
; IN: EAX = 0 for normal mode (9600 baud), or 1 for slow mode (1200 baud)
os_serial_port_enable:
  pusha
  mov edx, 0                            ; Configure serial port 1
  cmp eax, 1
  je .slow_mode
  mov ah, 0
  mov al, 11100011b                     ; 9600 baud, no parity, 8 data bits, 1 stop bit
  jmp .finish
.slow_mode:
  mov ah, 0
  mov al, 10000011b                     ; 1200 baud, no parity, 8 data bits, 1 stop bit
.finish:
  int 14h
  popa
  ret
; ------------------------------------------------------------------
; os_send_via_serial -- Send a byte via the serial port
; IN: AL = byte to send via serial; OUT: AH = Bit 7 clear on success
os_send_via_serial:
  pusha
  mov ah, 01h
  mov edx, 0                            ; COM1
  int 14h
  mov [.tmp], eax
  popa
  mov eax, [.tmp]
  ret
.tmp dd 0
; ------------------------------------------------------------------
; os_get_via_serial -- Get a byte from the serial port
; OUT: AL = byte that was received; OUT: AH = Bit 7 clear on success
os_get_via_serial:
  pusha
  mov ah, 02h
  mov edx, 0                            ; COM1
  int 14h
  mov [.tmp], eax
  popa
  mov eax, [.tmp]
  ret
.tmp dd 0
; ------------------------------------------------------------------
; os_seed_random -- Seed the random number generator based on clock
; IN: Nothing; OUT: Nothing (registers preserved)
os_seed_random:
  push ebx
  push eax
  mov ebx, 0
  mov al, 0x02                          ; Minute
  out 0x70, al
  in al, 0x71
  mov bl, al
  shl ebx, 8
  mov al, 0                             ; Second
  out 0x70, al
  in al, 0x71
  mov bl, al
  mov dword [os_random_seed], ebx       ; Seed will be something like 0x4435 (if it
                                        ; were 44 minutes and 35 seconds after the hour)
  pop eax
  pop ebx
  ret
  os_random_seed dd 0
; ------------------------------------------------------------------
; os_get_random -- Return a random integer between low and high (inclusive)
; IN: EAX = low integer, EBX = high integer
; OUT: ECX = random integer
os_get_random:
  push edx
  push ebx
  push eax
  sub ebx, eax                          ; We want a number between 0 and (high-low)
  call .generate_random
  mov edx, ebx
  add edx, 1
  mul edx
  mov ecx, edx
  pop eax
  pop ebx
  pop edx
  add ecx, eax                          ; Add the low offset back
  ret
.generate_random:
  push edx
  push ebx
  mov eax, [os_random_seed]
  mov edx, 7383h                        ; The magic number (random.org)
  mul edx                               ; EDX:EAX = EAX * EDX
  mov [os_random_seed], eax
  pop ebx
  pop edx
  ret
; ------------------------------------------------------------------
; os_speaker_tone -- Generate PC speaker tone (call os_speaker_off to turn off)
; IN: EAX = note frequency; OUT: Nothing (registers preserved)
os_speaker_tone:
  pusha
  mov ecx, eax                          ; Store note value for now
  mov al, 182
  out 43h, al
  mov eax, ecx                          ; Set up frequency
  out 42h, al
  mov al, ah
  out 42h, al
  in al, 61h                            ; Switch PC speaker on
  or al, 03h
  out 61h, al
  popa
  ret
; ------------------------------------------------------------------
; os_speaker_off -- Turn off PC speaker
; IN/OUT: Nothing (registers preserved)
os_speaker_off:
  pusha
  in al, 61h
  and al, 0FCh
  out 61h, al
  popa
  ret
; ------------------------------------------------------------------
; os_pause -- Delay execution for specified 110ms chunks
; IN: EAX = 100 millisecond chunks to wait (max delay is 32767,
; which multiplied by 55ms = 1802 seconds = 30 minutes)
os_pause:
  pusha
  cmp eax, 0
  je .time_up                           ; If delay = 0 then bail out
  mov ecx, 0
  mov [.counter_var], ecx               ; Zero the counter variable
  mov ebx, eax
  mov eax, 0
  mov al, 2                             ; 2 * 55ms = 110mS
  mul ebx                               ; Multiply by number of 110ms chunks required
  mov [.orig_req_delay], eax            ; Save it
  mov ah, 0
  int 1Ah                               ; Get tick count
  mov [.prev_tick_count], edx           ; Save it for later comparison
.checkloop:
  mov ah,0
  int 1Ah                               ; Get tick count again
  cmp [.prev_tick_count], edx           ; Compare with previous tick count
  jne .up_date                          ; If it's changed check it
  jmp .checkloop                        ; Otherwise wait some more
.time_up:
  popa
  ret
.up_date:
  mov eax, [.counter_var]               ; Inc counter_var
  inc eax
  mov [.counter_var], eax
  cmp eax, [.orig_req_delay]            ; Is counter_var = required delay?
  jge .time_up                          ; Yes, so bail out
  mov [.prev_tick_count], edx           ; No, so update .prev_tick_count
  jmp .checkloop                        ; And go wait some more
.orig_req_delay dd 0
.counter_var dd 0
.prev_tick_count dd 0


                                        ; Error messages text...
err_char_in_num db "Error: unexpected character in number", 0
err_quote_term db "Error: quoted string or character not terminated correctly", 0
err_print_type db "Error: PRINT command not followed by quoted text or variable", 0
err_cmd_unknown db "Error: unknown command", 0
err_goto_notlabel db "Error: GOTO or GOSUB not followed by label", 0
err_label_notfound db "Error: GOTO or GOSUB label not found", 0
err_return db "Error: RETURN without GOSUB", 0
err_nest_limit db "Error: FOR or GOSUB nest limit exceeded", 0
err_next db "Error: NEXT without FOR", 0
err_syntax db "Error: syntax error", 0
; ==================================================================
; DATA SECTION
orig_stack dd 0                         ; Original stack location when BASIC started
prog dd 0                               ; Pointer to current location in BASIC code
prog_end dd 0                           ; Pointer to final byte of BASIC code
load_point dd 0
token_type db 0                         ; Type of last token read (eg NUMBER, VARIABLE)
token: times 255 db 0                   ; Storage space for the token
variables: times 26 dd 0                ; Storage space for variables A to Z
for_variables: times 26 dd 0            ; Storage for FOR loops
for_code_points: times 26 dd 0          ; Storage for code positions where FOR loops start
alert_cmd db "ALERT", 0
call_cmd db "CALL", 0
cls_cmd db "CLS", 0
cursor_cmd db "CURSOR", 0
curschar_cmd db "CURSCHAR", 0
end_cmd db "END", 0
for_cmd db "FOR", 0
gosub_cmd db "GOSUB", 0
goto_cmd db "GOTO", 0
getkey_cmd db "GETKEY", 0
if_cmd db "IF", 0
input_cmd db "INPUT", 0
load_cmd db "LOAD", 0
move_cmd db "MOVE", 0
next_cmd db "NEXT", 0
pause_cmd db "PAUSE", 0
peek_cmd db "PEEK", 0
poke_cmd db "POKE", 0
port_cmd db "PORT", 0
print_cmd db "PRINT", 0
rem_cmd db "REM", 0
rand_cmd db "RAND", 0
return_cmd db "RETURN", 0
save_cmd db "SAVE", 0
serial_cmd db "SERIAL", 0
sound_cmd db "SOUND", 0
waitkey_cmd db "WAITKEY", 0
then_keyword db "THEN", 0
chr_keyword db "CHR", 0
hex_keyword db "HEX", 0
progstart_keyword db "PROGSTART", 0
ramstart_keyword db "RAMSTART", 0
gosub_depth db 0
gosub_points: times 10 dd 0             ; Points in code to RETURN to
string_vars: times 1024 db 0            ; 8 * 128 byte strings
; ------------------------------------------------------------------
basic_prog:
 DB ' CLS',13,10
 DB ' PRINT " Please type your name: " ;',13,10
 DB ' INPUT $N',13,10
 DB ' PRINT ""',13,10
 DB ' PRINT " Hello " ;',13,10
 DB ' PRINT $N ;',13,10
 DB ' PRINT ", welcome to MikeOS Basic!"',13,10
 DB ' PRINT ""',13,10
 DB ' PRINT " It supports FOR...NEXT loops and simple integer maths..."',13,10
 DB ' PRINT ""',13,10
 DB ' FOR I = 1 TO 15',13,10
 DB ' J = I * I',13,10
 DB ' K = J * I',13,10
 DB ' L = K * I',13,10
 DB ' PRINT I ;',13,10
 DB ' PRINT "        " ;',13,10
 DB ' PRINT J ;',13,10
 DB ' PRINT "        " ;',13,10
 DB ' PRINT K ;',13,10
 DB ' PRINT "        " ;',13,10
 DB ' PRINT L',13,10
 DB ' NEXT I',13,10
 DB ' PRINT ""',13,10
 DB ' PRINT " ...and IF...THEN and GOSUB and lots of other stuff. Bye!"',13,10
 DB ' END',13,10
