### -*- mode: asm -*-
### Ngaro Virtual Machine for 32-bit Linux systems
### Peter Salvi, 2011

### Compile with:
###   as ngaro.s -o ngaro.o   (add --gstabs for debug info)
###   ld ngaro.o -o ngaro

### Tests:
### - passes core
### - passes io_files
### - passes io_output
### - passes vocabs
        
### Parameters
        
        .equ DATA_STACK_DEPTH, 128
        .equ RETURN_STACK_DEPTH, 1024
        .equ MEMORY_SIZE, 1000000
        .equ MAX_STRING_LENGTH, 1024
        .equ MAX_OPEN_FILES, 8

### Allocations
        
        .section .bss

        .lcomm stats, 64        # see asm-generic/stat.h
        .lcomm termios_old, 36  # see asm-generic/termbits.h
        .lcomm termios, 36
        .lcomm buffer, 1
        .lcomm winsize, 8       # see asm-generic/termios.h
        .lcomm filename, 4
        
        .lcomm data, DATA_STACK_DEPTH*4
        .lcomm return, RETURN_STACK_DEPTH*4
        .lcomm ports, 5*4
        .lcomm memory, MEMORY_SIZE*4
        .lcomm str, MAX_STRING_LENGTH
        .lcomm input, MAX_OPEN_FILES*4

### Data

        .section .data

arg_error:
        .byte 19
        .ascii "Usage: ngaro image\n"
file_error:
        .byte 23
        .ascii "Cannot open image file\n"
stack_error:
        .byte 16
        .ascii "Stack underflow\n"
clear_screen:
        .byte 10
        .ascii "\033[2J\033[1;1H"
instr:
        .long ins_nop, ins_lit, ins_dup, ins_drop, ins_swap, ins_push
        .long ins_pop, ins_loop, ins_jump, ins_return, ins_lt_jump
        .long ins_gt_jump, ins_ne_jump, ins_eq_jump, ins_fetch, ins_store
        .long ins_add, ins_subtract, ins_multiply, ins_divmod, ins_and
        .long ins_or, ins_xor, ins_shl, ins_shr, ins_zero_exit
        .long ins_inc, ins_dec, ins_in, ins_out, ins_wait
file_modes:
        .long 0, 01101, 02101, 2 # r, trunc/create/w, append/create/w, r/w
input_id:
        .long 0
        
### Code

        .section .text
        .globl _start
_start:
        ## Save the original IO settings
        movl $54, %eax          # ioctl
        movl $0, %ebx           # stdin
        movl $0x5401, %ecx      # tcgets request
        leal termios_old, %edx  # save original termios struct
        int $0x80

        ## Check command line arguments
        popl %eax
        cmpl $2, %eax
        je argc_ok

        ## Wrong number of arguments
        leal arg_error, %ecx
        call write_str
        jmp quit

argc_ok:
        popl %eax               # program name
        popl %ebx               # image file name
        movl %ebx, filename

        ## Open file
        movl $5, %eax           # open
        movl $0, %ecx           # read only
        movl $0644, %edx        # permissions
        int $0x80
        testl %eax, %eax
        jge open_ok

        ## Cannot open file
        leal file_error, %ecx
        call write_str
        jmp quit

open_ok:
        movl %eax, %ebx

        ## Get file size
        movl $108, %eax         # fstat
        leal stats, %ecx        # stat record
        int $0x80

        ## Read file into memory
        movl $3, %eax           # read
        leal memory, %ecx       # buffer
        movl stats+20, %edx     # count
        int $0x80

        ## Close file
        movl $6, %eax           # close
        int $0x80

### Enter non-canonical mode (do not wait for enter, no echo)

        ## Get IO settings for modification
        movl $54, %eax          # ioctl
        movl $0, %ebx           # stdin
        movl $0x5401, %ecx      # tcgets request
        leal termios, %edx      # termios struct to be modified
        int $0x80

        ## Clear the flags
        movl $0xfff5, %edi      # clear echo & icanon flag
        andl %edi, termios+12

        ## Set the new IO mode
        movl $54, %eax          # ioctl
        movl $0, %ebx           # stdin
        movl $0x5402, %ecx      # tcsets request
        leal termios, %edx      # new termios struct
        int $0x80
        
### Start the VM

vm_start:
        movl $0, %ebp           # ip
        movl $-1, %edi          # data stack pointer
        movl $-1, %esi          # return stack pointer
        movl $0, input          # stdin is the primary input
        movl $0, input_id       # and it is the default
vm_loop:
        cmp $(MEMORY_SIZE/4), %ebp
        jge vm_start            # restart the VM if ip is out of range
        movl memory(,%ebp,4), %eax
        cmpl $30, %eax
        jg implicit_call
        call *instr(,%eax,4)
        incl %ebp               # next instruction
        jmp vm_loop
implicit_call:
        incl %esi
        movl %ebp, return(,%esi,4)
        movl %eax, %ebp
        jmp vm_loop
        
quit:
        ## Restore canonical mode
        movl $54, %eax          # ioctl
        movl $0, %ebx           # stdin
        movl $0x5402, %ecx      # tcsets request
        leal termios_old, %edx  # original termios struct
        int $0x80
        
        ## Quit
        movl $1, %eax           # exit
        movl $0, %ebx           # exit code
        int $0x80

### Procedures
        
write_str:                      # %ecx: string address (1st byte: string length)
        movl $4, %eax           # write
        movl $1, %ebx           # stdout
        xorl %edx, %edx
        movb (%ecx), %dl        # length
        incl %ecx               # string
        int $0x80
        ret

stack_underflow:
        leal stack_error, %ecx
        call write_str
        ret

convert_str:                    # stack: string in memory; result in str
        xorl %eax, %eax
        movl data(,%edi,4), %ebx
        xorl %ecx, %ecx
convert_loop:
        movb memory(,%ebx,4), %al
        movb %al, str(%ecx)
        incl %ebx
        incl %ecx
        testb %al, %al
        jne convert_loop
        ret

revert_str:                     # stack: destination in memory; source in %ebx
        xorl %eax, %eax
        movl data(,%edi,4), %ecx
revert_loop:
        movb (%ebx), %al
        movb %al, memory(,%ecx,4)
        incl %ebx
        incl %ecx
        testb %al, %al
        jne revert_loop
        ret

get_window_size:
        movl $54, %eax          # ioctl
        movl $0, %ebx           # stdin
        movl $0x5413, %ecx      # tiocgwinsz request
        leal winsize, %edx      # winsize struct
        int $0x80
        ret
        
### Some macros for convenience
        
.macro check_data_1
        testl %edi, %edi
        jl stack_underflow
.endm

.macro check_data_2
        testl %edi, %edi
        jle stack_underflow
.endm

.macro check_return_1
        testl %esi, %esi
        jl stack_underflow
.endm

.macro jump_if test
        check_data_2
        subl $2, %edi
        movl data+4(,%edi,4), %eax
        cmpl %eax, data+8(,%edi,4)
        \test ins_jump_\test
        incl %ebp
        ret
ins_jump_\test:
        movl memory+4(,%ebp,4), %ebp
        decl %ebp
        ret
.endm

.macro port_neq n branch k=0
        movl ports+\n*4, %eax
        cmpl $\k, %eax
        jne \branch
.endm

.macro port_eq n branch k=0
        movl ports+\n*4, %eax
        cmpl $\k, %eax
        je \branch
.endm

.macro set_port n k
        movl \k, ports+\n*4
.endm

### Instructions
        
ins_nop:
        ret

ins_lit:
        incl %ebp
        movl memory(,%ebp,4), %eax
        incl %edi
        movl %eax, data(,%edi,4)
        ret

ins_dup:
        check_data_1
        movl data(,%edi,4), %eax
        incl %edi
        movl %eax, data(,%edi,4)
        ret

ins_drop:
        check_data_1
        decl %edi
        ret

ins_swap:
        check_data_2
        movl data-4(,%edi,4), %eax
        xchgl data(,%edi,4), %eax
        movl %eax, data-4(,%edi,4)
        ret

ins_push:
        check_data_1
        incl %esi
        movl data(,%edi,4), %eax
        movl %eax, return(,%esi,4)
        decl %edi
        ret

ins_pop:
        check_return_1
        incl %edi
        movl return(,%esi,4), %eax
        movl %eax, data(,%edi,4)
        decl %esi
        ret

ins_loop:
        check_data_1
        decl data(,%edi,4)      # this is how retro.c works; not in the docs
        jle ins_loop_branch
        movl memory+4(,%ebp,4), %ebp
        decl %ebp
        ret
ins_loop_branch:
        incl %ebp
        decl %edi
        ret

ins_jump:
        movl memory+4(,%ebp,4), %ebp
        decl %ebp
        ret

ins_return:
        check_return_1
        movl return(,%esi,4), %ebp
        decl %esi
        ret

ins_lt_jump:
        jump_if jl

ins_gt_jump:
        jump_if jg
        
ins_ne_jump:
        jump_if jne

ins_eq_jump:
        jump_if je

ins_fetch:
        check_data_1
        movl data(,%edi,4), %eax
        movl memory(,%eax,4), %ebx
        movl %ebx, data(,%edi,4)
        ret

ins_store:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        movl data(,%edi,4), %ebx
        decl %edi
        movl %ebx, memory(,%eax,4)
        ret

ins_add:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        addl %eax, data(,%edi,4)
        ret

ins_subtract:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        subl %eax, data(,%edi,4)
        ret

ins_multiply:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        imull data(,%edi,4)
        movl %eax, data(,%edi,4)
        ret

ins_divmod:
        check_data_2
        movl data-4(,%edi,4), %eax
        xorl %edx, %edx
        testl %eax, %eax
        jl dividend_negative
        idivl data(,%edi,4)
        movl %edx, data-4(,%edi,4)
        movl %eax, data(,%edi,4)
        ret
dividend_negative:
        negl %eax
        idivl data(,%edi,4)
        negl %eax
        negl %edx
        movl %edx, data-4(,%edi,4)
        movl %eax, data(,%edi,4)
        ret

ins_and:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        andl %eax, data(,%edi,4)
        ret

ins_or:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        orl %eax, data(,%edi,4)
        ret

ins_xor:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        xorl %eax, data(,%edi,4)
        ret

ins_shl:
        check_data_2
        movl data(,%edi,4), %ecx
        decl %edi
        shll %cl, data(,%edi,4)
        ret

ins_shr:
        check_data_2
        movl data(,%edi,4), %ecx
        decl %edi
        shrl %cl, data(,%edi,4)
        ret

ins_zero_exit:
        check_data_1
        movl data(,%edi,4), %eax
        testl %eax, %eax
        je ins_zero_exit_branch
        ret
ins_zero_exit_branch:
        decl %edi
        movl return(,%esi,4), %ebp
        decl %esi
        ret

ins_inc:
        check_data_1
        incl data(,%edi,4)
        ret

ins_dec:
        check_data_1
        decl data(,%edi,4)
        ret

ins_in:
        check_data_1
        movl data(,%edi,4), %eax
        movl ports(,%eax,4), %ebx
        movl %ebx, data(,%edi,4)
        movl $0, ports(,%eax,4)
        ret

ins_out:
        check_data_2
        movl data(,%edi,4), %eax
        decl %edi
        movl data(,%edi,4), %ebx
        decl %edi
        movl %ebx, ports(,%eax,4)
        set_port 0 $0
        ret

ins_wait:
        port_neq 0 no_port

port1:  ## Input        
        port_neq 1 port2 1
        set_port 0 $0
input_again:
        movl $3, %eax             # read
        movl input_id, %ecx
        movl input(,%ecx,4), %ebx # handle
        leal buffer, %ecx         # buffer
        movl $1, %edx             # count
        int $0x80
        testl %eax, %eax
        jne input_ok
        movl input_id, %eax
        testl %eax, %eax
        je end_of_input
        movl input(,%eax,4), %ebx # handle
        movl $6, %eax             # close
        int $0x80
        decl input_id
        jmp input_again
end_of_input:
        popl %eax               # restore stack
        jmp quit
input_ok:
        xorl %eax, %eax
        movb buffer, %al
        set_port 1 %eax
        
port2:  ## Output
        port_neq 2 port4 1
        set_port 0 $0
        check_data_1
        cmpl $0, data(,%edi,4)
        jge port2_char
        leal clear_screen, %ecx
        call write_str
        jmp port2_done
port2_char:
        movl $4, %eax            # write
        movl $1, %ebx            # stdout
        leal data(,%edi,4), %ecx # string
        movl $1, %edx
        int $0x80
port2_done:
        set_port 2 $0
        decl %edi

port4:  ## File operations
        port_eq 4 port5
        set_port 0 $0

port4_p1: ## Save image
        cmpl $1, %eax
        jne port4_p2
        movl $5, %eax           # open
        movl filename, %ebx     # filename
        movl $01101, %ecx       # mode (trunc/create/w)
        movl $0644, %edx        # permissions
        int $0x80
        testl %eax, %eax
        jl cannot_save
        movl %eax, %ebx         # handle
        movl $4, %eax           # write
        leal memory, %ecx       # buffer
        movl memory+12, %edx    # count
        shll $2, %edx           # count
        int $0x80
        movl $6, %eax           # close
        int $0x80
cannot_save:
        set_port 4 $0
        jmp port5

port4_p2: ## Add input source
        cmpl $2, %eax
        jne port4_1
        check_data_1
        call convert_str
        decl %edi
        movl $5, %eax           # open
        leal str, %ebx          # filename
        movl $0, %ecx           # mode (r)
        movl $0644, %edx        # permissions
        int $0x80
        testl %eax, %eax
        jl cannot_include
        incl input_id
        movl input_id, %ebx
        movl %eax, input(,%ebx,4)
cannot_include:
        set_port 4 $0
        jmp port5

port4_1: ## Open file
        cmpl $-1, %eax
        jne port4_2
        check_data_2
        movl data(,%edi,4), %edx
        decl %edi
        call convert_str
        movl file_modes(,%edx,4), %ecx  # open mode
        leal str, %ebx                  # filename
        movl $5, %eax                   # open
        movl $0644, %edx                # permissions
        int $0x80
        decl %edi
        testl %eax, %eax
        jge file_open_ok
        set_port 4 $0
        jmp port5
file_open_ok:
        set_port 4 %eax
        jmp port5

port4_2: ## Read byte
        cmpl $-2, %eax
        jne port4_3
        check_data_1
        movl $3, %eax             # read
        movl data(,%edi,4), %ebx  # handle
        leal buffer, %ecx         # buffer
        movl $1, %edx             # count
        int $0x80
        decl %edi
        testl %eax, %eax
        jge read_byte_ok
        set_port 4 $0
        jmp port5
read_byte_ok:
        xorl %eax, %eax
        movb buffer, %al
        set_port 4 %eax
        jmp port5

port4_3: ## Write byte
        cmpl $-3, %eax
        jne port4_4
        check_data_2
        movl $4, %eax             # write
        movl data(,%edi,4), %ebx  # handle
        movl data-4(,%edi,4), %ecx
        movb %cl, buffer
        leal buffer, %ecx         # buffer
        movl $1, %edx             # count
        int $0x80
        subl $2, %edi
        testl %eax, %eax
        jge write_byte_ok
        set_port 4 $0
        jmp port5
write_byte_ok:
        set_port 4 $1
        jmp port5

port4_4: ## Close file
        cmpl $-4, %eax
        jne port4_5
        check_data_1
        movl $6, %eax             # close
        movl data(,%edi,4), %ebx  # handle
        int $0x80
        decl %edi
        set_port 4 %eax
        jmp port5

port4_5: ## Location in file
        cmpl $-5, %eax
        jne port4_6
        check_data_1
        movl $19, %eax            # lseek
        movl data(,%edi,4), %ebx  # handle
        movl $0, %ecx             # offset
        movl $1, %edx             # seek from current position
        int $0x80
        decl %edi
        set_port 4 %eax
        jmp port5

port4_6: ## Seek in file
        cmpl $-6, %eax
        jne port4_7
        check_data_2
        movl $19, %eax             # lseek
        movl data(,%edi,4), %ebx   # handle
        movl data-4(,%edi,4), %ecx # offset
        movl $0, %edx              # seek from the beginning
        int $0x80
        subl $2, %edi
        set_port 4 %eax
        jmp port5

port4_7: ## File size
        cmpl $-7, %eax
        jne port4_8
        check_data_1
        movl $108, %eax           # fstat
        movl data(,%edi,4), %ebx  # handle
        leal stats, %ecx          # stat record
        int $0x80
        decl %edi
        testl %eax, %eax
        jge file_size_ok
        set_port 4 $0
        jmp port5
file_size_ok:
        movl stats+20, %eax
        set_port 4 %eax
        jmp port5

port4_8: ## Delete file
        cmpl $-8, %eax
        jne port5
        check_data_1
        call convert_str
        movl $10, %eax            # unlink
        leal str, %ebx            # filename
        int $0x80
        decl %edi
        testl %eax, %eax
        je delete_file_ok
        set_port 4 $0
        jmp port5
delete_file_ok:
        set_port 4 $-1

port5:  ## Various features
        port_eq 5 no_port
        set_port 0 $0

port5_1: ## Memory size
        cmpl $-1, %eax
        jne port5_5
        set_port 5 $MEMORY_SIZE
        jmp no_port

port5_5: ## Data stack size
        cmpl $-5, %eax
        jne port5_6
        movl %edi, %ebx
        incl %ebx
        set_port 5 %ebx
        jmp no_port

port5_6: ## Return stack size
        cmpl $-6, %eax
        jne port5_8
        movl %esi, %ebx
        incl %ebx
        set_port 5 %ebx
        jmp no_port

port5_8: ## Current time
        cmpl $-8, %eax
        jne port5_9
        movl $13, %eax          # time
        movl $0, %ebx
        int $0x80
        set_port 5 %eax
        jmp no_port

port5_9: ## Quit
        cmpl $-9, %eax
        jne port5_10
        popl %eax               # restore stack
        jmp quit

port5_10: ## Environment variable search
        cmpl $-10, %eax
        jne port5_11
        check_data_2
        call convert_str
        decl %edi
        movl %esp, %edx
        popl %eax               # return address
        popl %eax               # NULL (end of command-line arguments)
env_loop:                       # find str in the environment and put in %ebx
        popl %ebx
        testl %ebx, %ebx
        je env_not_found
        xorl %ecx, %ecx
compare_loop:                   # compare the string at %ebx with str
        movb str(%ecx), %al
        testb %al, %al
        je str_ended
        cmpb $61, (%ebx)        # '='
        je env_loop             # premature end of environment variable string
        cmpb (%ebx), %al
        jne env_loop            # strings differ
        incl %ebx
        incl %ecx
        jmp compare_loop
str_ended:
        cmpb $61, (%ebx)        # '='
        jne env_loop            # premature end of str
        incl %ebx
        call revert_str
        jmp env_end
env_not_found:
        movl data(,%edi,4), %ecx
        movb $0, memory(,%ecx,4)
env_end:
        movl %edx, %esp
        decl %edi
        set_port 5 $0
        jmp no_port

port5_11: ## Window width
        cmpl $-11, %eax
        jne port5_12
        call get_window_size
        xorl %eax, %eax
        movw winsize, %ax
        set_port 5 %eax
        jmp no_port

port5_12: ## Window height
        cmpl $-11, %eax
        jne port5_end
        call get_window_size
        xorl %eax, %eax
        movw winsize+2, %ax
        set_port 5 %eax
        jmp no_port

port5_end:
        set_port 5 $0

no_port:
        ret