retroforth/vm/nga-python/retro.py
crc cb22c59b5a add support multiple bases
This set of patches:

- adds a `Base` variable
- adds `decimal`, `hex`, `octal`, and `binary`
- extends `s:to-number` to support `Base`
- extends `n:to-string` to support `Base`
- updates the glossary to cover these

Additionally:

- removes the 1024 cell limit for the kernel
- adds a header cell (7) for the start of the text input buffer
- adds a header cell (8) for the end of the text input buffer
- updates most Nga implementations that use the text input buffer to look at
  this instead of hard coding the address

FossilOrigin-Name: 391ad6fc0187e5ff3089d5c42d5d5e1c18fd0b2379c90a148ba44bb66145f020
2021-05-18 19:42:35 +00:00

747 lines
24 KiB
Python

#!/usr/bin/env python3
# Nga: a Virtual Machine
#
# Copyright (c) 2010 - 2021, Charles Childers
# Floating Point I/O by Arland Childers, (c) 2020
# Pythonista UI support by Arland Childers, (c) 2021
# Optimizations and process() rewrite by Greg Copeland
# -------------------------------------------------------------
# This implementation of the VM differs from the reference
# model in a number of important ways.
#
# To aid in performance, it does the following:
# - caching the Retro dictionary in a Python dict()
# - replaces some Retro words with implementations in Python
# - s:eq?
# - s:length
# - s:to-number
# - d:lookup
#
# Each major component is managed as a separate class. We have
# a class for each I/O device, for each stack, and for the
# memory pool. The main VM is also in a separate class.
#
# It's intended that an amalgamation tool will be developed to
# combine the separate files into a single one for deployment.
# -------------------------------------------------------------
import os, sys, math, time, struct, random, datetime, decimal
from ClockDevice import Clock
from RNGDevice import RNG
from FileSystemDevice import FileSystem
from FloatStack import FloatStack
from DecimalDevice import DecimalStack
from IntegerStack import IntegerStack
from Memory import Memory
from InitialImage import InitialImage
from BenchmarkDevice import BenchmarkDevice
# -------------------------------------------------------------
# To Support Python 2.x
# -------------------------------------------------------------
if sys.version_info.major == 3:
pass
elif sys.version_info.major == 2:
try:
input = raw_input
except NameError:
pass
# -------------------------------------------------------------
# For Pythonista users
# -------------------------------------------------------------
try:
import ui
from UIDevice import UI
except:
pass
class Retro:
def map_in(self, name):
return self.memory[self.find_entry(name) + 1]
def __init__(self):
self.ip = 0
self.stack = IntegerStack()
self.address = IntegerStack()
self.memory = Memory("ngaImage", InitialImage, 1000000)
self.clock = Clock()
self.rng = RNG()
self.files = FileSystem()
self.floats = FloatStack()
self.afloats = FloatStack()
self.decimals = DecimalStack()
self.adecimals = DecimalStack()
self.Dictionary = self.populate_dictionary()
self.Cached = self.cache_words()
try:
self.ui = UI('RETRO', 10, 10)
except:
pass
self.setup_devices()
self.instructions = [
self.i_nop,
self.i_lit,
self.i_dup,
self.i_drop,
self.i_swap,
self.i_push,
self.i_pop,
self.i_jump,
self.i_call,
self.i_ccall,
self.i_return,
self.i_eq,
self.i_neq,
self.i_lt,
self.i_gt,
self.i_fetch,
self.i_store,
self.i_add,
self.i_subtract,
self.i_multiply,
self.i_divmod,
self.i_and,
self.i_or,
self.i_xor,
self.i_shift,
self.i_zreturn,
self.i_halt,
self.i_ienumerate,
self.i_iquery,
self.i_iinvoke,
]
def div_mod(self, a, b):
x = abs(a)
y = abs(b)
q, r = divmod(x, y)
if a < 0 and b < 0:
r *= -1
elif a > 0 and b < 0:
q *= -1
elif a < 0 and b > 0:
r *= -1
q *= -1
return q, r
def cache_words(self):
Cached = dict()
Cached["interpreter"] = self.map_in("interpret")
Cached["not_found"] = self.map_in("err:notfound")
Cached["s:eq?"] = self.map_in("s:eq?")
Cached["s:to-number"] = self.map_in("s:to-number")
Cached["s:length"] = self.map_in("s:length")
Cached["d:lookup"] = self.map_in("d:lookup")
Cached["d:add-header"] = self.map_in("d:add-header")
return Cached
def populate_dictionary(self):
Dictionary = dict()
header = self.memory[2]
while header != 0:
named = self.extract_string(header + 3)
if not named in Dictionary:
Dictionary[named] = header
header = self.memory[header]
return Dictionary
def find_entry(self, named):
if named in self.Dictionary:
return self.Dictionary[named]
header = self.memory[2]
Done = False
while header != 0 and not Done:
if named == self.extract_string(header + 3):
self.Dictionary[named] = header
Done = True
else:
header = self.memory[header]
return header
def get_input(self):
return ord(sys.stdin.read(1))
def display_character(self):
if self.stack.tos() > 0 and self.stack.tos() < 128:
if self.stack.tos() == 8:
sys.stdout.write(chr(self.stack.pop()))
sys.stdout.write(chr(32))
sys.stdout.write(chr(8))
else:
sys.stdout.write(chr(self.stack.pop()))
else:
sys.stdout.write("\033[2J\033[1;1H")
self.stack.pop()
sys.stdout.flush()
def i_nop(self):
pass
def i_lit(self):
self.ip += 1
self.stack.push(self.memory[self.ip])
def i_dup(self):
self.stack.dup()
def i_drop(self):
self.stack.drop()
def i_swap(self):
self.stack.swap()
def i_push(self):
self.address.push(self.stack.pop())
def i_pop(self):
self.stack.push(self.address.pop())
def i_jump(self):
self.ip = self.stack.pop() - 1
def i_call(self):
self.address.push(self.ip)
self.ip = self.stack.pop() - 1
def i_ccall(self):
target = self.stack.pop()
if self.stack.pop() != 0:
self.address.push(self.ip)
self.ip = target - 1
def i_return(self):
self.ip = self.address.pop()
def i_eq(self):
a = self.stack.pop()
b = self.stack.pop()
if b == a:
self.stack.push(-1)
else:
self.stack.push(0)
def i_neq(self):
a = self.stack.pop()
b = self.stack.pop()
if b != a:
self.stack.push(-1)
else:
self.stack.push(0)
def i_lt(self):
a = self.stack.pop()
b = self.stack.pop()
if b < a:
self.stack.push(-1)
else:
self.stack.push(0)
def i_gt(self):
a = self.stack.pop()
b = self.stack.pop()
if b > a:
self.stack.push(-1)
else:
self.stack.push(0)
# The fetch instruction also handles certain
# introspection queries.
#
# Of note is the min and max values for a cell.
# In most VM implementations, this is limited
# to 32 bit or 64 bit ranges, but Python allows
# an unlimited range.
#
# I report as if the cells are capped at 128 bits
# but you can safely ignore this if running on
# the Python VM. (This does have an impact on
# floating point values, if using the `e:` words
# for converting them to/from an encoded format in
# standard cells, but should not affect anything
# else in the standard system)
def i_fetch_query(self, target):
if target == -1:
self.stack.push(self.stack.depth())
elif target == -2:
self.stack.push(self.address.depth())
elif target == -3:
self.stack.push(self.memory.size())
elif target == -4:
self.stack.push(-170141183460469231731687303715884105728)
elif target == -5:
self.stack.push(170141183460469231731687303715884105727)
else:
raise IndexError
def i_fetch(self):
target = self.stack.pop()
if target >= 0:
self.stack.push(self.memory[target])
else:
self.i_fetch_query(target)
def i_store(self):
mi = self.stack.pop()
self.memory[mi] = self.stack.pop()
def i_add(self):
t = self.stack.pop()
v = self.stack.pop()
self.stack.push(t + v)
def i_subtract(self):
t = self.stack.pop()
v = self.stack.pop()
self.stack.push(v - t)
def i_multiply(self):
t = self.stack.pop()
v = self.stack.pop()
self.stack.push(v * t)
def i_divmod(self):
t = self.stack.pop()
v = self.stack.pop()
b, a = self.div_mod(v, t)
self.stack.push(a)
self.stack.push(b)
def i_and(self):
t = self.stack.pop()
m = self.stack.pop()
self.stack.push(m & t)
def i_or(self):
t = self.stack.pop()
m = self.stack.pop()
self.stack.push(m | t)
def i_xor(self):
t = self.stack.pop()
m = self.stack.pop()
self.stack.push(m ^ t)
def i_shift(self):
t = self.stack.pop()
v = self.stack.pop()
if t < 0:
v <<= t * -1
else:
v >>= t
self.stack.push(v)
def i_zreturn(self):
if self.stack.tos() == 0:
self.stack.pop()
self.ip = self.address.pop()
def i_halt(self):
self.ip = 9000000
def i_ienumerate(self):
self.stack.push(8)
def i_iquery(self):
device = self.stack.pop()
if device == 0: # generic output
self.stack.push(0)
self.stack.push(0)
if device == 1: # floating point
self.stack.push(1)
self.stack.push(2)
if device == 2: # files
self.stack.push(0)
self.stack.push(4)
if device == 3: # rng
self.stack.push(0)
self.stack.push(10)
if device == 4: # time
self.stack.push(0)
self.stack.push(5)
if device == 5: # scripting
self.stack.push(0)
self.stack.push(9)
if device == 6: # decimal
self.stack.push(0)
self.stack.push(20)
if device == 7: # Pythonista UI
self.stack.push(0)
self.stack.push(941687072)
def file_open_params(self):
mode = self.stack.pop()
name = self.extract_string(self.stack.pop())
return name, mode
def file_write_params(self):
slot = self.stack.pop()
char = self.stack.pop()
return slot, char
def setup_devices(self):
self.files_instr = {
0: lambda: self.stack.push(self.files.open(self.file_open_params())),
1: lambda: self.files.close(self.stack.pop()),
2: lambda: self.stack.push(self.files.read(self.stack.pop())),
3: lambda: self.files.write(self.file_write_params()),
4: lambda: self.stack.push(self.files.pos(self.stack.pop())),
5: lambda: self.files.seek(),
6: lambda: self.stack.push(self.files.size(self.stack.pop())),
7: lambda: self.files.delete(self.extract_string(self.stack.pop())),
8: lambda: 1 + 1,
}
self.rng_instr = {0: lambda: self.stack.push(self.rng())}
self.clock_instr = {
0: lambda: self.stack.push(int(time.time())),
1: lambda: self.stack.push(self.clock["day"]),
2: lambda: self.stack.push(self.clock["month"]),
3: lambda: self.stack.push(self.clock["year"]),
4: lambda: self.stack.push(self.clock["hour"]),
5: lambda: self.stack.push(self.clock["minute"]),
6: lambda: self.stack.push(self.clock["second"]),
7: lambda: self.stack.push(self.clock["day_utc"]),
8: lambda: self.stack.push(self.clock["month_utc"]),
9: lambda: self.stack.push(self.clock["year_utc"]),
10: lambda: self.stack.push(self.clock["hour_utc"]),
11: lambda: self.stack.push(self.clock["minute_utc"]),
12: lambda: self.stack.push(self.clock["second_utc"]),
}
self.float_instr = {
0: lambda: self.floats.push(float(self.stack.pop())),
1: lambda: self.floats.push(float(self.extract_string(self.stack.pop()))),
2: lambda: self.stack.push(int(self.floats.pop())),
3: lambda: self.inject_string(str(self.floats.pop()), self.stack.pop()),
4: lambda: self.floats.add(),
5: lambda: self.floats.sub(),
6: lambda: self.floats.mul(),
7: lambda: self.floats.div(),
8: lambda: self.floats.floor(),
9: lambda: self.floats.ceil(),
10: lambda: self.floats.sqrt(),
11: lambda: self.stack.push(self.floats.eq()),
12: lambda: self.stack.push(self.floats.neq()),
13: lambda: self.stack.push(self.floats.lt()),
14: lambda: self.stack.push(self.floats.gt()),
15: lambda: self.stack.push(self.floats.depth()),
16: lambda: self.floats.dup(),
17: lambda: self.floats.drop(),
18: lambda: self.floats.swap(),
19: lambda: self.floats.log(),
20: lambda: self.floats.pow(),
21: lambda: self.floats.sin(),
22: lambda: self.floats.cos(),
23: lambda: self.floats.tan(),
24: lambda: self.floats.asin(),
25: lambda: self.floats.atan(),
26: lambda: self.floats.acos(),
27: lambda: self.afloats.push(self.floats.pop()),
28: lambda: self.floats.push(self.afloats.pop()),
29: lambda: self.stack.push(self.afloats.depth()),
}
self.decimal_instr = {
0: lambda: self.decimals.push(decimal.Decimal(self.stack.pop())),
1: lambda: self.decimals.push(
decimal.Decimal(self.extract_string(self.stack.pop()))
),
2: lambda: self.stack.push(int(self.decimals.pop())),
3: lambda: self.inject_string(str(self.decimals.pop()), self.stack.pop()),
4: lambda: self.decimals.add(),
5: lambda: self.decimals.sub(),
6: lambda: self.decimals.mul(),
7: lambda: self.decimals.div(),
8: lambda: self.decimals.floor(),
9: lambda: self.decimals.ceil(),
10: lambda: self.decimals.sqrt(),
11: lambda: self.stack.push(self.decimals.eq()),
12: lambda: self.stack.push(self.decimals.neq()),
13: lambda: self.stack.push(self.decimals.lt()),
14: lambda: self.stack.push(self.decimals.gt()),
15: lambda: self.stack.push(self.decimals.depth()),
16: lambda: self.decimals.dup(),
17: lambda: self.decimals.drop(),
18: lambda: self.decimals.swap(),
19: lambda: self.decimals.log(),
20: lambda: self.decimals.pow(),
21: lambda: self.decimals.sin(),
22: lambda: self.decimals.cos(),
23: lambda: self.decimals.tan(),
24: lambda: self.decimals.asin(),
25: lambda: self.decimals.atan(),
26: lambda: self.decimals.acos(),
27: lambda: self.adecimals.push(self.decimals.pop()),
28: lambda: self.decimals.push(self.adecimals.pop()),
29: lambda: self.stack.push(self.adecimals.depth()),
}
def i_iinvoke(self):
device = self.stack.pop()
# print('dev:', device)
if device == 0:
self.display_character()
if device == 1:
action = self.stack.pop()
self.float_instr[int(action)]()
if device == 2:
action = self.stack.pop()
self.files_instr[int(action)]()
if device == 3:
self.rng_instr[0]()
if device == 4:
action = self.stack.pop()
self.clock_instr[int(action)]()
if device == 5:
action = self.stack.pop()
if action == 0:
self.stack.push(len(sys.argv) - 2)
if action == 1:
a = self.stack.pop()
b = self.stack.pop()
self.stack.push(self.inject_string(sys.argv[a + 2], b))
if action == 2:
prior = self.address
prior_ip = self.ip
self.address = IntegerStack()
self.run_file(self.extract_string(self.stack.pop()))
self.address = prior
self.ip = prior_ip
if action == 3:
b = self.stack.pop()
self.stack.push(self.inject_string(sys.argv[0], b))
if device == 6:
action = self.stack.pop()
self.decimal_instr[int(action)]()
if device == 7:
action = self.stack.pop()
if action == 0:
view = self.extract_string(self.stack.pop())
name = self.extract_string(self.stack.pop())
which = None
if view == 'button':
which = ui.Button()
if view == 'label':
which = ui.Label()
if view == 'textfield':
which = ui.TextField()
if view == 'textview':
which = ui.TextView()
self.ui.create_view(name, which)
if action == 1:
self.ui.add_view(self.extract_string(self.stack.pop()))
if action == 2:
self.ui.remove_view(self.extract_string(self.stack.pop()))
if action == 3:
self.ui.present()
if action == 4:
name = self.extract_string(self.stack.pop())
w = self.stack.pop()
h = self.stack.pop()
self.ui.set_size(name, h, w)
if action == 5:
name = self.extract_string(self.stack.pop())
y = self.stack.pop()
x = self.stack.pop()
self.ui.set_coords(name, x, y)
if action == 6:
name = self.extract_string(self.stack.pop())
self.stack.push(self.ui.get_size(name)[0])
self.stack.push(self.ui.get_size(name)[1])
if action == 7:
name = self.extract_string(self.stack.pop())
self.stack.push(self.ui.get_coords(name)[0])
self.stack.push(self.ui.get_coords(name)[1])
if action == 8:
name = self.extract_string(self.stack.pop())
title = self.extract_string(self.stack.pop())
self.ui.set_title(name, title)
if action == 9:
name = self.extract_string(self.stack.pop())
text = self.extract_string(self.stack.pop())
self.ui.set_text(name, text)
if action == 10:
name = self.extract_string(self.stack.pop())
self.stack.push(self.ui.get_title(name))
if action == 11:
name = self.extract_string(self.stack.pop())
self.stack.push(self.ui.get_text(name))
if action == 12:
name = self.extract_string(self.stack.pop())
handler = self.stack.pop()
self.ui.add_action(name, handler, self)
if action == 13:
name = self.extract_string(self.stack.pop())
self.stack.push(self.ui.get_action(name))
def validate_opcode(self, I0, I1, I2, I3):
if (
(I0 >= 0 and I0 <= 29)
and (I1 >= 0 and I1 <= 29)
and (I2 >= 0 and I2 <= 29)
and (I3 >= 0 and I3 <= 29)
):
return True
else:
return False
def extract_string(self, at):
s = ""
while self.memory[at] != 0:
s = s + chr(self.memory[at])
at = at + 1
return s
def inject_string(self, s, to):
for c in s:
self.memory[to] = ord(c)
to = to + 1
self.memory[to] = 0
def execute(self, word, notfound):
if self.address.depth() == 0:
self.address.push(0)
self.ip = word
while self.ip < 1000000:
if self.ip == self.Cached["s:eq?"]:
a = self.extract_string(self.stack.pop())
b = self.extract_string(self.stack.pop())
if a == b:
self.stack.push(-1)
else:
self.stack.push(0)
self.ip = self.address.pop()
elif self.ip == self.Cached["d:lookup"]:
name = self.extract_string(self.stack.pop())
header = self.find_entry(name)
self.stack.push(header)
self.memory[self.Cached["d:lookup"] - 20] = header # "which"
self.ip = self.address.pop()
elif self.ip == self.Cached["s:to-number"]:
n = self.extract_string(self.stack.pop())
self.stack.push(int(n))
self.ip = self.address.pop()
elif self.ip == self.Cached["s:length"]:
n = len(self.extract_string(self.stack.pop()))
self.stack.push(n)
self.ip = self.address.pop()
else:
if self.ip == notfound:
print("ERROR: word not found!")
if self.ip == self.Cached["d:add-header"]:
self.Dictionary[self.extract_string(self.stack[-3])] = self.memory[
3
]
opcode = self.memory[self.ip]
I0 = opcode & 0xFF
I1 = (opcode >> 8) & 0xFF
I2 = (opcode >> 16) & 0xFF
I3 = (opcode >> 24) & 0xFF
if self.validate_opcode(I0, I1, I2, I3):
# print("Bytecode: ", I0, I1, I2, I3, "at", self.ip)
if I0 != 0:
self.instructions[I0]()
if I1 != 0:
self.instructions[I1]()
if I2 != 0:
self.instructions[I2]()
if I3 != 0:
self.instructions[I3]()
else:
print("Invalid Bytecode: ", opcode, "at", self.ip)
self.ip = 2000000
if self.address.depth() == 0:
self.ip = 2000000
self.ip = self.ip + 1
return
def run(self):
done = False
while not done:
line = input("\nOk> ")
if line == "bye":
done = True
else:
for token in line.split():
self.inject_string(token, self.memory[7])
self.stack.push(1024)
self.execute(self.Cached["interpreter"], self.Cached["not_found"])
def run_file(self, file):
if not os.path.exists(file):
print("File '{0}' not found".format(file))
return
in_block = False
with open(file, "r") as source:
for line in source.readlines():
if line.rstrip() == "~~~":
in_block = not in_block
elif in_block:
for token in line.strip().split():
self.inject_string(token, self.memory[7])
self.stack.push(1024)
self.execute(
self.Cached["interpreter"], self.Cached["not_found"]
)
def update_image(self):
import requests
import shutil
data = requests.get("https://forthworks.com/retro/ngaImage", stream=True)
with open("ngaImage", "wb") as f:
data.raw.decode_content = True
shutil.copyfileobj(data.raw, f)
if __name__ == "__main__":
retro = Retro()
if len(sys.argv) == 1:
retro.run()
if len(sys.argv) == 2:
retro.run_file(sys.argv[1])
sources = []
interactive = False
if len(sys.argv) > 2:
i = 1
e = len(sys.argv)
while i < e:
param = sys.argv[i]
if param == "-f":
i += 1
sources.append(sys.argv[i])
elif param == "-u":
i += 1
retro.memory.load_image(sys.argv[i])
elif param == "-i":
interactive = True
i += 1
if len(sys.argv) > 2 and sys.argv[1][0] != "-":
retro.run_file(sys.argv[1])
else:
for source in sources:
retro.run_file(source)
if interactive:
retro.run()