RosettaCodeData/Task/Compiler-virtual-machine-in.../Fortran/compiler-virtual-machine-in...

1535 lines
45 KiB
Fortran
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module compiler_type_kinds
use, intrinsic :: iso_fortran_env, only: int32
use, intrinsic :: iso_fortran_env, only: int64
implicit none
private
! Synonyms.
integer, parameter, public :: size_kind = int64
integer, parameter, public :: length_kind = size_kind
integer, parameter, public :: nk = size_kind
! Synonyms for character capable of storing a Unicode code point.
integer, parameter, public :: unicode_char_kind = selected_char_kind ('ISO_10646')
integer, parameter, public :: ck = unicode_char_kind
! Synonyms for integers capable of storing a Unicode code point.
integer, parameter, public :: unicode_ichar_kind = int32
integer, parameter, public :: ick = unicode_ichar_kind
! Synonyms for integers in the virtual machine or the interpreters
! runtime. (The Rosetta Code task says integers in the virtual
! machine are 32-bit, but there is nothing in the task that prevents
! us using 64-bit integers in the compiler and interpreter.)
integer, parameter, public :: runtime_int_kind = int64
integer, parameter, public :: rik = runtime_int_kind
end module compiler_type_kinds
module helpers
use, non_intrinsic :: compiler_type_kinds, only: nk, rik, ck
implicit none
private
public :: new_storage_size
public :: next_power_of_two
public :: isspace
public :: quoted_string
public :: int32_to_vm_bytes
public :: uint32_to_vm_bytes
public :: int32_from_vm_bytes
public :: uint32_from_vm_bytes
public :: bool2int
character(1, kind = ck), parameter, public :: horizontal_tab_char = char (9, kind = ck)
character(1, kind = ck), parameter, public :: linefeed_char = char (10, kind = ck)
character(1, kind = ck), parameter, public :: vertical_tab_char = char (11, kind = ck)
character(1, kind = ck), parameter, public :: formfeed_char = char (12, kind = ck)
character(1, kind = ck), parameter, public :: carriage_return_char = char (13, kind = ck)
character(1, kind = ck), parameter, public :: space_char = ck_' '
! The following is correct for Unix and its relatives.
character(1, kind = ck), parameter, public :: newline_char = linefeed_char
character(1, kind = ck), parameter, public :: backslash_char = char (92, kind = ck)
contains
elemental function new_storage_size (length_needed) result (size)
integer(kind = nk), intent(in) :: length_needed
integer(kind = nk) :: size
! Increase storage by orders of magnitude.
if (2_nk**32 < length_needed) then
size = huge (1_nk)
else
size = next_power_of_two (length_needed)
end if
end function new_storage_size
elemental function next_power_of_two (x) result (y)
integer(kind = nk), intent(in) :: x
integer(kind = nk) :: y
!
! It is assumed that no more than 64 bits are used.
!
! The branch-free algorithm is that of
! https://archive.is/nKxAc#RoundUpPowerOf2
!
! Fill in bits until one less than the desired power of two is
! reached, and then add one.
!
y = x - 1
y = ior (y, ishft (y, -1))
y = ior (y, ishft (y, -2))
y = ior (y, ishft (y, -4))
y = ior (y, ishft (y, -8))
y = ior (y, ishft (y, -16))
y = ior (y, ishft (y, -32))
y = y + 1
end function next_power_of_two
elemental function isspace (ch) result (bool)
character(1, kind = ck), intent(in) :: ch
logical :: bool
bool = (ch == horizontal_tab_char) .or. &
& (ch == linefeed_char) .or. &
& (ch == vertical_tab_char) .or. &
& (ch == formfeed_char) .or. &
& (ch == carriage_return_char) .or. &
& (ch == space_char)
end function isspace
function quoted_string (str) result (qstr)
character(*, kind = ck), intent(in) :: str
character(:, kind = ck), allocatable :: qstr
integer(kind = nk) :: n, i, j
! Compute n = the size of qstr.
n = 2_nk
do i = 1_nk, len (str, kind = nk)
select case (str(i:i))
case (newline_char, backslash_char)
n = n + 2
case default
n = n + 1
end select
end do
allocate (character(n, kind = ck) :: qstr)
! Quote the string.
qstr(1:1) = ck_'"'
j = 2_nk
do i = 1_nk, len (str, kind = nk)
select case (str(i:i))
case (newline_char)
qstr(j:j) = backslash_char
qstr((j + 1):(j + 1)) = ck_'n'
j = j + 2
case (backslash_char)
qstr(j:j) = backslash_char
qstr((j + 1):(j + 1)) = backslash_char
j = j + 2
case default
qstr(j:j) = str(i:i)
j = j + 1
end select
end do
if (j /= n) error stop ! Check code correctness.
qstr(n:n) = ck_'"'
end function quoted_string
subroutine int32_to_vm_bytes (n, bytes, i)
integer(kind = rik), intent(in) :: n
character(1), intent(inout) :: bytes(0:*)
integer(kind = rik), intent(in) :: i
!
! The virtual machine is presumed to be little-endian. Because I
! slightly prefer little-endian.
!
bytes(i) = achar (ibits (n, 0, 8))
bytes(i + 1) = achar (ibits (n, 8, 8))
bytes(i + 2) = achar (ibits (n, 16, 8))
bytes(i + 3) = achar (ibits (n, 24, 8))
end subroutine int32_to_vm_bytes
subroutine uint32_to_vm_bytes (n, bytes, i)
integer(kind = rik), intent(in) :: n
character(1), intent(inout) :: bytes(0:*)
integer(kind = rik), intent(in) :: i
call int32_to_vm_bytes (n, bytes, i)
end subroutine uint32_to_vm_bytes
subroutine int32_from_vm_bytes (n, bytes, i)
integer(kind = rik), intent(out) :: n
character(1), intent(in) :: bytes(0:*)
integer(kind = rik), intent(in) :: i
!
! The virtual machine is presumed to be little-endian. Because I
! slightly prefer little-endian.
!
call uint32_from_vm_bytes (n, bytes, i)
if (ibits (n, 31, 1) == 1) then
! Extend the sign bit.
n = ior (n, not ((2_rik ** 32) - 1))
end if
end subroutine int32_from_vm_bytes
subroutine uint32_from_vm_bytes (n, bytes, i)
integer(kind = rik), intent(out) :: n
character(1), intent(in) :: bytes(0:*)
integer(kind = rik), intent(in) :: i
!
! The virtual machine is presumed to be little-endian. Because I
! slightly prefer little-endian.
!
integer(kind = rik) :: n0, n1, n2, n3
n0 = iachar (bytes(i), kind = rik)
n1 = ishft (iachar (bytes(i + 1), kind = rik), 8)
n2 = ishft (iachar (bytes(i + 2), kind = rik), 16)
n3 = ishft (iachar (bytes(i + 3), kind = rik), 24)
n = ior (n0, ior (n1, ior (n2, n3)))
end subroutine uint32_from_vm_bytes
elemental function bool2int (bool) result (int)
logical, intent(in) :: bool
integer(kind = rik) :: int
if (bool) then
int = 1_rik
else
int = 0_rik
end if
end function bool2int
end module helpers
module string_buffers
use, intrinsic :: iso_fortran_env, only: error_unit
use, intrinsic :: iso_fortran_env, only: int64
use, non_intrinsic :: compiler_type_kinds, only: nk, ck, ick
use, non_intrinsic :: helpers
implicit none
private
public :: strbuf_t
public :: skip_whitespace
public :: skip_non_whitespace
public :: skip_whitespace_backwards
public :: at_end_of_line
type :: strbuf_t
integer(kind = nk), private :: len = 0
!
! chars is made public for efficient access to the individual
! characters.
!
character(1, kind = ck), allocatable, public :: chars(:)
contains
procedure, pass, private :: ensure_storage => strbuf_t_ensure_storage
procedure, pass :: to_unicode_full_string => strbuf_t_to_unicode_full_string
procedure, pass :: to_unicode_substring => strbuf_t_to_unicode_substring
procedure, pass :: length => strbuf_t_length
procedure, pass :: set => strbuf_t_set
procedure, pass :: append => strbuf_t_append
generic :: to_unicode => to_unicode_full_string
generic :: to_unicode => to_unicode_substring
generic :: assignment(=) => set
end type strbuf_t
contains
function strbuf_t_to_unicode_full_string (strbuf) result (s)
class(strbuf_t), intent(in) :: strbuf
character(:, kind = ck), allocatable :: s
!
! This does not actually ensure that the string is valid Unicode;
! any 31-bit character is supported.
!
integer(kind = nk) :: i
allocate (character(len = strbuf%len, kind = ck) :: s)
do i = 1, strbuf%len
s(i:i) = strbuf%chars(i)
end do
end function strbuf_t_to_unicode_full_string
function strbuf_t_to_unicode_substring (strbuf, i, j) result (s)
!
! Extreme values of i and j are allowed, as shortcuts for from
! the beginning, up to the end, or empty substring.
!
class(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i, j
character(:, kind = ck), allocatable :: s
!
! This does not actually ensure that the string is valid Unicode;
! any 31-bit character is supported.
!
integer(kind = nk) :: i1, j1
integer(kind = nk) :: n
integer(kind = nk) :: k
i1 = max (1_nk, i)
j1 = min (strbuf%len, j)
n = max (0_nk, (j1 - i1) + 1_nk)
allocate (character(n, kind = ck) :: s)
do k = 1, n
s(k:k) = strbuf%chars(i1 + (k - 1_nk))
end do
end function strbuf_t_to_unicode_substring
elemental function strbuf_t_length (strbuf) result (n)
class(strbuf_t), intent(in) :: strbuf
integer(kind = nk) :: n
n = strbuf%len
end function strbuf_t_length
subroutine strbuf_t_ensure_storage (strbuf, length_needed)
class(strbuf_t), intent(inout) :: strbuf
integer(kind = nk), intent(in) :: length_needed
integer(kind = nk) :: len_needed
integer(kind = nk) :: new_size
type(strbuf_t) :: new_strbuf
len_needed = max (length_needed, 1_nk)
if (.not. allocated (strbuf%chars)) then
! Initialize a new strbuf%chars array.
new_size = new_storage_size (len_needed)
allocate (strbuf%chars(1:new_size))
else if (ubound (strbuf%chars, 1) < len_needed) then
! Allocate a new strbuf%chars array, larger than the current
! one, but containing the same characters.
new_size = new_storage_size (len_needed)
allocate (new_strbuf%chars(1:new_size))
new_strbuf%chars(1:strbuf%len) = strbuf%chars(1:strbuf%len)
call move_alloc (new_strbuf%chars, strbuf%chars)
end if
end subroutine strbuf_t_ensure_storage
subroutine strbuf_t_set (dst, src)
class(strbuf_t), intent(inout) :: dst
class(*), intent(in) :: src
integer(kind = nk) :: n
integer(kind = nk) :: i
select type (src)
type is (character(*, kind = ck))
n = len (src, kind = nk)
call dst%ensure_storage(n)
do i = 1, n
dst%chars(i) = src(i:i)
end do
dst%len = n
type is (character(*))
n = len (src, kind = nk)
call dst%ensure_storage(n)
do i = 1, n
dst%chars(i) = src(i:i)
end do
dst%len = n
class is (strbuf_t)
n = src%len
call dst%ensure_storage(n)
dst%chars(1:n) = src%chars(1:n)
dst%len = n
class default
error stop
end select
end subroutine strbuf_t_set
subroutine strbuf_t_append (dst, src)
class(strbuf_t), intent(inout) :: dst
class(*), intent(in) :: src
integer(kind = nk) :: n_dst, n_src, n
integer(kind = nk) :: i
select type (src)
type is (character(*, kind = ck))
n_dst = dst%len
n_src = len (src, kind = nk)
n = n_dst + n_src
call dst%ensure_storage(n)
do i = 1, n_src
dst%chars(n_dst + i) = src(i:i)
end do
dst%len = n
type is (character(*))
n_dst = dst%len
n_src = len (src, kind = nk)
n = n_dst + n_src
call dst%ensure_storage(n)
do i = 1, n_src
dst%chars(n_dst + i) = src(i:i)
end do
dst%len = n
class is (strbuf_t)
n_dst = dst%len
n_src = src%len
n = n_dst + n_src
call dst%ensure_storage(n)
dst%chars((n_dst + 1):n) = src%chars(1:n_src)
dst%len = n
class default
error stop
end select
end subroutine strbuf_t_append
function skip_whitespace (strbuf, i) result (j)
class(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
integer(kind = nk) :: j
logical :: done
j = i
done = .false.
do while (.not. done)
if (at_end_of_line (strbuf, j)) then
done = .true.
else if (.not. isspace (strbuf%chars(j))) then
done = .true.
else
j = j + 1
end if
end do
end function skip_whitespace
function skip_non_whitespace (strbuf, i) result (j)
class(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
integer(kind = nk) :: j
logical :: done
j = i
done = .false.
do while (.not. done)
if (at_end_of_line (strbuf, j)) then
done = .true.
else if (isspace (strbuf%chars(j))) then
done = .true.
else
j = j + 1
end if
end do
end function skip_non_whitespace
function skip_whitespace_backwards (strbuf, i) result (j)
class(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
integer(kind = nk) :: j
logical :: done
j = i
done = .false.
do while (.not. done)
if (j == -1) then
done = .true.
else if (.not. isspace (strbuf%chars(j))) then
done = .true.
else
j = j - 1
end if
end do
end function skip_whitespace_backwards
function at_end_of_line (strbuf, i) result (bool)
class(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
logical :: bool
bool = (strbuf%length() < i)
end function at_end_of_line
end module string_buffers
module reading_one_line_from_a_stream
use, intrinsic :: iso_fortran_env, only: input_unit
use, intrinsic :: iso_fortran_env, only: error_unit
use, non_intrinsic :: compiler_type_kinds, only: nk, ck, ick
use, non_intrinsic :: string_buffers
implicit none
private
! get_line_from_stream: read an entire input line from a stream into
! a strbuf_t.
public :: get_line_from_stream
character(1, kind = ck), parameter :: linefeed_char = char (10, kind = ck)
! The following is correct for Unix and its relatives.
character(1, kind = ck), parameter :: newline_char = linefeed_char
contains
subroutine get_line_from_stream (unit_no, eof, no_newline, strbuf)
integer, intent(in) :: unit_no
logical, intent(out) :: eof ! End of file?
logical, intent(out) :: no_newline ! There is a line but it has no
! newline? (Thus eof also must
! be .true.)
class(strbuf_t), intent(inout) :: strbuf
character(1, kind = ck) :: ch
strbuf = ''
call get_ch (unit_no, eof, ch)
do while (.not. eof .and. ch /= newline_char)
call strbuf%append (ch)
call get_ch (unit_no, eof, ch)
end do
no_newline = eof .and. (strbuf%length() /= 0)
end subroutine get_line_from_stream
subroutine get_ch (unit_no, eof, ch)
!
! Read a single code point from the stream.
!
! Currently this procedure simply inputs ASCII bytes rather than
! Unicode code points.
!
integer, intent(in) :: unit_no
logical, intent(out) :: eof
character(1, kind = ck), intent(out) :: ch
integer :: stat
character(1) :: c = '*'
eof = .false.
if (unit_no == input_unit) then
call get_input_unit_char (c, stat)
else
read (unit = unit_no, iostat = stat) c
end if
if (stat < 0) then
ch = ck_'*'
eof = .true.
else if (0 < stat) then
write (error_unit, '("Input error with status code ", I0)') stat
stop 1
else
ch = char (ichar (c, kind = ick), kind = ck)
end if
end subroutine get_ch
!!!
!!! If you tell gfortran you want -std=f2008 or -std=f2018, you likely
!!! will need to add also -fall-intrinsics or -U__GFORTRAN__
!!!
!!! The first way, you get the FGETC intrinsic. The latter way, you
!!! get the C interface code that uses getchar(3).
!!!
#ifdef __GFORTRAN__
subroutine get_input_unit_char (c, stat)
!
! The following works if you are using gfortran.
!
! (FGETC is considered a feature for backwards compatibility with
! g77. However, I know of no way to reconfigure input_unit as a
! Fortran 2003 stream, for use with ordinary read.)
!
character, intent(inout) :: c
integer, intent(out) :: stat
call fgetc (input_unit, c, stat)
end subroutine get_input_unit_char
#else
subroutine get_input_unit_char (c, stat)
!
! An alternative implementation of get_input_unit_char. This
! actually reads input from the C standard input, which might not
! be the same as input_unit.
!
use, intrinsic :: iso_c_binding, only: c_int
character, intent(inout) :: c
integer, intent(out) :: stat
interface
!
! Use getchar(3) to read characters from standard input. This
! assumes there is actually such a function available, and that
! getchar(3) does not exist solely as a macro. (One could write
! ones own getchar() if necessary, of course.)
!
function getchar () result (c) bind (c, name = 'getchar')
use, intrinsic :: iso_c_binding, only: c_int
integer(kind = c_int) :: c
end function getchar
end interface
integer(kind = c_int) :: i_char
i_char = getchar ()
!
! The C standard requires that EOF have a negative value. If the
! value returned by getchar(3) is not EOF, then it will be
! representable as an unsigned char. Therefore, to check for end
! of file, one need only test whether i_char is negative.
!
if (i_char < 0) then
stat = -1
else
stat = 0
c = char (i_char)
end if
end subroutine get_input_unit_char
#endif
end module reading_one_line_from_a_stream
module vm_reader
use, intrinsic :: iso_fortran_env, only: error_unit
use, non_intrinsic :: compiler_type_kinds
use, non_intrinsic :: helpers
use, non_intrinsic :: string_buffers
use, non_intrinsic :: reading_one_line_from_a_stream
implicit none
private
public :: vm_code_t
public :: vm_t
public :: read_vm
!
! Arbitrarily chosen opcodes.
!
! I think there should be a no-operation nop opcode, to reserve
! space for later hand-patching. :)
!
integer, parameter, public :: opcode_nop = 0
integer, parameter, public :: opcode_halt = 1
integer, parameter, public :: opcode_add = 2
integer, parameter, public :: opcode_sub = 3
integer, parameter, public :: opcode_mul = 4
integer, parameter, public :: opcode_div = 5
integer, parameter, public :: opcode_mod = 6
integer, parameter, public :: opcode_lt = 7
integer, parameter, public :: opcode_gt = 8
integer, parameter, public :: opcode_le = 9
integer, parameter, public :: opcode_ge = 10
integer, parameter, public :: opcode_eq = 11
integer, parameter, public :: opcode_ne = 12
integer, parameter, public :: opcode_and = 13
integer, parameter, public :: opcode_or = 14
integer, parameter, public :: opcode_neg = 15
integer, parameter, public :: opcode_not = 16
integer, parameter, public :: opcode_prtc = 17
integer, parameter, public :: opcode_prti = 18
integer, parameter, public :: opcode_prts = 19
integer, parameter, public :: opcode_fetch = 20
integer, parameter, public :: opcode_store = 21
integer, parameter, public :: opcode_push = 22
integer, parameter, public :: opcode_jmp = 23
integer, parameter, public :: opcode_jz = 24
character(8, kind = ck), parameter, public :: opcode_names(0:24) = &
& (/ "nop ", &
& "halt ", &
& "add ", &
& "sub ", &
& "mul ", &
& "div ", &
& "mod ", &
& "lt ", &
& "gt ", &
& "le ", &
& "ge ", &
& "eq ", &
& "ne ", &
& "and ", &
& "or ", &
& "neg ", &
& "not ", &
& "prtc ", &
& "prti ", &
& "prts ", &
& "fetch ", &
& "store ", &
& "push ", &
& "jmp ", &
& "jz " /)
type :: vm_code_t
integer(kind = rik), private :: len = 0_rik
character(1), allocatable :: bytes(:)
contains
procedure, pass, private :: ensure_storage => vm_code_t_ensure_storage
procedure, pass :: length => vm_code_t_length
end type vm_code_t
type :: vm_t
integer(kind = rik), allocatable :: string_boundaries(:)
character(:, kind = ck), allocatable :: strings
character(1), allocatable :: data(:)
character(1), allocatable :: stack(:)
type(vm_code_t) :: code
integer(kind = rik) :: sp = 0_rik
integer(kind = rik) :: pc = 0_rik
end type vm_t
contains
subroutine vm_code_t_ensure_storage (code, length_needed)
class(vm_code_t), intent(inout) :: code
integer(kind = nk), intent(in) :: length_needed
integer(kind = nk) :: len_needed
integer(kind = nk) :: new_size
type(vm_code_t) :: new_code
len_needed = max (length_needed, 1_nk)
if (.not. allocated (code%bytes)) then
! Initialize a new code%bytes array.
new_size = new_storage_size (len_needed)
allocate (code%bytes(0:(new_size - 1)))
else if (ubound (code%bytes, 1) < len_needed - 1) then
! Allocate a new code%bytes array, larger than the current one,
! but containing the same bytes.
new_size = new_storage_size (len_needed)
allocate (new_code%bytes(0:(new_size - 1)))
new_code%bytes(0:(code%len - 1)) = code%bytes(0:(code%len - 1))
call move_alloc (new_code%bytes, code%bytes)
end if
end subroutine vm_code_t_ensure_storage
elemental function vm_code_t_length (code) result (len)
class(vm_code_t), intent(in) :: code
integer(kind = rik) :: len
len = code%len
end function vm_code_t_length
subroutine read_vm (inp, strbuf, vm)
integer, intent(in) :: inp
type(strbuf_t), intent(inout) :: strbuf
type(vm_t), intent(out) :: vm
integer(kind = rik) :: data_size
integer(kind = rik) :: number_of_strings
! Read the header.
call read_datasize_and_number_of_strings (inp, strbuf, data_size, number_of_strings)
! Allocate storage for data_size 32-bit numbers. Initialize them
! to zero, for no better reason than that C initializes global
! variables to zero.
allocate (vm%data(0_rik:(4_rik * (data_size - 1))), source = achar (0))
! Allocate storage for indices/bounds of the strings to be loaded
! into the string storage space.
allocate (vm%string_boundaries(0_rik:number_of_strings))
! Fill the strings storage and the string boundaries array.
call read_strings (inp, strbuf, number_of_strings, vm)
! Read the program instructions.
call read_code (inp, strbuf, vm)
! Allocate a stack. Let us say that the stack size must be a
! multiple of 4, and is fixed at 65536 = 4**8 bytes. Pushing a
! 32-bit integer increases the stack pointer by 4, popping
! decreases it by 4.
allocate (vm%stack(0_rik:(4_rik ** 8)))
end subroutine read_vm
subroutine read_datasize_and_number_of_strings (inp, strbuf, data_size, number_of_strings)
integer, intent(in) :: inp
type(strbuf_t), intent(inout) :: strbuf
integer(kind = rik), intent(out) :: data_size
integer(kind = rik), intent(out) :: number_of_strings
logical :: eof
logical :: no_newline
integer(kind = nk) :: i, j
character(:, kind = ck), allocatable :: data_size_str
character(:, kind = ck), allocatable :: number_of_strings_str
integer :: stat
call get_line_from_stream (inp, eof, no_newline, strbuf)
if (eof) call bad_vm_assembly
i = skip_whitespace (strbuf, 1_nk)
i = skip_datasize_keyword (strbuf, i)
i = skip_whitespace (strbuf, i)
i = skip_specific_character (strbuf, i, ck_':')
i = skip_whitespace (strbuf, i)
j = skip_non_whitespace (strbuf, i)
if (j == i) call bad_vm_assembly
allocate (data_size_str, source = strbuf%to_unicode (i, j - 1))
i = skip_whitespace(strbuf, j)
i = skip_strings_keyword (strbuf, i)
i = skip_whitespace (strbuf, i)
i = skip_specific_character (strbuf, i, ck_':')
i = skip_whitespace (strbuf, i)
j = skip_non_whitespace (strbuf, i)
if (j == i) call bad_vm_assembly
allocate (number_of_strings_str, source = strbuf%to_unicode (i, j - 1))
read (data_size_str, *, iostat = stat) data_size
if (stat /= 0) call bad_vm_assembly
read (number_of_strings_str, *, iostat = stat) number_of_strings
if (stat /= 0) call bad_vm_assembly
end subroutine read_datasize_and_number_of_strings
subroutine read_strings (inp, strbuf, number_of_strings, vm)
integer, intent(in) :: inp
type(strbuf_t), intent(inout) :: strbuf
integer(kind = rik), intent(in) :: number_of_strings
type(vm_t), intent(inout) :: vm
type(strbuf_t) :: strings_temporary
integer(kind = rik) :: i
vm%string_boundaries(0) = 0_rik
do i = 0_rik, number_of_strings - 1
call read_one_string (inp, strbuf, strings_temporary)
vm%string_boundaries(i + 1) = strings_temporary%length()
end do
allocate (vm%strings, source = strings_temporary%to_unicode())
end subroutine read_strings
subroutine read_one_string (inp, strbuf, strings_temporary)
integer, intent(in) :: inp
type(strbuf_t), intent(inout) :: strbuf
type(strbuf_t), intent(inout) :: strings_temporary
logical :: eof
logical :: no_newline
integer(kind = nk) :: i
logical :: done
call get_line_from_stream (inp, eof, no_newline, strbuf)
if (eof) call bad_vm_assembly
i = skip_whitespace (strbuf, 1_nk)
i = skip_specific_character (strbuf, i, ck_'"')
done = .false.
do while (.not. done)
if (i == strbuf%length() + 1) call bad_vm_assembly
if (strbuf%chars(i) == ck_'"') then
done = .true.
else if (strbuf%chars(i) == backslash_char) then
if (i == strbuf%length()) call bad_vm_assembly
select case (strbuf%chars(i + 1))
case (ck_'n')
call strings_temporary%append(newline_char)
case (backslash_char)
call strings_temporary%append(backslash_char)
case default
call bad_vm_assembly
end select
i = i + 2
else
call strings_temporary%append(strbuf%chars(i))
i = i + 1
end if
end do
end subroutine read_one_string
subroutine read_code (inp, strbuf, vm)
integer, intent(in) :: inp
type(strbuf_t), intent(inout) :: strbuf
type(vm_t), intent(inout) :: vm
logical :: eof
logical :: no_newline
call get_line_from_stream (inp, eof, no_newline, strbuf)
do while (.not. eof)
call parse_instruction (strbuf, vm%code)
call get_line_from_stream (inp, eof, no_newline, strbuf)
end do
end subroutine read_code
subroutine parse_instruction (strbuf, code)
type(strbuf_t), intent(in) :: strbuf
type(vm_code_t), intent(inout) :: code
integer(kind = nk) :: i, j
integer :: stat
integer :: opcode
integer(kind = rik) :: i_vm
integer(kind = rik) :: arg
character(8, kind = ck) :: opcode_name_str
character(:, kind = ck), allocatable :: i_vm_str
character(:, kind = ck), allocatable :: arg_str
i = skip_whitespace (strbuf, 1_nk)
j = skip_non_whitespace (strbuf, i)
if (j == i) call bad_vm_assembly
allocate (i_vm_str, source = strbuf%to_unicode(i, j - 1))
read (i_vm_str, *, iostat = stat) i_vm
if (stat /= 0) call bad_vm_assembly
i = skip_whitespace (strbuf, j)
j = skip_non_whitespace (strbuf, i)
opcode_name_str = ck_' '
opcode_name_str(1:(j - i)) = strbuf%to_unicode(i, j - 1)
opcode = findloc (opcode_names, opcode_name_str, 1) - 1
if (opcode == -1) call bad_vm_assembly
select case (opcode)
case (opcode_push)
call code%ensure_storage(i_vm + 5)
code%bytes(i_vm) = achar (opcode)
i = skip_whitespace (strbuf, j)
j = skip_non_whitespace (strbuf, i)
if (j == i) call bad_vm_assembly
allocate (arg_str, source = strbuf%to_unicode(i, j - 1))
read (arg_str, *, iostat = stat) arg
if (stat /= 0) call bad_vm_assembly
call int32_to_vm_bytes (arg, code%bytes, i_vm + 1)
code%len = max (code%len, i_vm + 5)
case (opcode_fetch, opcode_store)
call code%ensure_storage(i_vm + 5)
code%bytes(i_vm) = achar (opcode)
i = skip_whitespace (strbuf, j)
i = skip_specific_character (strbuf, i, ck_'[')
i = skip_whitespace (strbuf, i)
j = skip_non_whitespace (strbuf, i)
if (j == i) call bad_vm_assembly
if (strbuf%chars(j - 1) == ck_']') j = j - 1
allocate (arg_str, source = strbuf%to_unicode(i, j - 1))
read (arg_str, *, iostat = stat) arg
if (stat /= 0) call bad_vm_assembly
call uint32_to_vm_bytes (arg, code%bytes, i_vm + 1)
code%len = max (code%len, i_vm + 5)
case (opcode_jmp, opcode_jz)
call code%ensure_storage(i_vm + 5)
code%bytes(i_vm) = achar (opcode)
call code%ensure_storage(i_vm + 5)
code%bytes(i_vm) = achar (opcode)
i = skip_whitespace (strbuf, j)
i = skip_specific_character (strbuf, i, ck_'(')
i = skip_whitespace (strbuf, i)
j = skip_non_whitespace (strbuf, i)
if (j == i) call bad_vm_assembly
if (strbuf%chars(j - 1) == ck_')') j = j - 1
allocate (arg_str, source = strbuf%to_unicode(i, j - 1))
read (arg_str, *, iostat = stat) arg
if (stat /= 0) call bad_vm_assembly
call int32_to_vm_bytes (arg, code%bytes, i_vm + 1)
code%len = max (code%len, i_vm + 5)
case default
call code%ensure_storage(i_vm + 1)
code%bytes(i_vm) = achar (opcode)
code%len = max (code%len, i_vm + 1)
end select
end subroutine parse_instruction
function skip_datasize_keyword (strbuf, i) result (j)
type(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
integer(kind = nk) :: j
j = skip_specific_character (strbuf, i, ck_'D')
j = skip_specific_character (strbuf, j, ck_'a')
j = skip_specific_character (strbuf, j, ck_'t')
j = skip_specific_character (strbuf, j, ck_'a')
j = skip_specific_character (strbuf, j, ck_'s')
j = skip_specific_character (strbuf, j, ck_'i')
j = skip_specific_character (strbuf, j, ck_'z')
j = skip_specific_character (strbuf, j, ck_'e')
end function skip_datasize_keyword
function skip_strings_keyword (strbuf, i) result (j)
type(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
integer(kind = nk) :: j
j = skip_specific_character (strbuf, i, ck_'S')
j = skip_specific_character (strbuf, j, ck_'t')
j = skip_specific_character (strbuf, j, ck_'r')
j = skip_specific_character (strbuf, j, ck_'i')
j = skip_specific_character (strbuf, j, ck_'n')
j = skip_specific_character (strbuf, j, ck_'g')
j = skip_specific_character (strbuf, j, ck_'s')
end function skip_strings_keyword
function skip_specific_character (strbuf, i, ch) result (j)
type(strbuf_t), intent(in) :: strbuf
integer(kind = nk), intent(in) :: i
character(1, kind = ck), intent(in) :: ch
integer(kind = nk) :: j
if (strbuf%length() < i) call bad_vm_assembly
if (strbuf%chars(i) /= ch) call bad_vm_assembly
j = i + 1
end function skip_specific_character
subroutine bad_vm_assembly
write (error_unit, '("The input is not a correct virtual machine program.")')
stop 1
end subroutine bad_vm_assembly
end module vm_reader
module vm_runner
use, intrinsic :: iso_fortran_env, only: error_unit
use, non_intrinsic :: compiler_type_kinds
use, non_intrinsic :: helpers
use, non_intrinsic :: vm_reader
implicit none
private
public :: run_vm
contains
subroutine run_vm (outp, vm)
integer, intent(in) :: outp
type(vm_t), intent(inout) :: vm
logical :: done
integer :: opcode
vm%sp = 0
vm%pc = 0
done = .false.
do while (.not. done)
if (vm%pc < 0 .or. vm%code%length() <= vm%pc) call pc_error
opcode = iachar (vm%code%bytes(vm%pc))
vm%pc = vm%pc + 1
select case (opcode)
case (opcode_nop)
continue
case (opcode_halt)
done = .true.
case (opcode_add)
call alu_add (vm)
case (opcode_sub)
call alu_sub (vm)
case (opcode_mul)
call alu_mul (vm)
case (opcode_div)
call alu_div (vm)
case (opcode_mod)
call alu_mod (vm)
case (opcode_lt)
call alu_lt (vm)
case (opcode_gt)
call alu_gt (vm)
case (opcode_le)
call alu_le (vm)
case (opcode_ge)
call alu_ge (vm)
case (opcode_eq)
call alu_eq (vm)
case (opcode_ne)
call alu_ne (vm)
case (opcode_and)
call alu_and (vm)
case (opcode_or)
call alu_or (vm)
case (opcode_neg)
call alu_neg (vm)
case (opcode_not)
call alu_not (vm)
case (opcode_prtc)
call prtc (outp, vm)
case (opcode_prti)
call prti (outp, vm)
case (opcode_prts)
call prts (outp, vm)
case (opcode_fetch)
call fetch_int32 (vm)
case (opcode_store)
call store_int32 (vm)
case (opcode_push)
call push_int32 (vm)
case (opcode_jmp)
call jmp (vm)
case (opcode_jz)
call jz (vm)
case default
write (error_unit, '("VM opcode unrecognized: ", I0)') opcode
stop 1
end select
end do
end subroutine run_vm
subroutine push_int32 (vm)
type(vm_t), intent(inout) :: vm
!
! Push the 32-bit integer data at pc to the stack, then increment
! pc by 4.
!
if (ubound (vm%stack, 1) < vm%sp) then
write (error_unit, '("VM stack overflow")')
stop 1
end if
if (vm%code%length() <= vm%pc + 4) call pc_error
vm%stack(vm%sp:(vm%sp + 3)) = vm%code%bytes(vm%pc:(vm%pc + 3))
vm%sp = vm%sp + 4
vm%pc = vm%pc + 4
end subroutine push_int32
subroutine fetch_int32 (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: i
integer(kind = rik) :: x
if (vm%code%length() <= vm%pc + 4) call pc_error
call uint32_from_vm_bytes (i, vm%code%bytes, vm%pc)
vm%pc = vm%pc + 4
if (ubound (vm%data, 1) < i * 4) then
write (error_unit, '("VM data access error")')
stop 1
end if
call int32_from_vm_bytes (x, vm%data, i * 4)
if (ubound (vm%stack, 1) < vm%sp) then
write (error_unit, '("VM stack overflow")')
stop 1
end if
call int32_to_vm_bytes (x, vm%stack, vm%sp)
vm%sp = vm%sp + 4
end subroutine fetch_int32
subroutine store_int32 (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: i
integer(kind = rik) :: x
if (vm%code%length() <= vm%pc + 4) call pc_error
call uint32_from_vm_bytes (i, vm%code%bytes, vm%pc)
vm%pc = vm%pc + 4
call ensure_there_is_enough_stack_data (vm, 4_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 4)
vm%sp = vm%sp - 4
if (ubound (vm%data, 1) < i * 4) then
write (error_unit, '("VM data access error")')
stop 1
end if
call int32_to_vm_bytes (x, vm%data, i * 4)
end subroutine store_int32
subroutine jmp (vm)
type(vm_t), intent(inout) :: vm
!
! Add the 32-bit data at pc to pc itself.
!
integer(kind = rik) :: x
if (vm%code%length() <= vm%pc + 4) call pc_error
call int32_from_vm_bytes (x, vm%code%bytes, vm%pc)
vm%pc = vm%pc + x
end subroutine jmp
subroutine jz (vm)
type(vm_t), intent(inout) :: vm
!
! Conditionally add the 32-bit data at pc to pc itself.
!
integer(kind = rik) :: x
call ensure_there_is_enough_stack_data (vm, 4_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 4)
vm%sp = vm%sp - 4
if (x == 0) then
if (vm%code%length() <= vm%pc + 4) call pc_error
call int32_from_vm_bytes (x, vm%code%bytes, vm%pc)
vm%pc = vm%pc + x
else
vm%pc = vm%pc + 4
end if
end subroutine jz
subroutine alu_neg (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x
call ensure_there_is_enough_stack_data (vm, 4_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 4)
x = -x
call int32_to_vm_bytes (x, vm%stack, vm%sp - 4)
end subroutine alu_neg
subroutine alu_not (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x
call ensure_there_is_enough_stack_data (vm, 4_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 4)
x = bool2int (x == 0_rik)
call int32_to_vm_bytes (x, vm%stack, vm%sp - 4)
end subroutine alu_not
subroutine alu_add (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = x + y
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_add
subroutine alu_sub (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = x - y
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_sub
subroutine alu_mul (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = x * y
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_mul
subroutine alu_div (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = x / y ! This works like / in C.
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_div
subroutine alu_mod (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = mod (x, y) ! This works like % in C.
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_mod
subroutine alu_lt (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x < y)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_lt
subroutine alu_gt (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x > y)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_gt
subroutine alu_le (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x <= y)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_le
subroutine alu_ge (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x >= y)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_ge
subroutine alu_eq (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x == y)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_eq
subroutine alu_ne (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x /= y)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_ne
subroutine alu_and (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x /= 0 .and. y /= 0)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_and
subroutine alu_or (vm)
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x, y, z
call ensure_there_is_enough_stack_data (vm, 8_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 8)
call int32_from_vm_bytes (y, vm%stack, vm%sp - 4)
z = bool2int (x /= 0 .or. y /= 0)
call int32_to_vm_bytes (z, vm%stack, vm%sp - 8)
vm%sp = vm%sp - 4
end subroutine alu_or
subroutine ensure_there_is_enough_stack_data (vm, n)
type(vm_t), intent(in) :: vm
integer(kind = rik), intent(in) :: n
if (vm%sp < n) then
write (error_unit, '("VM stack underflow")')
stop 1
end if
end subroutine ensure_there_is_enough_stack_data
subroutine prtc (outp, vm)
integer, intent(in) :: outp
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x
call ensure_there_is_enough_stack_data (vm, 4_rik)
call uint32_from_vm_bytes (x, vm%stack, vm%sp - 4)
write (outp, '(A1)', advance = 'no') char (x, kind = ck)
vm%sp = vm%sp - 4
end subroutine prtc
subroutine prti (outp, vm)
integer, intent(in) :: outp
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x
call ensure_there_is_enough_stack_data (vm, 4_rik)
call int32_from_vm_bytes (x, vm%stack, vm%sp - 4)
write (outp, '(I0)', advance = 'no') x
vm%sp = vm%sp - 4
end subroutine prti
subroutine prts (outp, vm)
integer, intent(in) :: outp
type(vm_t), intent(inout) :: vm
integer(kind = rik) :: x
integer(kind = rik) :: i, j
call ensure_there_is_enough_stack_data (vm, 4_rik)
call uint32_from_vm_bytes (x, vm%stack, vm%sp - 4)
if (ubound (vm%string_boundaries, 1) - 1 < x) then
write (error_unit, '("VM string boundary error")')
stop 1
end if
i = vm%string_boundaries(x)
j = vm%string_boundaries(x + 1)
write (outp, '(A)', advance = 'no') vm%strings((i + 1):j)
vm%sp = vm%sp - 4
end subroutine prts
subroutine pc_error
write (error_unit, '("VM program counter error")')
stop 1
end subroutine pc_error
end module vm_runner
program vm
use, intrinsic :: iso_fortran_env, only: input_unit
use, intrinsic :: iso_fortran_env, only: output_unit
use, intrinsic :: iso_fortran_env, only: error_unit
use, non_intrinsic :: compiler_type_kinds
use, non_intrinsic :: string_buffers
use, non_intrinsic :: vm_reader
use, non_intrinsic :: vm_runner
implicit none
integer, parameter :: inp_unit_no = 100
integer, parameter :: outp_unit_no = 101
integer :: arg_count
character(200) :: arg
integer :: inp
integer :: outp
arg_count = command_argument_count ()
if (3 <= arg_count) then
call print_usage
else
if (arg_count == 0) then
inp = input_unit
outp = output_unit
else if (arg_count == 1) then
call get_command_argument (1, arg)
inp = open_for_input (trim (arg))
outp = output_unit
else if (arg_count == 2) then
call get_command_argument (1, arg)
inp = open_for_input (trim (arg))
call get_command_argument (2, arg)
outp = open_for_output (trim (arg))
end if
block
type(strbuf_t) :: strbuf
type(vm_t) :: vm
call read_vm (inp, strbuf, vm)
call run_vm (outp, vm)
end block
end if
contains
function open_for_input (filename) result (unit_no)
character(*), intent(in) :: filename
integer :: unit_no
integer :: stat
open (unit = inp_unit_no, file = filename, status = 'old', &
& action = 'read', access = 'stream', form = 'unformatted', &
& iostat = stat)
if (stat /= 0) then
write (error_unit, '("Error: failed to open ", 1A, " for input")') filename
stop 1
end if
unit_no = inp_unit_no
end function open_for_input
function open_for_output (filename) result (unit_no)
character(*), intent(in) :: filename
integer :: unit_no
integer :: stat
open (unit = outp_unit_no, file = filename, action = 'write', iostat = stat)
if (stat /= 0) then
write (error_unit, '("Error: failed to open ", 1A, " for output")') filename
stop 1
end if
unit_no = outp_unit_no
end function open_for_output
subroutine print_usage
character(200) :: progname
call get_command_argument (0, progname)
write (output_unit, '("Usage: ", 1A, " [INPUT_FILE [OUTPUT_FILE]]")') &
& trim (progname)
end subroutine print_usage
end program vm